• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
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 
setText(int resId)139     public void setText(int resId) {
140         final CharSequence text = getContext().getText(resId);
141         setText(text);
142     }
143 
setText(CharSequence text)144     public void setText(CharSequence text) {
145         mText.clear();
146         mText.append(text);
147 
148         mHasMeasurements = false;
149 
150         requestLayout();
151         invalidate();
152     }
153 
setForegroundColor(int color)154     public void setForegroundColor(int color) {
155         mForegroundColor = color;
156 
157         invalidate();
158     }
159 
160     @Override
setBackgroundColor(int color)161     public void setBackgroundColor(int color) {
162         mBackgroundColor = color;
163 
164         invalidate();
165     }
166 
setEdgeType(int edgeType)167     public void setEdgeType(int edgeType) {
168         mEdgeType = edgeType;
169 
170         invalidate();
171     }
172 
setEdgeColor(int color)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      */
setTextSize(float size)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 
setTypeface(Typeface typeface)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 
setAlignment(Alignment textAlignment)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int l, int t, int r, int b)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 
computeMeasurements(int maxWidth)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 = StaticLayout.Builder.obtain(mText, 0, mText.length(), mTextPaint, maxWidth)
260                 .setAlignment(mAlignment)
261                 .setLineSpacing(mSpacingAdd, mSpacingMult)
262                 .setUseLineSpacingFromFallbacks(true)
263                 .build();
264 
265         return true;
266     }
267 
setStyle(int styleId)268     public void setStyle(int styleId) {
269         final Context context = mContext;
270         final ContentResolver cr = context.getContentResolver();
271         final CaptionStyle style;
272         if (styleId == CaptionStyle.PRESET_CUSTOM) {
273             style = CaptionStyle.getCustomStyle(cr);
274         } else {
275             style = CaptionStyle.PRESETS[styleId];
276         }
277 
278         final CaptionStyle defStyle = CaptionStyle.DEFAULT;
279         mForegroundColor = style.hasForegroundColor() ?
280                 style.foregroundColor : defStyle.foregroundColor;
281         mBackgroundColor = style.hasBackgroundColor() ?
282                 style.backgroundColor : defStyle.backgroundColor;
283         mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType;
284         mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor;
285         mHasMeasurements = false;
286 
287         final Typeface typeface = style.getTypeface();
288         setTypeface(typeface);
289 
290         requestLayout();
291     }
292 
293     @Override
onDraw(Canvas c)294     protected void onDraw(Canvas c) {
295         final StaticLayout layout = mLayout;
296         if (layout == null) {
297             return;
298         }
299 
300         final int saveCount = c.save();
301         final int innerPaddingX = mInnerPaddingX;
302         c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
303 
304         final int lineCount = layout.getLineCount();
305         final Paint textPaint = mTextPaint;
306         final Paint paint = mPaint;
307         final RectF bounds = mLineBounds;
308 
309         if (Color.alpha(mBackgroundColor) > 0) {
310             final float cornerRadius = mCornerRadius;
311             float previousBottom = layout.getLineTop(0);
312 
313             paint.setColor(mBackgroundColor);
314             paint.setStyle(Style.FILL);
315 
316             for (int i = 0; i < lineCount; i++) {
317                 bounds.left = layout.getLineLeft(i) -innerPaddingX;
318                 bounds.right = layout.getLineRight(i) + innerPaddingX;
319                 bounds.top = previousBottom;
320                 bounds.bottom = layout.getLineBottom(i);
321                 previousBottom = bounds.bottom;
322 
323                 c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
324             }
325         }
326 
327         final int edgeType = mEdgeType;
328         if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
329             textPaint.setStrokeJoin(Join.ROUND);
330             textPaint.setStrokeWidth(mOutlineWidth);
331             textPaint.setColor(mEdgeColor);
332             textPaint.setStyle(Style.FILL_AND_STROKE);
333 
334             for (int i = 0; i < lineCount; i++) {
335                 layout.drawText(c, i, i);
336             }
337         } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
338             textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
339         } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED
340                 || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) {
341             final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED;
342             final int colorUp = raised ? Color.WHITE : mEdgeColor;
343             final int colorDown = raised ? mEdgeColor : Color.WHITE;
344             final float offset = mShadowRadius / 2f;
345 
346             textPaint.setColor(mForegroundColor);
347             textPaint.setStyle(Style.FILL);
348             textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
349 
350             for (int i = 0; i < lineCount; i++) {
351                 layout.drawText(c, i, i);
352             }
353 
354             textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
355         }
356 
357         textPaint.setColor(mForegroundColor);
358         textPaint.setStyle(Style.FILL);
359 
360         for (int i = 0; i < lineCount; i++) {
361             layout.drawText(c, i, i);
362         }
363 
364         textPaint.setShadowLayer(0, 0, 0, 0);
365         c.restoreToCount(saveCount);
366     }
367 }
368