Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 35bac199 authored by Alan Viverette's avatar Alan Viverette Committed by Android (Google) Code Review
Browse files

Merge "Fix background and line wrapping in CaptionTextView" into klp-dev

parents 0f5578ce c30f124c
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -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>

+3 −3
Original line number Diff line number Diff line
@@ -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>
+276 −215
Original line number Diff line number Diff line
@@ -18,292 +18,353 @@ 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) {
        super(context, attrs);
        this(context, attrs, 0);
    }

    public CaptioningTextView(Context context) {
        super(context);
    }
    public CaptioningTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs);

    public void applyStyleAndFontSize(int styleId) {
        final Context context = mContext;
        final ContentResolver cr = context.getContentResolver();
        final CaptionStyle style;
        if (styleId == CaptionStyle.PRESET_CUSTOM) {
            style = CaptionStyle.getCustomStyle(cr);
        } else {
            style = CaptionStyle.PRESETS[styleId];
        }
        final Theme theme = context.getTheme();
        final TypedArray a = theme.obtainStyledAttributes(
                    attrs, android.R.styleable.TextView, defStyle, 0);

        setTextColor(style.foregroundColor);
        setBackgroundColor(style.backgroundColor);
        setTypeface(style.getTypeface());
        CharSequence text = "";
        int textSize = 15;

        // Clears all outlines.
        applyEdge(style.edgeType, style.edgeColor, 4.0f);
        final int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);

        final float fontSize = CaptioningManager.getFontSize(cr);
        if (fontSize != 0) {
            setTextSize(fontSize);
            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;
            }
        }

    /**
     * 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);
        // 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);
    }

            mEdgeType = type;
            mEdgeColor = color;
            mEdgeWidth = width;
    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();
    }

    @Override
    public void setShadowLayer(float radius, float dx, float dy, int color) {
        mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
    public void setForegroundColor(int color) {
        mForegroundColor = color;

        super.setShadowLayer(radius, dx, dy, color);
        invalidate();
    }

    /**
     * Gives the text an outline of the specified pixel width and color.
     */
    public void setOutlineLayer(float width, int color) {
        width *= 2.0f;
    @Override
    public void setBackgroundColor(int color) {
        mBackgroundColor = color;

        invalidate();
    }

        mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
    public void setEdgeType(int edgeType) {
        mEdgeType = edgeType;

        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);
    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();
        }
    }

    /**
     * @return the color of the outline layer
     * @see #setOutlineLayer(float, int)
     */
    public int getOutlineColor() {
        return mOutlineColor;
    }
    public void setTypeface(Typeface typeface) {
        if (mPaint.getTypeface() != typeface) {
            mHasMeasurements = false;
            mPaint.setTypeface(typeface);

    /**
     * @return the width of the outline layer
     * @see #setOutlineLayer(float, int)
     */
    public float getOutlineWidth() {
        return mOutlineWidth;
            requestLayout();
        }
    }

    @Override
    public Editable getEditableText() {
        final CharSequence text = getText();
        if (text instanceof Editable) {
            return (Editable) text;
        }
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);

        setText(text, BufferType.EDITABLE);
        return (Editable) getText();
    }
        if (computeMeasurements(widthSpec)) {
            final StaticLayout layout = mLayout;

    @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);
            // 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 {
                mBackgroundSpan.setColor(color);
            setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
        }

            if (mHasBackground) {
                invalidate();
            } else {
                mHasBackground = true;
                getEditableText().setSpan(mBackgroundSpan, 0, length(), 0);
    }

    @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;
        }

    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        // Account for padding.
        final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX;
        maxWidth -= paddingX;

        if (mBackgroundSpan != null) {
            getEditableText().setSpan(mBackgroundSpan, 0, lengthAfter, 0);
        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];
        }

    @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);
            }

            // Draw outline and background only.
            super.onDraw(c);

            // Restore the shadow.
            if (shadowRadius > 0) {
                setShadowLayer(shadowRadius, shadowDx, shadowDy, shadowColor);
            }

            // 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;
        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];
        }

            // Draw foreground only.
            super.onDraw(c);
        final int lineCount = (int) (runLength / maxWidth) + 1;
        final int lineLength = (int) (runLength / lineCount);

        // Build line break buffer.
        final StringBuilder breakText = mBreakText;
        breakText.setLength(0);

            // Restore the background.
            if (mBackgroundSpan != null) {
                mBackgroundSpan.setColor(color);
        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;
            }
        } else {
            super.onDraw(c);

            runLength += textWidths[i];

            if (runLength > maxRunLength) {
                maxRunLength = (int) Math.ceil(runLength);
            }
        }
        breakText.append(text.subSequence(lastBreak, textLength));

    public static class MutableBackgroundColorSpan extends CharacterStyle
            implements UpdateAppearance, ParcelableSpan {
        private int mColor;
        mHasMeasurements = true;
        mLastMeasuredWidth = maxWidth;

        public MutableBackgroundColorSpan(int color) {
            mColor = color;
        mLayout = new StaticLayout(breakText, paint, maxRunLength, Alignment.ALIGN_LEFT,
                mSpacingMult, mSpacingAdd, true);

        return true;
    }

        public MutableBackgroundColorSpan(Parcel src) {
            mColor = src.readInt();
    public void setStyle(int styleId) {
        final Context context = mContext;
        final ContentResolver cr = context.getContentResolver();
        final CaptionStyle style;
        if (styleId == CaptionStyle.PRESET_CUSTOM) {
            style = CaptionStyle.getCustomStyle(cr);
        } else {
            style = CaptionStyle.PRESETS[styleId];
        }

        public void setColor(int color) {
            mColor = color;
        mForegroundColor = style.foregroundColor;
        mBackgroundColor = style.backgroundColor;
        mEdgeType = style.edgeType;
        mEdgeColor = style.edgeColor;
        mHasMeasurements = false;

        final Typeface typeface = style.getTypeface();
        setTypeface(typeface);

        requestLayout();
    }

    @Override
        public int getSpanTypeId() {
            return TextUtils.BACKGROUND_COLOR_SPAN;
    protected void onDraw(Canvas c) {
        final StaticLayout layout = mLayout;
        if (layout == null) {
            return;
        }

        @Override
        public int describeContents() {
            return 0;
        final int saveCount = c.save();
        final int innerPaddingX = mInnerPaddingX;
        c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);

        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);
            }
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(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);

            for (int i = 0; i < lineCount; i++) {
                layout.drawText(c, i, i);
            }
        }

        public int getBackgroundColor() {
            return mColor;
        if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
            paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            ds.bgColor = mColor;
        paint.setColor(mForegroundColor);
        paint.setStyle(Style.FILL);

        for (int i = 0; i < lineCount; i++) {
            layout.drawText(c, i, i);
        }

        c.restoreToCount(saveCount);
    }
}
+4 −2
Original line number Diff line number Diff line
@@ -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) {
+4 −1
Original line number Diff line number Diff line
@@ -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(