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