From c30f124c917b55a93b3d4880d8d992d649a076f0 Mon Sep 17 00:00:00 2001 From: Alan Viverette Date: Tue, 20 Aug 2013 15:58:38 -0700 Subject: [PATCH] Fix background and line wrapping in CaptionTextView Also fixes where two style names were accidentally switched, makes the font sizes larger, and cleans up the CaptionTextView styling APIs in preparation for moving it into the framework. BUG: 10396663 Change-Id: I00999723a67bce2659d913b70dd4420ed32f955c --- res/layout/preset_picker_item.xml | 6 +- res/values/arrays.xml | 6 +- .../accessibility/CaptioningTextView.java | 541 ++++++++++-------- .../accessibility/EdgeTypePreference.java | 6 +- .../ToggleCaptioningPreferenceFragment.java | 5 +- 5 files changed, 315 insertions(+), 249 deletions(-) diff --git a/res/layout/preset_picker_item.xml b/res/layout/preset_picker_item.xml index 6f99980fd54..1b66970b8f0 100644 --- a/res/layout/preset_picker_item.xml +++ b/res/layout/preset_picker_item.xml @@ -27,9 +27,9 @@ diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 44981152342..e58107af82a 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -735,11 +735,11 @@ - 6.0 12.0 24.0 - 32.0 48.0 + 72.0 + 96.0 @@ -854,8 +854,8 @@ - Black on white White on black + Black on white Yellow on black Yellow on blue Custom diff --git a/src/com/android/settings/accessibility/CaptioningTextView.java b/src/com/android/settings/accessibility/CaptioningTextView.java index 7bb677e83e1..d720e8fce1e 100644 --- a/src/com/android/settings/accessibility/CaptioningTextView.java +++ b/src/com/android/settings/accessibility/CaptioningTextView.java @@ -18,51 +18,273 @@ package com.android.settings.accessibility; import android.content.ContentResolver; import android.content.Context; -import android.content.res.ColorStateList; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.Paint.Cap; import android.graphics.Paint.Join; import android.graphics.Paint.Style; -import android.os.Parcel; -import android.support.v4.view.ViewCompat; -import android.text.Editable; -import android.text.ParcelableSpan; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.text.Layout.Alignment; +import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; -import android.text.style.CharacterStyle; -import android.text.style.UpdateAppearance; import android.util.AttributeSet; -import android.view.accessibility.CaptioningManager; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; import android.view.accessibility.CaptioningManager.CaptionStyle; -import android.widget.TextView; -public class CaptioningTextView extends TextView { - private MutableBackgroundColorSpan mBackgroundSpan; - private ColorStateList mOutlineColorState; - private float mOutlineWidth; - private int mOutlineColor; +public class CaptioningTextView extends View { + // Ratio of inner padding to font size. + private static final float INNER_PADDING_RATIO = 0.125f; - private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE; - private int mEdgeColor = Color.TRANSPARENT; - private float mEdgeWidth = 0; + // Default style dimensions in dips. + private static final float CORNER_RADIUS = 2.0f; + private static final float OUTLINE_WIDTH = 2.0f; + private static final float SHADOW_RADIUS = 2.0f; + private static final float SHADOW_OFFSET_X = 2.0f; + private static final float SHADOW_OFFSET_Y = 2.0f; - private boolean mHasBackground = false; + // Styled dimensions. + private final float mCornerRadius; + private final float mOutlineWidth; + private final float mShadowRadius; + private final float mShadowOffsetX; + private final float mShadowOffsetY; - public CaptioningTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } + /** Temporary rectangle used for computing line bounds. */ + private final RectF mLineBounds = new RectF(); + + /** Temporary array used for computing line wrapping. */ + private float[] mTextWidths; + + /** Reusable string builder used for holding text. */ + private final StringBuilder mText = new StringBuilder(); + private final StringBuilder mBreakText = new StringBuilder(); + + private TextPaint mPaint; + + private int mForegroundColor; + private int mBackgroundColor; + private int mEdgeColor; + private int mEdgeType; + + private boolean mHasMeasurements; + private int mLastMeasuredWidth; + private StaticLayout mLayout; + + private float mSpacingMult = 1; + private float mSpacingAdd = 0; + private int mInnerPaddingX = 0; public CaptioningTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CaptioningTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); + + final Theme theme = context.getTheme(); + final TypedArray a = theme.obtainStyledAttributes( + attrs, android.R.styleable.TextView, defStyle, 0); + + CharSequence text = ""; + int textSize = 15; + + final int n = a.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case android.R.styleable.TextView_text: + text = a.getText(attr); + break; + case android.R.styleable.TextView_lineSpacingExtra: + mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd); + break; + case android.R.styleable.TextView_lineSpacingMultiplier: + mSpacingMult = a.getFloat(attr, mSpacingMult); + break; + case android.R.styleable.TextAppearance_textSize: + textSize = a.getDimensionPixelSize(attr, textSize); + break; + } + } + + // Set up density-dependent properties. + // TODO: Move these to a default style. + final DisplayMetrics m = getContext().getResources().getDisplayMetrics(); + mCornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CORNER_RADIUS, m); + mOutlineWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, OUTLINE_WIDTH, m); + mShadowRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_RADIUS, m); + mShadowOffsetX = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_OFFSET_Y, m); + mShadowOffsetY = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_OFFSET_X, m); + + final TextPaint paint = new TextPaint(); + paint.setAntiAlias(true); + paint.setSubpixelText(true); + + mPaint = paint; + + setText(text); + setTextSize(textSize); } - public CaptioningTextView(Context context) { - super(context); + public void setText(int resId) { + final CharSequence text = getContext().getText(resId); + setText(text); } - public void applyStyleAndFontSize(int styleId) { + public void setText(CharSequence text) { + mText.setLength(0); + mText.append(text); + + mHasMeasurements = false; + + requestLayout(); + } + + public void setForegroundColor(int color) { + mForegroundColor = color; + + invalidate(); + } + + @Override + public void setBackgroundColor(int color) { + mBackgroundColor = color; + + invalidate(); + } + + public void setEdgeType(int edgeType) { + mEdgeType = edgeType; + + invalidate(); + } + + public void setEdgeColor(int color) { + mEdgeColor = color; + + invalidate(); + } + + public void setTextSize(float size) { + final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + final float pixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, size, metrics); + if (mPaint.getTextSize() != size) { + mHasMeasurements = false; + mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f); + mPaint.setTextSize(size); + + requestLayout(); + } + } + + public void setTypeface(Typeface typeface) { + if (mPaint.getTypeface() != typeface) { + mHasMeasurements = false; + mPaint.setTypeface(typeface); + + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthSpec = MeasureSpec.getSize(widthMeasureSpec); + + if (computeMeasurements(widthSpec)) { + final StaticLayout layout = mLayout; + + // Account for padding. + final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2; + final int width = layout.getWidth() + paddingX; + final int height = layout.getHeight() + mPaddingTop + mPaddingBottom; + setMeasuredDimension(width, height); + } else { + setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL); + } + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + final int width = r - l; + + computeMeasurements(width); + } + + private boolean computeMeasurements(int maxWidth) { + if (mHasMeasurements && maxWidth == mLastMeasuredWidth) { + return true; + } + + // Account for padding. + final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX; + maxWidth -= paddingX; + + if (maxWidth <= 0) { + return false; + } + + final TextPaint paint = mPaint; + final CharSequence text = mText; + final int textLength = text.length(); + if (mTextWidths == null || mTextWidths.length < textLength) { + mTextWidths = new float[textLength]; + } + + final float[] textWidths = mTextWidths; + paint.getTextWidths(text, 0, textLength, textWidths); + + // Compute total length. + float runLength = 0; + for (int i = 0; i < textLength; i++) { + runLength += textWidths[i]; + } + + final int lineCount = (int) (runLength / maxWidth) + 1; + final int lineLength = (int) (runLength / lineCount); + + // Build line break buffer. + final StringBuilder breakText = mBreakText; + breakText.setLength(0); + + int line = 0; + int lastBreak = 0; + int maxRunLength = 0; + runLength = 0; + for (int i = 0; i < textLength; i++) { + if (runLength > lineLength) { + final CharSequence sequence = text.subSequence(lastBreak, i); + final int trimmedLength = TextUtils.getTrimmedLength(sequence); + breakText.append(sequence, 0, trimmedLength); + breakText.append('\n'); + lastBreak = i; + runLength = 0; + } + + runLength += textWidths[i]; + + if (runLength > maxRunLength) { + maxRunLength = (int) Math.ceil(runLength); + } + } + breakText.append(text.subSequence(lastBreak, textLength)); + + mHasMeasurements = true; + mLastMeasuredWidth = maxWidth; + + mLayout = new StaticLayout(breakText, paint, maxRunLength, Alignment.ALIGN_LEFT, + mSpacingMult, mSpacingAdd, true); + + return true; + } + + public void setStyle(int styleId) { final Context context = mContext; final ContentResolver cr = context.getContentResolver(); final CaptionStyle style; @@ -72,238 +294,77 @@ public class CaptioningTextView extends TextView { style = CaptionStyle.PRESETS[styleId]; } - setTextColor(style.foregroundColor); - setBackgroundColor(style.backgroundColor); - setTypeface(style.getTypeface()); + mForegroundColor = style.foregroundColor; + mBackgroundColor = style.backgroundColor; + mEdgeType = style.edgeType; + mEdgeColor = style.edgeColor; + mHasMeasurements = false; - // Clears all outlines. - applyEdge(style.edgeType, style.edgeColor, 4.0f); + final Typeface typeface = style.getTypeface(); + setTypeface(typeface); - final float fontSize = CaptioningManager.getFontSize(cr); - if (fontSize != 0) { - setTextSize(fontSize); - } - } - - /** - * Applies an edge preset using a combination of {@link #setOutlineLayer} - * and {@link #setShadowLayer}. Any subsequent calls to either of these - * methods will invalidate the applied preset. - * - * @param type Type of edge to apply, one of: - *
    - *
  • {@link CaptionStyle#EDGE_TYPE_NONE} - *
  • {@link CaptionStyle#EDGE_TYPE_OUTLINE} - *
  • {@link CaptionStyle#EDGE_TYPE_DROP_SHADOW} - *
- * @param color Edge color as a packed 32-bit ARGB color. - * @param width Width of the edge in pixels. - */ - public void applyEdge(int type, int color, float width) { - if (mEdgeType != type || mEdgeColor != color || mEdgeWidth != width) { - final int textColor = getTextColors().getDefaultColor(); - switch (type) { - case CaptionStyle.EDGE_TYPE_DROP_SHADOW: - setOutlineLayer(0, 0); - super.setShadowLayer(width, width, width, color); - break; - case CaptionStyle.EDGE_TYPE_OUTLINE: - setOutlineLayer(width, color); - super.setShadowLayer(0, 0, 0, 0); - break; - default: - super.setShadowLayer(0, 0, 0, 0); - setOutlineLayer(0, 0); - } - - mEdgeType = type; - mEdgeColor = color; - mEdgeWidth = width; - } - } - - @Override - public void setShadowLayer(float radius, float dx, float dy, int color) { - mEdgeType = CaptionStyle.EDGE_TYPE_NONE; - - super.setShadowLayer(radius, dx, dy, color); - } - - /** - * Gives the text an outline of the specified pixel width and color. - */ - public void setOutlineLayer(float width, int color) { - width *= 2.0f; - - mEdgeType = CaptionStyle.EDGE_TYPE_NONE; - - if (mOutlineColor != color || mOutlineWidth != width) { - mOutlineColorState = ColorStateList.valueOf(color); - mOutlineColor = color; - mOutlineWidth = width; - invalidate(); - - // TODO: Remove after display list bug is fixed. - if (width > 0 && Color.alpha(color) != 0) { - setLayerType(ViewCompat.LAYER_TYPE_SOFTWARE, null); - } else { - setLayerType(ViewCompat.LAYER_TYPE_HARDWARE, null); - } - } - } - - /** - * @return the color of the outline layer - * @see #setOutlineLayer(float, int) - */ - public int getOutlineColor() { - return mOutlineColor; - } - - /** - * @return the width of the outline layer - * @see #setOutlineLayer(float, int) - */ - public float getOutlineWidth() { - return mOutlineWidth; - } - - @Override - public Editable getEditableText() { - final CharSequence text = getText(); - if (text instanceof Editable) { - return (Editable) text; - } - - setText(text, BufferType.EDITABLE); - return (Editable) getText(); - } - - @Override - public void setBackgroundColor(int color) { - if (Color.alpha(color) == 0) { - if (mHasBackground) { - mHasBackground = false; - getEditableText().removeSpan(mBackgroundSpan); - } - } else { - if (mBackgroundSpan == null) { - mBackgroundSpan = new MutableBackgroundColorSpan(color); - } else { - mBackgroundSpan.setColor(color); - } - - if (mHasBackground) { - invalidate(); - } else { - mHasBackground = true; - getEditableText().setSpan(mBackgroundSpan, 0, length(), 0); - } - } - } - - @Override - protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { - super.onTextChanged(text, start, lengthBefore, lengthAfter); - - if (mBackgroundSpan != null) { - getEditableText().setSpan(mBackgroundSpan, 0, lengthAfter, 0); - } + requestLayout(); } @Override protected void onDraw(Canvas c) { - if (mOutlineWidth > 0 && Color.alpha(mOutlineColor) > 0) { - final TextPaint textPaint = getPaint(); - final Paint.Style previousStyle = textPaint.getStyle(); - final ColorStateList previousColors = getTextColors(); - textPaint.setStyle(Style.STROKE); - textPaint.setStrokeWidth(mOutlineWidth); - textPaint.setStrokeCap(Cap.ROUND); - textPaint.setStrokeJoin(Join.ROUND); + final StaticLayout layout = mLayout; + if (layout == null) { + return; + } - setTextColor(mOutlineColorState); + final int saveCount = c.save(); + final int innerPaddingX = mInnerPaddingX; + c.translate(mPaddingLeft + innerPaddingX, mPaddingTop); - // Remove the shadow. - final float shadowRadius = getShadowRadius(); - final float shadowDx = getShadowDx(); - final float shadowDy = getShadowDy(); - final int shadowColor = getShadowColor(); - if (shadowRadius > 0) { - setShadowLayer(0, 0, 0, 0); + final RectF bounds = mLineBounds; + final int lineCount = layout.getLineCount(); + final Paint paint = layout.getPaint(); + paint.setShadowLayer(0, 0, 0, 0); + + final int backgroundColor = mBackgroundColor; + if (Color.alpha(backgroundColor) > 0) { + paint.setColor(backgroundColor); + paint.setStyle(Style.FILL); + + final float cornerRadius = mCornerRadius; + float previousBottom = layout.getLineTop(0); + + for (int i = 0; i < lineCount; i++) { + bounds.left = layout.getLineLeft(i) - innerPaddingX; + bounds.right = layout.getLineRight(i) + innerPaddingX; + bounds.top = previousBottom; + bounds.bottom = layout.getLineBottom(i); + + previousBottom = bounds.bottom; + + c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint); } + } - // Draw outline and background only. - super.onDraw(c); + final int edgeType = mEdgeType; + if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) { + paint.setColor(mEdgeColor); + paint.setStyle(Style.FILL_AND_STROKE); + paint.setStrokeJoin(Join.ROUND); + paint.setStrokeWidth(mOutlineWidth); - // Restore the shadow. - if (shadowRadius > 0) { - setShadowLayer(shadowRadius, shadowDx, shadowDy, shadowColor); + for (int i = 0; i < lineCount; i++) { + layout.drawText(c, i, i); } - - // Restore original settings. - textPaint.setStyle(previousStyle); - setTextColor(previousColors); - - // Remove the background. - final int color; - if (mBackgroundSpan != null) { - color = mBackgroundSpan.getBackgroundColor(); - mBackgroundSpan.setColor(Color.TRANSPARENT); - } else { - color = 0; - } - - // Draw foreground only. - super.onDraw(c); - - // Restore the background. - if (mBackgroundSpan != null) { - mBackgroundSpan.setColor(color); - } - } else { - super.onDraw(c); - } - } - - public static class MutableBackgroundColorSpan extends CharacterStyle - implements UpdateAppearance, ParcelableSpan { - private int mColor; - - public MutableBackgroundColorSpan(int color) { - mColor = color; } - public MutableBackgroundColorSpan(Parcel src) { - mColor = src.readInt(); + if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { + paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor); } - public void setColor(int color) { - mColor = color; + paint.setColor(mForegroundColor); + paint.setStyle(Style.FILL); + + for (int i = 0; i < lineCount; i++) { + layout.drawText(c, i, i); } - @Override - public int getSpanTypeId() { - return TextUtils.BACKGROUND_COLOR_SPAN; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mColor); - } - - public int getBackgroundColor() { - return mColor; - } - - @Override - public void updateDrawState(TextPaint ds) { - ds.bgColor = mColor; - } + c.restoreToCount(saveCount); } } diff --git a/src/com/android/settings/accessibility/EdgeTypePreference.java b/src/com/android/settings/accessibility/EdgeTypePreference.java index d0dee1d41ef..d146960f052 100644 --- a/src/com/android/settings/accessibility/EdgeTypePreference.java +++ b/src/com/android/settings/accessibility/EdgeTypePreference.java @@ -50,12 +50,14 @@ public class EdgeTypePreference extends ListDialogPreference { protected void onBindListItem(View view, int index) { final float fontSize = CaptioningManager.getFontSize(getContext().getContentResolver()); final CaptioningTextView preview = (CaptioningTextView) view.findViewById(R.id.preview); - preview.setTextColor(Color.WHITE); + + preview.setForegroundColor(Color.WHITE); preview.setBackgroundColor(Color.TRANSPARENT); preview.setTextSize(fontSize); final int value = getValueAt(index); - preview.applyEdge(value, Color.BLACK, 4.0f); + preview.setEdgeType(value); + preview.setEdgeColor(Color.BLACK); final CharSequence title = getTitleAt(index); if (title != null) { diff --git a/src/com/android/settings/accessibility/ToggleCaptioningPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleCaptioningPreferenceFragment.java index d0daf663d09..e248d14bb3a 100644 --- a/src/com/android/settings/accessibility/ToggleCaptioningPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleCaptioningPreferenceFragment.java @@ -86,10 +86,13 @@ public class ToggleCaptioningPreferenceFragment extends Fragment { } public static void applyCaptionProperties(CaptioningTextView previewText, int styleId) { - previewText.applyStyleAndFontSize(styleId); + previewText.setStyle(styleId); final Context context = previewText.getContext(); final ContentResolver cr = context.getContentResolver(); + final float fontSize = CaptioningManager.getFontSize(cr); + previewText.setTextSize(fontSize); + final Locale locale = CaptioningManager.getLocale(cr); if (locale != null) { final CharSequence localizedText = AccessibilityUtils.getTextForLocale(