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 
17 package com.android.internal.widget;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Join;
27 import android.graphics.Paint.Style;
28 import android.graphics.RectF;
29 import android.graphics.Typeface;
30 import android.text.Layout.Alignment;
31 import android.text.SpannableStringBuilder;
32 import android.text.StaticLayout;
33 import android.text.TextPaint;
34 import android.util.AttributeSet;
35 import android.view.View;
36 import android.view.accessibility.CaptioningManager.CaptionStyle;
37 
38 public 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 spannable string builder used for holding text. */
59     private final SpannableStringBuilder mText = new SpannableStringBuilder();
60 
61     private Alignment mAlignment = Alignment.ALIGN_CENTER;
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 
SubtitleView(Context context)78     public SubtitleView(Context context) {
79         this(context, null);
80     }
81 
SubtitleView(Context context, AttributeSet attrs)82     public SubtitleView(Context context, AttributeSet attrs) {
83         this(context, attrs, 0);
84     }
85 
SubtitleView(Context context, AttributeSet attrs, int defStyleAttr)86     public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
87         this(context, attrs, defStyleAttr, 0);
88     }
89 
SubtitleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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         a.recycle();
119 
120         // Set up density-dependent properties.
121         // TODO: Move these to a default style.
122         final Resources res = getContext().getResources();
123         mCornerRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_corner_radius);
124         mOutlineWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_outline_width);
125         mShadowRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_radius);
126         mShadowOffsetX = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_offset);
127         mShadowOffsetY = mShadowOffsetX;
128 
129         mTextPaint = new TextPaint();
130         mTextPaint.setAntiAlias(true);
131         mTextPaint.setSubpixelText(true);
132 
133         mPaint = new Paint();
134         mPaint.setAntiAlias(true);
135 
136         setText(text);
137         setTextSize(textSize);
138     }
139 
setText(int resId)140     public void setText(int resId) {
141         final CharSequence text = getContext().getText(resId);
142         setText(text);
143     }
144 
setText(CharSequence text)145     public void setText(CharSequence text) {
146         mText.clear();
147         mText.append(text);
148 
149         mHasMeasurements = false;
150 
151         requestLayout();
152         invalidate();
153     }
154 
setForegroundColor(int color)155     public void setForegroundColor(int color) {
156         mForegroundColor = color;
157 
158         invalidate();
159     }
160 
161     @Override
setBackgroundColor(int color)162     public void setBackgroundColor(int color) {
163         mBackgroundColor = color;
164 
165         invalidate();
166     }
167 
setEdgeType(int edgeType)168     public void setEdgeType(int edgeType) {
169         mEdgeType = edgeType;
170 
171         invalidate();
172     }
173 
setEdgeColor(int color)174     public void setEdgeColor(int color) {
175         mEdgeColor = color;
176 
177         invalidate();
178     }
179 
180     /**
181      * Sets the text size in pixels.
182      *
183      * @param size the text size in pixels
184      */
setTextSize(float size)185     public void setTextSize(float size) {
186         if (mTextPaint.getTextSize() != size) {
187             mTextPaint.setTextSize(size);
188             mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
189 
190             mHasMeasurements = false;
191 
192             requestLayout();
193             invalidate();
194         }
195     }
196 
setTypeface(Typeface typeface)197     public void setTypeface(Typeface typeface) {
198         if (mTextPaint.getTypeface() != typeface) {
199             mTextPaint.setTypeface(typeface);
200 
201             mHasMeasurements = false;
202 
203             requestLayout();
204             invalidate();
205         }
206     }
207 
setAlignment(Alignment textAlignment)208     public void setAlignment(Alignment textAlignment) {
209         if (mAlignment != textAlignment) {
210             mAlignment = textAlignment;
211 
212             mHasMeasurements = false;
213 
214             requestLayout();
215             invalidate();
216         }
217     }
218 
219     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)220     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
221         final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
222 
223         if (computeMeasurements(widthSpec)) {
224             final StaticLayout layout = mLayout;
225 
226             // Account for padding.
227             final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
228             final int width = layout.getWidth() + paddingX;
229             final int height = layout.getHeight() + mPaddingTop + mPaddingBottom;
230             setMeasuredDimension(width, height);
231         } else {
232             setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
233         }
234     }
235 
236     @Override
onLayout(boolean changed, int l, int t, int r, int b)237     public void onLayout(boolean changed, int l, int t, int r, int b) {
238         final int width = r - l;
239 
240         computeMeasurements(width);
241     }
242 
computeMeasurements(int maxWidth)243     private boolean computeMeasurements(int maxWidth) {
244         if (mHasMeasurements && maxWidth == mLastMeasuredWidth) {
245             return true;
246         }
247 
248         // Account for padding.
249         final int paddingX = mPaddingLeft + mPaddingRight + mInnerPaddingX * 2;
250         maxWidth -= paddingX;
251         if (maxWidth <= 0) {
252             return false;
253         }
254 
255         // TODO: Implement minimum-difference line wrapping. Adding the results
256         // of Paint.getTextWidths() seems to return different values than
257         // StaticLayout.getWidth(), so this is non-trivial.
258         mHasMeasurements = true;
259         mLastMeasuredWidth = maxWidth;
260         mLayout = StaticLayout.Builder.obtain(mText, 0, mText.length(), mTextPaint, maxWidth)
261                 .setAlignment(mAlignment)
262                 .setLineSpacing(mSpacingAdd, mSpacingMult)
263                 .setUseLineSpacingFromFallbacks(true)
264                 .build();
265 
266         return true;
267     }
268 
setStyle(int styleId)269     public void setStyle(int styleId) {
270         final Context context = mContext;
271         final ContentResolver cr = context.getContentResolver();
272         final CaptionStyle style;
273         if (styleId == CaptionStyle.PRESET_CUSTOM) {
274             style = CaptionStyle.getCustomStyle(cr);
275         } else {
276             style = CaptionStyle.PRESETS[styleId];
277         }
278 
279         final CaptionStyle defStyle = CaptionStyle.DEFAULT;
280         mForegroundColor = style.hasForegroundColor() ?
281                 style.foregroundColor : defStyle.foregroundColor;
282         mBackgroundColor = style.hasBackgroundColor() ?
283                 style.backgroundColor : defStyle.backgroundColor;
284         mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType;
285         mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor;
286         mHasMeasurements = false;
287 
288         final Typeface typeface = style.getTypeface();
289         setTypeface(typeface);
290 
291         requestLayout();
292     }
293 
294     @Override
onDraw(Canvas c)295     protected void onDraw(Canvas c) {
296         final StaticLayout layout = mLayout;
297         if (layout == null) {
298             return;
299         }
300 
301         final int saveCount = c.save();
302         final int innerPaddingX = mInnerPaddingX;
303         c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
304 
305         final int lineCount = layout.getLineCount();
306         final Paint textPaint = mTextPaint;
307         final Paint paint = mPaint;
308         final RectF bounds = mLineBounds;
309 
310         if (Color.alpha(mBackgroundColor) > 0) {
311             final float cornerRadius = mCornerRadius;
312             float previousBottom = layout.getLineTop(0);
313 
314             paint.setColor(mBackgroundColor);
315             paint.setStyle(Style.FILL);
316 
317             for (int i = 0; i < lineCount; i++) {
318                 bounds.left = layout.getLineLeft(i) -innerPaddingX;
319                 bounds.right = layout.getLineRight(i) + innerPaddingX;
320                 bounds.top = previousBottom;
321                 bounds.bottom = layout.getLineBottom(i);
322                 previousBottom = bounds.bottom;
323 
324                 c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
325             }
326         }
327 
328         final int edgeType = mEdgeType;
329         if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
330             textPaint.setStrokeJoin(Join.ROUND);
331             textPaint.setStrokeWidth(mOutlineWidth);
332             textPaint.setColor(mEdgeColor);
333             textPaint.setStyle(Style.FILL_AND_STROKE);
334 
335             for (int i = 0; i < lineCount; i++) {
336                 layout.drawText(c, i, i);
337             }
338         } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
339             textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
340         } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED
341                 || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) {
342             final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED;
343             final int colorUp = raised ? Color.WHITE : mEdgeColor;
344             final int colorDown = raised ? mEdgeColor : Color.WHITE;
345             final float offset = mShadowRadius / 2f;
346 
347             textPaint.setColor(mForegroundColor);
348             textPaint.setStyle(Style.FILL);
349             textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
350 
351             for (int i = 0; i < lineCount; i++) {
352                 layout.drawText(c, i, i);
353             }
354 
355             textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
356         }
357 
358         textPaint.setColor(mForegroundColor);
359         textPaint.setStyle(Style.FILL);
360 
361         for (int i = 0; i < lineCount; i++) {
362             layout.drawText(c, i, i);
363         }
364 
365         textPaint.setShadowLayer(0, 0, 0, 0);
366         c.restoreToCount(saveCount);
367     }
368 }
369