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