Home | History | Annotate | Download | only in widget
      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