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