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
This commit is contained in:
Alan Viverette
2013-08-20 15:58:38 -07:00
parent c7679b0ef2
commit c30f124c91
5 changed files with 315 additions and 249 deletions

View File

@@ -27,9 +27,9 @@
<com.android.settings.accessibility.CaptioningTextView
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/captioning_preview_characters" />
</FrameLayout>

View File

@@ -735,11 +735,11 @@
<!-- Values for captioning font size preference. -->
<string-array name="captioning_font_size_selector_values" translatable="false" >
<item>6.0</item>
<item>12.0</item>
<item>24.0</item>
<item>32.0</item>
<item>48.0</item>
<item>72.0</item>
<item>96.0</item>
</string-array>
<!-- Titles for captioning character edge type preference. [CHAR LIMIT=35] -->
@@ -854,8 +854,8 @@
<!-- Titles for captioning text style preset preference. [CHAR LIMIT=35] -->
<string-array name="captioning_preset_selector_titles" >
<item>Black on white</item>
<item>White on black</item>
<item>Black on white</item>
<item>Yellow on black</item>
<item>Yellow on blue</item>
<item>Custom</item>

View File

@@ -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;
}
}
public CaptioningTextView(Context context) {
super(context);
// 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 void applyStyleAndFontSize(int styleId) {
public void setText(int resId) {
final CharSequence text = getContext().getText(resId);
setText(text);
}
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:
* <ul>
* <li>{@link CaptionStyle#EDGE_TYPE_NONE}
* <li>{@link CaptionStyle#EDGE_TYPE_OUTLINE}
* <li>{@link CaptionStyle#EDGE_TYPE_DROP_SHADOW}
* </ul>
* @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);
setTextColor(mOutlineColorState);
// 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 StaticLayout layout = mLayout;
if (layout == null) {
return;
}
// Draw outline and background only.
super.onDraw(c);
final int saveCount = c.save();
final int innerPaddingX = mInnerPaddingX;
c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
// Restore the shadow.
if (shadowRadius > 0) {
setShadowLayer(shadowRadius, shadowDx, shadowDy, shadowColor);
}
final RectF bounds = mLineBounds;
final int lineCount = layout.getLineCount();
final Paint paint = layout.getPaint();
paint.setShadowLayer(0, 0, 0, 0);
// Restore original settings.
textPaint.setStyle(previousStyle);
setTextColor(previousColors);
final int backgroundColor = mBackgroundColor;
if (Color.alpha(backgroundColor) > 0) {
paint.setColor(backgroundColor);
paint.setStyle(Style.FILL);
// Remove the background.
final int color;
if (mBackgroundSpan != null) {
color = mBackgroundSpan.getBackgroundColor();
mBackgroundSpan.setColor(Color.TRANSPARENT);
} else {
color = 0;
}
final float cornerRadius = mCornerRadius;
float previousBottom = layout.getLineTop(0);
// Draw foreground only.
super.onDraw(c);
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);
// Restore the background.
if (mBackgroundSpan != null) {
mBackgroundSpan.setColor(color);
}
} else {
super.onDraw(c);
previousBottom = bounds.bottom;
c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
}
}
public static class MutableBackgroundColorSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
private int mColor;
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);
public MutableBackgroundColorSpan(int color) {
mColor = color;
for (int i = 0; i < lineCount; i++) {
layout.drawText(c, i, i);
}
}
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);
}
}

View File

@@ -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) {

View File

@@ -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(