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