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;
     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;
     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 
     78     public SubtitleView(Context context) {
     79         this(context, null);
     80     }
     81 
     82     public SubtitleView(Context context, AttributeSet attrs) {
     83         this(context, attrs, 0);
     84     }
     85 
     86     public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
     87         this(context, attrs, defStyleAttr, 0);
     88     }
     89 
     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 
    139     public void setText(int resId) {
    140         final CharSequence text = getContext().getText(resId);
    141         setText(text);
    142     }
    143 
    144     public void setText(CharSequence text) {
    145         mText.clear();
    146         mText.append(text);
    147 
    148         mHasMeasurements = false;
    149 
    150         requestLayout();
    151         invalidate();
    152     }
    153 
    154     public void setForegroundColor(int color) {
    155         mForegroundColor = color;
    156 
    157         invalidate();
    158     }
    159 
    160     @Override
    161     public void setBackgroundColor(int color) {
    162         mBackgroundColor = color;
    163 
    164         invalidate();
    165     }
    166 
    167     public void setEdgeType(int edgeType) {
    168         mEdgeType = edgeType;
    169 
    170         invalidate();
    171     }
    172 
    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      */
    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 
    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 
    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
    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
    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 
    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 = new StaticLayout(
    260                 mText, mTextPaint, maxWidth, mAlignment, mSpacingMult, mSpacingAdd, true);
    261 
    262         return true;
    263     }
    264 
    265     public void setStyle(int styleId) {
    266         final Context context = mContext;
    267         final ContentResolver cr = context.getContentResolver();
    268         final CaptionStyle style;
    269         if (styleId == CaptionStyle.PRESET_CUSTOM) {
    270             style = CaptionStyle.getCustomStyle(cr);
    271         } else {
    272             style = CaptionStyle.PRESETS[styleId];
    273         }
    274 
    275         final CaptionStyle defStyle = CaptionStyle.DEFAULT;
    276         mForegroundColor = style.hasForegroundColor() ?
    277                 style.foregroundColor : defStyle.foregroundColor;
    278         mBackgroundColor = style.hasBackgroundColor() ?
    279                 style.backgroundColor : defStyle.backgroundColor;
    280         mEdgeType = style.hasEdgeType() ? style.edgeType : defStyle.edgeType;
    281         mEdgeColor = style.hasEdgeColor() ? style.edgeColor : defStyle.edgeColor;
    282         mHasMeasurements = false;
    283 
    284         final Typeface typeface = style.getTypeface();
    285         setTypeface(typeface);
    286 
    287         requestLayout();
    288     }
    289 
    290     @Override
    291     protected void onDraw(Canvas c) {
    292         final StaticLayout layout = mLayout;
    293         if (layout == null) {
    294             return;
    295         }
    296 
    297         final int saveCount = c.save();
    298         final int innerPaddingX = mInnerPaddingX;
    299         c.translate(mPaddingLeft + innerPaddingX, mPaddingTop);
    300 
    301         final int lineCount = layout.getLineCount();
    302         final Paint textPaint = mTextPaint;
    303         final Paint paint = mPaint;
    304         final RectF bounds = mLineBounds;
    305 
    306         if (Color.alpha(mBackgroundColor) > 0) {
    307             final float cornerRadius = mCornerRadius;
    308             float previousBottom = layout.getLineTop(0);
    309 
    310             paint.setColor(mBackgroundColor);
    311             paint.setStyle(Style.FILL);
    312 
    313             for (int i = 0; i < lineCount; i++) {
    314                 bounds.left = layout.getLineLeft(i) -innerPaddingX;
    315                 bounds.right = layout.getLineRight(i) + innerPaddingX;
    316                 bounds.top = previousBottom;
    317                 bounds.bottom = layout.getLineBottom(i);
    318                 previousBottom = bounds.bottom;
    319 
    320                 c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
    321             }
    322         }
    323 
    324         final int edgeType = mEdgeType;
    325         if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
    326             textPaint.setStrokeJoin(Join.ROUND);
    327             textPaint.setStrokeWidth(mOutlineWidth);
    328             textPaint.setColor(mEdgeColor);
    329             textPaint.setStyle(Style.FILL_AND_STROKE);
    330 
    331             for (int i = 0; i < lineCount; i++) {
    332                 layout.drawText(c, i, i);
    333             }
    334         } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
    335             textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor);
    336         } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED
    337                 || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) {
    338             final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED;
    339             final int colorUp = raised ? Color.WHITE : mEdgeColor;
    340             final int colorDown = raised ? mEdgeColor : Color.WHITE;
    341             final float offset = mShadowRadius / 2f;
    342 
    343             textPaint.setColor(mForegroundColor);
    344             textPaint.setStyle(Style.FILL);
    345             textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
    346 
    347             for (int i = 0; i < lineCount; i++) {
    348                 layout.drawText(c, i, i);
    349             }
    350 
    351             textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown);
    352         }
    353 
    354         textPaint.setColor(mForegroundColor);
    355         textPaint.setStyle(Style.FILL);
    356 
    357         for (int i = 0; i < lineCount; i++) {
    358             layout.drawText(c, i, i);
    359         }
    360 
    361         textPaint.setShadowLayer(0, 0, 0, 0);
    362         c.restoreToCount(saveCount);
    363     }
    364 }
    365