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