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

Commit c30f124c authored by Alan Viverette's avatar Alan Viverette
Browse files

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
parent c7679b0e
Loading
Loading
Loading
Loading
+3 −3
Original line number Original line Diff line number Diff line
@@ -27,9 +27,9 @@


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


+3 −3
Original line number Original line Diff line number Diff line
@@ -735,11 +735,11 @@


    <!-- Values for captioning font size preference. -->
    <!-- Values for captioning font size preference. -->
    <string-array name="captioning_font_size_selector_values" translatable="false" >
    <string-array name="captioning_font_size_selector_values" translatable="false" >
        <item>6.0</item>
        <item>12.0</item>
        <item>12.0</item>
        <item>24.0</item>
        <item>24.0</item>
        <item>32.0</item>
        <item>48.0</item>
        <item>48.0</item>
        <item>72.0</item>
        <item>96.0</item>
    </string-array>
    </string-array>


    <!-- Titles for captioning character edge type preference. [CHAR LIMIT=35] -->
    <!-- Titles for captioning character edge type preference. [CHAR LIMIT=35] -->
@@ -854,8 +854,8 @@


    <!-- Titles for captioning text style preset preference. [CHAR LIMIT=35] -->
    <!-- Titles for captioning text style preset preference. [CHAR LIMIT=35] -->
    <string-array name="captioning_preset_selector_titles" >
    <string-array name="captioning_preset_selector_titles" >
        <item>Black on white</item>
        <item>White on black</item>
        <item>White on black</item>
        <item>Black on white</item>
        <item>Yellow on black</item>
        <item>Yellow on black</item>
        <item>Yellow on blue</item>
        <item>Yellow on blue</item>
        <item>Custom</item>
        <item>Custom</item>
+276 −215
Original line number Original line Diff line number Diff line
@@ -18,292 +18,353 @@ package com.android.settings.accessibility;


import android.content.ContentResolver;
import android.content.ContentResolver;
import android.content.Context;
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.Canvas;
import android.graphics.Color;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Paint.Style;
import android.os.Parcel;
import android.graphics.RectF;
import android.support.v4.view.ViewCompat;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.Layout.Alignment;
import android.text.ParcelableSpan;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.UpdateAppearance;
import android.util.AttributeSet;
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.view.accessibility.CaptioningManager.CaptionStyle;
import android.widget.TextView;


public class CaptioningTextView extends TextView {
public class CaptioningTextView extends View {
    private MutableBackgroundColorSpan mBackgroundSpan;
    // Ratio of inner padding to font size.
    private ColorStateList mOutlineColorState;
    private static final float INNER_PADDING_RATIO = 0.125f;
    private float mOutlineWidth;
    private int mOutlineColor;


    private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
    // Default style dimensions in dips.
    private int mEdgeColor = Color.TRANSPARENT;
    private static final float CORNER_RADIUS = 2.0f;
    private float mEdgeWidth = 0;
    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) {
    /** Temporary rectangle used for computing line bounds. */
        super(context, attrs, defStyle);
    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) {
    public CaptioningTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this(context, attrs, 0);
    }
    }


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


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


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


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


        final float fontSize = CaptioningManager.getFontSize(cr);
            switch (attr) {
        if (fontSize != 0) {
                case android.R.styleable.TextView_text:
            setTextSize(fontSize);
                    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.
     * Applies an edge preset using a combination of {@link #setOutlineLayer}
        // TODO: Move these to a default style.
     * and {@link #setShadowLayer}. Any subsequent calls to either of these
        final DisplayMetrics m = getContext().getResources().getDisplayMetrics();
     * methods will invalidate the applied preset.
        mCornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CORNER_RADIUS, m);
     *
        mOutlineWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, OUTLINE_WIDTH, m);
     * @param type Type of edge to apply, one of:
        mShadowRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_RADIUS, m);
     *            <ul>
        mShadowOffsetX = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_OFFSET_Y, m);
     *            <li>{@link CaptionStyle#EDGE_TYPE_NONE}
        mShadowOffsetY = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_OFFSET_X, m);
     *            <li>{@link CaptionStyle#EDGE_TYPE_OUTLINE}

     *            <li>{@link CaptionStyle#EDGE_TYPE_DROP_SHADOW}
        final TextPaint paint = new TextPaint();
     *            </ul>
        paint.setAntiAlias(true);
     * @param color Edge color as a packed 32-bit ARGB color.
        paint.setSubpixelText(true);
     * @param width Width of the edge in pixels.

     */
        mPaint = paint;
    public void applyEdge(int type, int color, float width) {

        if (mEdgeType != type || mEdgeColor != color || mEdgeWidth != width) {
        setText(text);
            final int textColor = getTextColors().getDefaultColor();
        setTextSize(textSize);
            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;
    public void setText(int resId) {
            mEdgeColor = color;
        final CharSequence text = getContext().getText(resId);
            mEdgeWidth = width;
        setText(text);
    }
    }

    public void setText(CharSequence text) {
        mText.setLength(0);
        mText.append(text);

        mHasMeasurements = false;

        requestLayout();
    }
    }


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


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


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

        width *= 2.0f;
        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();
        invalidate();
    }


            // TODO: Remove after display list bug is fixed.
    public void setEdgeColor(int color) {
            if (width > 0 && Color.alpha(color) != 0) {
        mEdgeColor = color;
                setLayerType(ViewCompat.LAYER_TYPE_SOFTWARE, null);

            } else {
        invalidate();
                setLayerType(ViewCompat.LAYER_TYPE_HARDWARE, null);
    }
    }

    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) {
     * @return the color of the outline layer
        if (mPaint.getTypeface() != typeface) {
     * @see #setOutlineLayer(float, int)
            mHasMeasurements = false;
     */
            mPaint.setTypeface(typeface);
    public int getOutlineColor() {
        return mOutlineColor;
    }


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


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


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


    @Override
            // Account for padding.
    public void setBackgroundColor(int color) {
            final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
        if (Color.alpha(color) == 0) {
            final int width = layout.getWidth() + paddingX;
            if (mHasBackground) {
            final int height = layout.getHeight() + mPaddingTop + mPaddingBottom;
                mHasBackground = false;
            setMeasuredDimension(width, height);
                getEditableText().removeSpan(mBackgroundSpan);
            }
        } else {
            if (mBackgroundSpan == null) {
                mBackgroundSpan = new MutableBackgroundColorSpan(color);
        } else {
        } 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
        // Account for padding.
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX;
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        maxWidth -= paddingX;


        if (mBackgroundSpan != null) {
        if (maxWidth <= 0) {
            getEditableText().setSpan(mBackgroundSpan, 0, lengthAfter, 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
        final float[] textWidths = mTextWidths;
    protected void onDraw(Canvas c) {
        paint.getTextWidths(text, 0, textLength, textWidths);
        if (mOutlineWidth > 0 && Color.alpha(mOutlineColor) > 0) {

            final TextPaint textPaint = getPaint();
        // Compute total length.
            final Paint.Style previousStyle = textPaint.getStyle();
        float runLength = 0;
            final ColorStateList previousColors = getTextColors();
        for (int i = 0; i < textLength; i++) {
            textPaint.setStyle(Style.STROKE);
            runLength += textWidths[i];
            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;
        }
        }


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

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


            // Restore the background.
        int line = 0;
            if (mBackgroundSpan != null) {
        int lastBreak = 0;
                mBackgroundSpan.setColor(color);
        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
        mHasMeasurements = true;
            implements UpdateAppearance, ParcelableSpan {
        mLastMeasuredWidth = maxWidth;
        private int mColor;


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

        return true;
    }
    }


        public MutableBackgroundColorSpan(Parcel src) {
    public void setStyle(int styleId) {
            mColor = src.readInt();
        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) {
        mForegroundColor = style.foregroundColor;
            mColor = color;
        mBackgroundColor = style.backgroundColor;
        mEdgeType = style.edgeType;
        mEdgeColor = style.edgeColor;
        mHasMeasurements = false;

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

        requestLayout();
    }
    }


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


        @Override
        final int saveCount = c.save();
        public int describeContents() {
        final int innerPaddingX = mInnerPaddingX;
            return 0;
        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
        final int edgeType = mEdgeType;
        public void writeToParcel(Parcel dest, int flags) {
        if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
            dest.writeInt(mColor);
            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() {
        if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
            return mColor;
            paint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
        }
        }


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

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

        c.restoreToCount(saveCount);
    }
    }
}
}
+4 −2
Original line number Original line Diff line number Diff line
@@ -50,12 +50,14 @@ public class EdgeTypePreference extends ListDialogPreference {
    protected void onBindListItem(View view, int index) {
    protected void onBindListItem(View view, int index) {
        final float fontSize = CaptioningManager.getFontSize(getContext().getContentResolver());
        final float fontSize = CaptioningManager.getFontSize(getContext().getContentResolver());
        final CaptioningTextView preview = (CaptioningTextView) view.findViewById(R.id.preview);
        final CaptioningTextView preview = (CaptioningTextView) view.findViewById(R.id.preview);
        preview.setTextColor(Color.WHITE);

        preview.setForegroundColor(Color.WHITE);
        preview.setBackgroundColor(Color.TRANSPARENT);
        preview.setBackgroundColor(Color.TRANSPARENT);
        preview.setTextSize(fontSize);
        preview.setTextSize(fontSize);


        final int value = getValueAt(index);
        final int value = getValueAt(index);
        preview.applyEdge(value, Color.BLACK, 4.0f);
        preview.setEdgeType(value);
        preview.setEdgeColor(Color.BLACK);


        final CharSequence title = getTitleAt(index);
        final CharSequence title = getTitleAt(index);
        if (title != null) {
        if (title != null) {
+4 −1
Original line number Original line Diff line number Diff line
@@ -86,10 +86,13 @@ public class ToggleCaptioningPreferenceFragment extends Fragment {
    }
    }


    public static void applyCaptionProperties(CaptioningTextView previewText, int styleId) {
    public static void applyCaptionProperties(CaptioningTextView previewText, int styleId) {
        previewText.applyStyleAndFontSize(styleId);
        previewText.setStyle(styleId);


        final Context context = previewText.getContext();
        final Context context = previewText.getContext();
        final ContentResolver cr = context.getContentResolver();
        final ContentResolver cr = context.getContentResolver();
        final float fontSize = CaptioningManager.getFontSize(cr);
        previewText.setTextSize(fontSize);

        final Locale locale = CaptioningManager.getLocale(cr);
        final Locale locale = CaptioningManager.getLocale(cr);
        if (locale != null) {
        if (locale != null) {
            final CharSequence localizedText = AccessibilityUtils.getTextForLocale(
            final CharSequence localizedText = AccessibilityUtils.getTextForLocale(