Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
      5  * in compliance with the License. You may obtain a copy of the License at
      6  *
      7  * http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the License
     10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
     11  * or implied. See the License for the specific language governing permissions and limitations under
     12  * the License.
     13  */
     14 package androidx.leanback.widget;
     15 
     16 import android.content.Context;
     17 import android.content.res.TypedArray;
     18 import android.text.Layout;
     19 import android.util.AttributeSet;
     20 import android.util.TypedValue;
     21 import android.widget.TextView;
     22 
     23 import androidx.leanback.R;
     24 
     25 /**
     26  * <p>A {@link android.widget.TextView} that adjusts text size automatically in response
     27  * to certain trigger conditions, such as text that wraps over multiple lines.</p>
     28  */
     29 class ResizingTextView extends TextView {
     30 
     31     /**
     32      * Trigger text resize when text flows into the last line of a multi-line text view.
     33      */
     34     public static final int TRIGGER_MAX_LINES = 0x01;
     35 
     36     private int mTriggerConditions; // Union of trigger conditions
     37     private int mResizedTextSize;
     38     // Note: Maintaining line spacing turned out not to be useful, and will be removed in
     39     // the next round of design for this class (b/18736630). For now it simply defaults to false.
     40     private boolean mMaintainLineSpacing;
     41     private int mResizedPaddingAdjustmentTop;
     42     private int mResizedPaddingAdjustmentBottom;
     43 
     44     private boolean mIsResized = false;
     45     // Remember default properties in case we need to restore them
     46     private boolean mDefaultsInitialized = false;
     47     private int mDefaultTextSize;
     48     private float mDefaultLineSpacingExtra;
     49     private int mDefaultPaddingTop;
     50     private int mDefaultPaddingBottom;
     51 
     52     public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     53         super(ctx, attrs, defStyleAttr);
     54         TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.lbResizingTextView,
     55                 defStyleAttr, defStyleRes);
     56 
     57         try {
     58             mTriggerConditions = a.getInt(
     59                     R.styleable.lbResizingTextView_resizeTrigger, TRIGGER_MAX_LINES);
     60             mResizedTextSize = a.getDimensionPixelSize(
     61                     R.styleable.lbResizingTextView_resizedTextSize, -1);
     62             mMaintainLineSpacing = a.getBoolean(
     63                     R.styleable.lbResizingTextView_maintainLineSpacing, false);
     64             mResizedPaddingAdjustmentTop = a.getDimensionPixelOffset(
     65                     R.styleable.lbResizingTextView_resizedPaddingAdjustmentTop, 0);
     66             mResizedPaddingAdjustmentBottom = a.getDimensionPixelOffset(
     67                     R.styleable.lbResizingTextView_resizedPaddingAdjustmentBottom, 0);
     68         } finally {
     69             a.recycle();
     70         }
     71     }
     72 
     73     public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr) {
     74         this(ctx, attrs, defStyleAttr, 0);
     75     }
     76 
     77     public ResizingTextView(Context ctx, AttributeSet attrs) {
     78         // TODO We should define our own style that inherits from TextViewStyle, to set defaults
     79         // for new styleables,  We then pass the appropriate R.attr up the constructor chain here.
     80         this(ctx, attrs, android.R.attr.textViewStyle);
     81     }
     82 
     83     public ResizingTextView(Context ctx) {
     84         this(ctx, null);
     85     }
     86 
     87     /**
     88      * @return the trigger conditions used to determine whether resize occurs
     89      */
     90     public int getTriggerConditions() {
     91         return mTriggerConditions;
     92     }
     93 
     94     /**
     95      * Set the trigger conditions used to determine whether resize occurs. Pass
     96      * a union of trigger condition constants, such as {@link ResizingTextView#TRIGGER_MAX_LINES}.
     97      *
     98      * @param conditions A union of trigger condition constants
     99      */
    100     public void setTriggerConditions(int conditions) {
    101         if (mTriggerConditions != conditions) {
    102             mTriggerConditions = conditions;
    103             // Always request a layout when trigger conditions change
    104             requestLayout();
    105         }
    106     }
    107 
    108     /**
    109      * @return the resized text size
    110      */
    111     public int getResizedTextSize() {
    112         return mResizedTextSize;
    113     }
    114 
    115     /**
    116      * Set the text size for resized text.
    117      *
    118      * @param size The text size for resized text
    119      */
    120     public void setResizedTextSize(int size) {
    121         if (mResizedTextSize != size) {
    122             mResizedTextSize = size;
    123             resizeParamsChanged();
    124         }
    125     }
    126 
    127     /**
    128      * @return whether or not to maintain line spacing when resizing text.
    129      * The default is true.
    130      */
    131     public boolean getMaintainLineSpacing() {
    132         return mMaintainLineSpacing;
    133     }
    134 
    135     /**
    136      * Set whether or not to maintain line spacing when resizing text.
    137      * The default is true.
    138      *
    139      * @param maintain Whether or not to maintain line spacing
    140      */
    141     public void setMaintainLineSpacing(boolean maintain) {
    142         if (mMaintainLineSpacing != maintain) {
    143             mMaintainLineSpacing = maintain;
    144             resizeParamsChanged();
    145         }
    146     }
    147 
    148     /**
    149      * @return desired adjustment to top padding for resized text
    150      */
    151     public int getResizedPaddingAdjustmentTop() {
    152         return mResizedPaddingAdjustmentTop;
    153     }
    154 
    155     /**
    156      * Set the desired adjustment to top padding for resized text.
    157      *
    158      * @param adjustment The adjustment to top padding, in pixels
    159      */
    160     public void setResizedPaddingAdjustmentTop(int adjustment) {
    161         if (mResizedPaddingAdjustmentTop != adjustment) {
    162             mResizedPaddingAdjustmentTop = adjustment;
    163             resizeParamsChanged();
    164         }
    165     }
    166 
    167     /**
    168      * @return desired adjustment to bottom padding for resized text
    169      */
    170     public int getResizedPaddingAdjustmentBottom() {
    171         return mResizedPaddingAdjustmentBottom;
    172     }
    173 
    174     /**
    175      * Set the desired adjustment to bottom padding for resized text.
    176      *
    177      * @param adjustment The adjustment to bottom padding, in pixels
    178      */
    179     public void setResizedPaddingAdjustmentBottom(int adjustment) {
    180         if (mResizedPaddingAdjustmentBottom != adjustment) {
    181             mResizedPaddingAdjustmentBottom = adjustment;
    182             resizeParamsChanged();
    183         }
    184     }
    185 
    186     private void resizeParamsChanged() {
    187         // If we're not resized, then changing resize parameters doesn't
    188         // affect layout, so don't bother requesting.
    189         if (mIsResized) {
    190             requestLayout();
    191         }
    192     }
    193 
    194     @Override
    195     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    196         if (!mDefaultsInitialized) {
    197             mDefaultTextSize = (int) getTextSize();
    198             mDefaultLineSpacingExtra = getLineSpacingExtra();
    199             mDefaultPaddingTop = getPaddingTop();
    200             mDefaultPaddingBottom = getPaddingBottom();
    201             mDefaultsInitialized = true;
    202         }
    203 
    204         // Always try first to measure with defaults. Otherwise, we may think we can get away
    205         // with larger text sizes later when we actually can't.
    206         setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
    207         setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
    208         setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
    209 
    210         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    211 
    212         boolean resizeText = false;
    213 
    214         final Layout layout = getLayout();
    215         if (layout != null) {
    216             if ((mTriggerConditions & TRIGGER_MAX_LINES) > 0) {
    217                 final int lineCount = layout.getLineCount();
    218                 final int maxLines = getMaxLines();
    219                 if (maxLines > 1) {
    220                     resizeText = lineCount == maxLines;
    221                 }
    222             }
    223         }
    224 
    225         final int currentSizePx = (int) getTextSize();
    226         boolean remeasure = false;
    227         if (resizeText) {
    228             if (mResizedTextSize != -1 && currentSizePx != mResizedTextSize) {
    229                 setTextSize(TypedValue.COMPLEX_UNIT_PX, mResizedTextSize);
    230                 remeasure = true;
    231             }
    232             // Check for other desired adjustments in addition to the text size
    233             final float targetLineSpacingExtra = mDefaultLineSpacingExtra
    234                     + mDefaultTextSize - mResizedTextSize;
    235             if (mMaintainLineSpacing && getLineSpacingExtra() != targetLineSpacingExtra) {
    236                 setLineSpacing(targetLineSpacingExtra, getLineSpacingMultiplier());
    237                 remeasure = true;
    238             }
    239             final int paddingTop = mDefaultPaddingTop + mResizedPaddingAdjustmentTop;
    240             final int paddingBottom = mDefaultPaddingBottom + mResizedPaddingAdjustmentBottom;
    241             if (getPaddingTop() != paddingTop || getPaddingBottom() != paddingBottom) {
    242                 setPaddingTopAndBottom(paddingTop, paddingBottom);
    243                 remeasure = true;
    244             }
    245         } else {
    246             // Use default size, line spacing, and padding
    247             if (mResizedTextSize != -1 && currentSizePx != mDefaultTextSize) {
    248                 setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
    249                 remeasure = true;
    250             }
    251             if (mMaintainLineSpacing && getLineSpacingExtra() != mDefaultLineSpacingExtra) {
    252                 setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
    253                 remeasure = true;
    254             }
    255             if (getPaddingTop() != mDefaultPaddingTop
    256                     || getPaddingBottom() != mDefaultPaddingBottom) {
    257                 setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
    258                 remeasure = true;
    259             }
    260         }
    261         mIsResized = resizeText;
    262         if (remeasure) {
    263             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    264         }
    265     }
    266 
    267     private void setPaddingTopAndBottom(int paddingTop, int paddingBottom) {
    268         if (isPaddingRelative()) {
    269             setPaddingRelative(getPaddingStart(), paddingTop, getPaddingEnd(), paddingBottom);
    270         } else {
    271             setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), paddingBottom);
    272         }
    273     }
    274 }
    275