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