SubtitleView.java revision e88aee8ad85b01229b12dbc0c3cc2f0b8b490192
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.widget;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Paint;
26import android.graphics.Paint.Join;
27import android.graphics.Paint.Style;
28import android.graphics.RectF;
29import android.graphics.Typeface;
30import android.text.Layout.Alignment;
31import android.text.StaticLayout;
32import android.text.TextPaint;
33import android.util.AttributeSet;
34import android.util.DisplayMetrics;
35import android.view.View;
36import android.view.accessibility.CaptioningManager.CaptionStyle;
37
38public class SubtitleView extends View {
39    // Ratio of inner padding to font size.
40    private static final float INNER_PADDING_RATIO = 0.125f;
41
42    /** Color used for the shadowed edge of a bevel. */
43    private static final int COLOR_BEVEL_DARK = 0x80000000;
44
45    /** Color used for the illuminated edge of a bevel. */
46    private static final int COLOR_BEVEL_LIGHT = 0x80FFFFFF;
47
48    // Styled dimensions.
49    private final float mCornerRadius;
50    private final float mOutlineWidth;
51    private final float mShadowRadius;
52    private final float mShadowOffsetX;
53    private final float mShadowOffsetY;
54
55    /** Temporary rectangle used for computing line bounds. */
56    private final RectF mLineBounds = new RectF();
57
58    /** Reusable string builder used for holding text. */
59    private final StringBuilder mText = new StringBuilder();
60
61    private Alignment mAlignment;
62    private TextPaint mTextPaint;
63    private Paint mPaint;
64
65    private int mForegroundColor;
66    private int mBackgroundColor;
67    private int mEdgeColor;
68    private int mEdgeType;
69
70    private boolean mHasMeasurements;
71    private int mLastMeasuredWidth;
72    private StaticLayout mLayout;
73
74    private float mSpacingMult = 1;
75    private float mSpacingAdd = 0;
76    private int mInnerPaddingX = 0;
77
78    public SubtitleView(Context context) {
79        this(context, null);
80    }
81
82    public SubtitleView(Context context, AttributeSet attrs) {
83        this(context, attrs, 0);
84    }
85
86    public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
87        this(context, attrs, defStyleAttr, 0);
88    }
89
90    public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
91        super(context, attrs);
92
93        final TypedArray a = context.obtainStyledAttributes(
94                    attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
95
96        CharSequence text = "";
97        int textSize = 15;
98
99        final int n = a.getIndexCount();
100        for (int i = 0; i < n; i++) {
101            int attr = a.getIndex(i);
102
103            switch (attr) {
104                case android.R.styleable.TextView_text:
105                    text = a.getText(attr);
106                    break;
107                case android.R.styleable.TextView_lineSpacingExtra:
108                    mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
109                    break;
110                case android.R.styleable.TextView_lineSpacingMultiplier:
111                    mSpacingMult = a.getFloat(attr, mSpacingMult);
112                    break;
113                case android.R.styleable.TextAppearance_textSize:
114                    textSize = a.getDimensionPixelSize(attr, textSize);
115                    break;
116            }
117        }
118
119        // Set up density-dependent properties.
120        // TODO: Move these to a default style.
121        final Resources res = getContext().getResources();
122        mCornerRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_corner_radius);
123        mOutlineWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_outline_width);
124        mShadowRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_radius);
125        mShadowOffsetX = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_offset);
126        mShadowOffsetY = mShadowOffsetX;
127
128        mTextPaint = new TextPaint();
129        mTextPaint.setAntiAlias(true);
130        mTextPaint.setSubpixelText(true);
131
132        mPaint = new Paint();
133        mPaint.setAntiAlias(true);
134
135        setText(text);
136        setTextSize(textSize);
137    }
138
139    public void setText(int resId) {
140        final CharSequence text = getContext().getText(resId);
141        setText(text);
142    }
143
144    public void setText(CharSequence text) {
145        mText.setLength(0);
146        mText.append(text);
147
148        mHasMeasurements = false;
149
150        requestLayout();
151    }
152
153    public void setForegroundColor(int color) {
154        mForegroundColor = color;
155
156        invalidate();
157    }
158
159    @Override
160    public void setBackgroundColor(int color) {
161        mBackgroundColor = color;
162
163        invalidate();
164    }
165
166    public void setEdgeType(int edgeType) {
167        mEdgeType = edgeType;
168
169        invalidate();
170    }
171
172    public void setEdgeColor(int color) {
173        mEdgeColor = color;
174
175        invalidate();
176    }
177
178    /**
179     * Sets the text size in pixels.
180     *
181     * @param size the text size in pixels
182     */
183    public void setTextSize(float size) {
184        if (mTextPaint.getTextSize() != size) {
185            mTextPaint.setTextSize(size);
186            mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
187
188            mHasMeasurements = false;
189
190            requestLayout();
191            invalidate();
192        }
193    }
194
195    public void setTypeface(Typeface typeface) {
196        if (mTextPaint.getTypeface() != typeface) {
197            mTextPaint.setTypeface(typeface);
198
199            mHasMeasurements = false;
200
201            requestLayout();
202            invalidate();
203        }
204    }
205
206    public void setAlignment(Alignment textAlignment) {
207        if (mAlignment != textAlignment) {
208            mAlignment = textAlignment;
209
210            mHasMeasurements = false;
211
212            requestLayout();
213            invalidate();
214        }
215    }
216
217    @Override
218    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
219        final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
220
221        if (computeMeasurements(widthSpec)) {
222            final StaticLayout layout = mLayout;
223
224            // Account for padding.
225            final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
226            final int width = layout.getWidth() + paddingX;
227            final int height = layout.getHeight() + mPaddingTop + mPaddingBottom;
228            setMeasuredDimension(width, height);
229        } else {
230            setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
231        }
232    }
233
234    @Override
235    public void onLayout(boolean changed, int l, int t, int r, int b) {
236        final int width = r - l;
237
238        computeMeasurements(width);
239    }
240
241    private boolean computeMeasurements(int maxWidth) {
242        if (mHasMeasurements && maxWidth == mLastMeasuredWidth) {
243            return true;
244        }
245
246        // Account for padding.
247        final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
248        maxWidth -= paddingX;
249        if (maxWidth <= 0) {
250            return false;
251        }
252
253        // TODO: Implement minimum-difference line wrapping. Adding the results
254        // of Paint.getTextWidths() seems to return different values than
255        // StaticLayout.getWidth(), so this is non-trivial.
256        mHasMeasurements = true;
257        mLastMeasuredWidth = maxWidth;
258        mLayout = new StaticLayout(
259                mText, mTextPaint, maxWidth, mAlignment, mSpacingMult, mSpacingAdd, true);
260
261        return true;
262    }
263
264    public void setStyle(int styleId) {
265        final Context context = mContext;
266        final ContentResolver cr = context.getContentResolver();
267        final CaptionStyle style;
268        if (styleId == CaptionStyle.PRESET_CUSTOM) {
269            style = CaptionStyle.getCustomStyle(cr);
270        } else {
271            style = CaptionStyle.PRESETS[styleId];
272        }
273
274        final CaptionStyle defStyle = CaptionStyle.DEFAULT;
275        mForegroundColor = style.hasForegroundColor() ?
276                style.foregroundColor : defStyle.foregroundColor;
277        mBackgroundColor = style.hasBackgroundColor() ?
278                style.backgroundColor : defStyle.backgroundColor;
279        mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType;
280        mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor;
281        mHasMeasurements = false;
282
283        final Typeface typeface = style.getTypeface();
284        setTypeface(typeface);
285
286        requestLayout();
287    }
288
289    @Override
290    protected void onDraw(Canvas c) {
291        final StaticLayout layout = mLayout;
292        if (layout == null) {
293            return;
294        }
295
296        final int saveCount = c.save();
297        final int innerPaddingX = mInnerPaddingX;
298        c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
299
300        final int lineCount = layout.getLineCount();
301        final Paint textPaint = mTextPaint;
302        final Paint paint = mPaint;
303        final RectF bounds = mLineBounds;
304
305        if (Color.alpha(mBackgroundColor) > 0) {
306            final float cornerRadius = mCornerRadius;
307            float previousBottom = layout.getLineTop(0);
308
309            paint.setColor(mBackgroundColor);
310            paint.setStyle(Style.FILL);
311
312            for (int i = 0; i < lineCount; i++) {
313                bounds.left = layout.getLineLeft(i) -innerPaddingX;
314                bounds.right = layout.getLineRight(i) + innerPaddingX;
315                bounds.top = previousBottom;
316                bounds.bottom = layout.getLineBottom(i);
317                previousBottom = bounds.bottom;
318
319                c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
320            }
321        }
322
323        final int edgeType = mEdgeType;
324        if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
325            textPaint.setStrokeJoin(Join.ROUND);
326            textPaint.setStrokeWidth(mOutlineWidth);
327            textPaint.setColor(mEdgeColor);
328            textPaint.setStyle(Style.FILL_AND_STROKE);
329
330            for (int i = 0; i < lineCount; i++) {
331                layout.drawText(c, i, i);
332            }
333        } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
334            textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
335        } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED
336                || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) {
337            final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED;
338            final int colorUp = raised ? Color.WHITE : mEdgeColor;
339            final int colorDown = raised ? mEdgeColor : Color.WHITE;
340            final float offset = mShadowRadius / 2f;
341
342            textPaint.setColor(mForegroundColor);
343            textPaint.setStyle(Style.FILL);
344            textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
345
346            for (int i = 0; i < lineCount; i++) {
347                layout.drawText(c, i, i);
348            }
349
350            textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
351        }
352
353        textPaint.setColor(mForegroundColor);
354        textPaint.setStyle(Style.FILL);
355
356        for (int i = 0; i < lineCount; i++) {
357            layout.drawText(c, i, i);
358        }
359
360        textPaint.setShadowLayer(0, 0, 0, 0);
361        c.restoreToCount(saveCount);
362    }
363}
364