SubtitleView.java revision 3f7360df823b45b8b8c9a1f00dd1ec221664f798
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        invalidate();
152    }
153
154    public void setForegroundColor(int color) {
155        mForegroundColor = color;
156
157        invalidate();
158    }
159
160    @Override
161    public void setBackgroundColor(int color) {
162        mBackgroundColor = color;
163
164        invalidate();
165    }
166
167    public void setEdgeType(int edgeType) {
168        mEdgeType = edgeType;
169
170        invalidate();
171    }
172
173    public void setEdgeColor(int color) {
174        mEdgeColor = color;
175
176        invalidate();
177    }
178
179    /**
180     * Sets the text size in pixels.
181     *
182     * @param size the text size in pixels
183     */
184    public void setTextSize(float size) {
185        if (mTextPaint.getTextSize() != size) {
186            mTextPaint.setTextSize(size);
187            mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
188
189            mHasMeasurements = false;
190
191            requestLayout();
192            invalidate();
193        }
194    }
195
196    public void setTypeface(Typeface typeface) {
197        if (mTextPaint.getTypeface() != typeface) {
198            mTextPaint.setTypeface(typeface);
199
200            mHasMeasurements = false;
201
202            requestLayout();
203            invalidate();
204        }
205    }
206
207    public void setAlignment(Alignment textAlignment) {
208        if (mAlignment != textAlignment) {
209            mAlignment = textAlignment;
210
211            mHasMeasurements = false;
212
213            requestLayout();
214            invalidate();
215        }
216    }
217
218    @Override
219    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
220        final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
221
222        if (computeMeasurements(widthSpec)) {
223            final StaticLayout layout = mLayout;
224
225            // Account for padding.
226            final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
227            final int width = layout.getWidth() + paddingX;
228            final int height = layout.getHeight() + mPaddingTop + mPaddingBottom;
229            setMeasuredDimension(width, height);
230        } else {
231            setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
232        }
233    }
234
235    @Override
236    public void onLayout(boolean changed, int l, int t, int r, int b) {
237        final int width = r - l;
238
239        computeMeasurements(width);
240    }
241
242    private boolean computeMeasurements(int maxWidth) {
243        if (mHasMeasurements && maxWidth == mLastMeasuredWidth) {
244            return true;
245        }
246
247        // Account for padding.
248        final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
249        maxWidth -= paddingX;
250        if (maxWidth <= 0) {
251            return false;
252        }
253
254        // TODO: Implement minimum-difference line wrapping. Adding the results
255        // of Paint.getTextWidths() seems to return different values than
256        // StaticLayout.getWidth(), so this is non-trivial.
257        mHasMeasurements = true;
258        mLastMeasuredWidth = maxWidth;
259        mLayout = new StaticLayout(
260                mText, mTextPaint, maxWidth, mAlignment, mSpacingMult, mSpacingAdd, true);
261
262        return true;
263    }
264
265    public void setStyle(int styleId) {
266        final Context context = mContext;
267        final ContentResolver cr = context.getContentResolver();
268        final CaptionStyle style;
269        if (styleId == CaptionStyle.PRESET_CUSTOM) {
270            style = CaptionStyle.getCustomStyle(cr);
271        } else {
272            style = CaptionStyle.PRESETS[styleId];
273        }
274
275        final CaptionStyle defStyle = CaptionStyle.DEFAULT;
276        mForegroundColor = style.hasForegroundColor() ?
277                style.foregroundColor : defStyle.foregroundColor;
278        mBackgroundColor = style.hasBackgroundColor() ?
279                style.backgroundColor : defStyle.backgroundColor;
280        mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType;
281        mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor;
282        mHasMeasurements = false;
283
284        final Typeface typeface = style.getTypeface();
285        setTypeface(typeface);
286
287        requestLayout();
288    }
289
290    @Override
291    protected void onDraw(Canvas c) {
292        final StaticLayout layout = mLayout;
293        if (layout == null) {
294            return;
295        }
296
297        final int saveCount = c.save();
298        final int innerPaddingX = mInnerPaddingX;
299        c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
300
301        final int lineCount = layout.getLineCount();
302        final Paint textPaint = mTextPaint;
303        final Paint paint = mPaint;
304        final RectF bounds = mLineBounds;
305
306        if (Color.alpha(mBackgroundColor) > 0) {
307            final float cornerRadius = mCornerRadius;
308            float previousBottom = layout.getLineTop(0);
309
310            paint.setColor(mBackgroundColor);
311            paint.setStyle(Style.FILL);
312
313            for (int i = 0; i < lineCount; i++) {
314                bounds.left = layout.getLineLeft(i) -innerPaddingX;
315                bounds.right = layout.getLineRight(i) + innerPaddingX;
316                bounds.top = previousBottom;
317                bounds.bottom = layout.getLineBottom(i);
318                previousBottom = bounds.bottom;
319
320                c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
321            }
322        }
323
324        final int edgeType = mEdgeType;
325        if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
326            textPaint.setStrokeJoin(Join.ROUND);
327            textPaint.setStrokeWidth(mOutlineWidth);
328            textPaint.setColor(mEdgeColor);
329            textPaint.setStyle(Style.FILL_AND_STROKE);
330
331            for (int i = 0; i < lineCount; i++) {
332                layout.drawText(c, i, i);
333            }
334        } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
335            textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
336        } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED
337                || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) {
338            final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED;
339            final int colorUp = raised ? Color.WHITE : mEdgeColor;
340            final int colorDown = raised ? mEdgeColor : Color.WHITE;
341            final float offset = mShadowRadius / 2f;
342
343            textPaint.setColor(mForegroundColor);
344            textPaint.setStyle(Style.FILL);
345            textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
346
347            for (int i = 0; i < lineCount; i++) {
348                layout.drawText(c, i, i);
349            }
350
351            textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
352        }
353
354        textPaint.setColor(mForegroundColor);
355        textPaint.setStyle(Style.FILL);
356
357        for (int i = 0; i < lineCount; i++) {
358            layout.drawText(c, i, i);
359        }
360
361        textPaint.setShadowLayer(0, 0, 0, 0);
362        c.restoreToCount(saveCount);
363    }
364}
365