summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/autoresizetext/AutoResizeTextView.java')
-rw-r--r--java/com/android/incallui/autoresizetext/AutoResizeTextView.java316
1 files changed, 316 insertions, 0 deletions
diff --git a/java/com/android/incallui/autoresizetext/AutoResizeTextView.java b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
new file mode 100644
index 000000000..eedcbe5bb
--- /dev/null
+++ b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.autoresizetext;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.RectF;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.text.Layout.Alignment;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.SparseIntArray;
+import android.util.TypedValue;
+import android.widget.TextView;
+import javax.annotation.Nullable;
+
+/**
+ * A TextView that automatically scales its text to completely fill its allotted width.
+ *
+ * <p>Note: In some edge cases, the binary search algorithm to find the best fit may slightly
+ * overshoot / undershoot its constraints. See b/26704434. No minimal repro case has been
+ * found yet. A known workaround is the solution provided on StackOverflow:
+ * http://stackoverflow.com/a/5535672
+ */
+public class AutoResizeTextView extends TextView {
+ private static final int NO_LINE_LIMIT = -1;
+ private static final float DEFAULT_MIN_TEXT_SIZE = 16.0f;
+ private static final int DEFAULT_RESIZE_STEP_UNIT = TypedValue.COMPLEX_UNIT_PX;
+
+ private final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ private final RectF availableSpaceRect = new RectF();
+ private final SparseIntArray textSizesCache = new SparseIntArray();
+ private final TextPaint textPaint = new TextPaint();
+ private int resizeStepUnit = DEFAULT_RESIZE_STEP_UNIT;
+ private float minTextSize = DEFAULT_MIN_TEXT_SIZE;
+ private float maxTextSize;
+ private int maxWidth;
+ private int maxLines;
+ private float lineSpacingMultiplier = 1.0f;
+ private float lineSpacingExtra = 0.0f;
+
+ public AutoResizeTextView(Context context) {
+ super(context, null, 0);
+ initialize(context, null, 0, 0);
+ }
+
+ public AutoResizeTextView(Context context, AttributeSet attrs) {
+ super(context, attrs, 0);
+ initialize(context, attrs, 0, 0);
+ }
+
+ public AutoResizeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context, attrs, defStyleAttr, 0);
+ }
+
+ public AutoResizeTextView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private void initialize(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray typedArray = context.getTheme().obtainStyledAttributes(
+ attrs, R.styleable.AutoResizeTextView, defStyleAttr, defStyleRes);
+ readAttrs(typedArray);
+ textPaint.set(getPaint());
+ }
+
+ /** Overridden because getMaxLines is only defined in JB+. */
+ @Override
+ public final int getMaxLines() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getMaxLines();
+ } else {
+ return maxLines;
+ }
+ }
+
+ /** Overridden because getMaxLines is only defined in JB+. */
+ @Override
+ public final void setMaxLines(int maxLines) {
+ super.setMaxLines(maxLines);
+ this.maxLines = maxLines;
+ }
+
+ /** Overridden because getLineSpacingMultiplier is only defined in JB+. */
+ @Override
+ public final float getLineSpacingMultiplier() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getLineSpacingMultiplier();
+ } else {
+ return lineSpacingMultiplier;
+ }
+ }
+
+ /** Overridden because getLineSpacingExtra is only defined in JB+. */
+ @Override
+ public final float getLineSpacingExtra() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getLineSpacingExtra();
+ } else {
+ return lineSpacingExtra;
+ }
+ }
+
+ /**
+ * Overridden because getLineSpacingMultiplier and getLineSpacingExtra are only defined in JB+.
+ */
+ @Override
+ public final void setLineSpacing(float add, float mult) {
+ super.setLineSpacing(add, mult);
+ lineSpacingMultiplier = mult;
+ lineSpacingExtra = add;
+ }
+
+ /**
+ * Although this overrides the setTextSize method from the TextView base class, it changes the
+ * semantics a bit: Calling setTextSize now specifies the maximum text size to be used by this
+ * view. If the text can't fit with that text size, the text size will be scaled down, up to the
+ * minimum text size specified in {@link #setMinTextSize}.
+ *
+ * <p>Note that the final size unit will be truncated to the nearest integer value of the
+ * specified unit.
+ */
+ @Override
+ public final void setTextSize(int unit, float size) {
+ float maxTextSize = TypedValue.applyDimension(unit, size, displayMetrics);
+ if (this.maxTextSize != maxTextSize) {
+ this.maxTextSize = maxTextSize;
+ // TODO: It's not actually necessary to clear the whole cache here. To optimize cache
+ // deletion we'd have to delete all entries in the cache with a value equal or larger than
+ // MIN(old_max_size, new_max_size) when changing maxTextSize; and all entries with a value
+ // equal or smaller than MAX(old_min_size, new_min_size) when changing minTextSize.
+ textSizesCache.clear();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets the lower text size limit and invalidate the view.
+ *
+ * <p>The parameters follow the same behavior as they do in {@link #setTextSize}.
+ *
+ * <p>Note that the final size unit will be truncated to the nearest integer value of the
+ * specified unit.
+ */
+ public final void setMinTextSize(int unit, float size) {
+ float minTextSize = TypedValue.applyDimension(unit, size, displayMetrics);
+ if (this.minTextSize != minTextSize) {
+ this.minTextSize = minTextSize;
+ textSizesCache.clear();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets the unit to use as step units when computing the resized font size. This view's text
+ * contents will always be rendered as a whole integer value in the unit specified here. For
+ * example, if the unit is {@link TypedValue#COMPLEX_UNIT_SP}, then the text size may end up
+ * being 13sp or 14sp, but never 13.5sp.
+ *
+ * <p>By default, the AutoResizeTextView uses the unit {@link TypedValue#COMPLEX_UNIT_PX}.
+ *
+ * @param unit the unit type to use; must be a known unit type from {@link TypedValue}.
+ */
+ public final void setResizeStepUnit(int unit) {
+ if (resizeStepUnit != unit) {
+ resizeStepUnit = unit;
+ requestLayout();
+ }
+ }
+
+ private void readAttrs(TypedArray typedArray) {
+ resizeStepUnit = typedArray.getInt(
+ R.styleable.AutoResizeTextView_autoResizeText_resizeStepUnit, DEFAULT_RESIZE_STEP_UNIT);
+ minTextSize = (int) typedArray.getDimension(
+ R.styleable.AutoResizeTextView_autoResizeText_minTextSize, DEFAULT_MIN_TEXT_SIZE);
+ maxTextSize = (int) getTextSize();
+ }
+
+ private void adjustTextSize() {
+ int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ int maxHeight = getMeasuredHeight() - getPaddingBottom() - getPaddingTop();
+
+ if (maxWidth <= 0 || maxHeight <= 0) {
+ return;
+ }
+
+ this.maxWidth = maxWidth;
+ availableSpaceRect.right = maxWidth;
+ availableSpaceRect.bottom = maxHeight;
+ int minSizeInStepSizeUnits = (int) Math.ceil(convertToResizeStepUnits(minTextSize));
+ int maxSizeInStepSizeUnits = (int) Math.floor(convertToResizeStepUnits(maxTextSize));
+ float textSize = computeTextSize(
+ minSizeInStepSizeUnits, maxSizeInStepSizeUnits, availableSpaceRect);
+ super.setTextSize(resizeStepUnit, textSize);
+ }
+
+ private boolean suggestedSizeFitsInSpace(float suggestedSizeInPx, RectF availableSpace) {
+ textPaint.setTextSize(suggestedSizeInPx);
+ String text = getText().toString();
+ int maxLines = getMaxLines();
+ if (maxLines == 1) {
+ // If single line, check the line's height and width.
+ return textPaint.getFontSpacing() <= availableSpace.bottom
+ && textPaint.measureText(text) <= availableSpace.right;
+ } else {
+ // If multiline, lay the text out, then check the number of lines, the layout's height,
+ // and each line's width.
+ StaticLayout layout = new StaticLayout(text,
+ textPaint,
+ maxWidth,
+ Alignment.ALIGN_NORMAL,
+ getLineSpacingMultiplier(),
+ getLineSpacingExtra(),
+ true);
+
+ // Return false if we need more than maxLines. The text is obviously too big in this case.
+ if (maxLines != NO_LINE_LIMIT && layout.getLineCount() > maxLines) {
+ return false;
+ }
+ // Return false if the height of the layout is too big.
+ return layout.getHeight() <= availableSpace.bottom;
+ }
+ }
+
+ /**
+ * Computes the final text size to use for this text view, factoring in any previously
+ * cached computations.
+ *
+ * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit}
+ * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit}
+ */
+ private float computeTextSize(int minSize, int maxSize, RectF availableSpace) {
+ CharSequence text = getText();
+ if (text != null && textSizesCache.get(text.hashCode()) != 0) {
+ return textSizesCache.get(text.hashCode());
+ }
+ int size = binarySearchSizes(minSize, maxSize, availableSpace);
+ textSizesCache.put(text == null ? 0 : text.hashCode(), size);
+ return size;
+ }
+
+ /**
+ * Performs a binary search to find the largest font size that will still fit within the size
+ * available to this view.
+ * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit}
+ * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit}
+ */
+ private int binarySearchSizes(int minSize, int maxSize, RectF availableSpace) {
+ int bestSize = minSize;
+ int low = minSize + 1;
+ int high = maxSize;
+ int sizeToTry;
+ while (low <= high) {
+ sizeToTry = (low + high) / 2;
+ float dimension = TypedValue.applyDimension(resizeStepUnit, sizeToTry, displayMetrics);
+ if (suggestedSizeFitsInSpace(dimension, availableSpace)) {
+ bestSize = low;
+ low = sizeToTry + 1;
+ } else {
+ high = sizeToTry - 1;
+ bestSize = high;
+ }
+ }
+ return bestSize;
+ }
+
+ private float convertToResizeStepUnits(float dimension) {
+ // To figure out the multiplier between a raw dimension and the resizeStepUnit, we invert the
+ // conversion of 1 resizeStepUnit to a raw dimension.
+ float multiplier = 1 / TypedValue.applyDimension(resizeStepUnit, 1, displayMetrics);
+ return dimension * multiplier;
+ }
+
+ @Override
+ protected final void onTextChanged(
+ final CharSequence text, final int start, final int before, final int after) {
+ super.onTextChanged(text, start, before, after);
+ adjustTextSize();
+ }
+
+ @Override
+ protected final void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+ if (width != oldWidth || height != oldHeight) {
+ textSizesCache.clear();
+ adjustTextSize();
+ }
+ }
+
+ @Override
+ protected final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ adjustTextSize();
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}