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