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