From 5cd7efb267a809d53150fbded2750f13f99449c4 Mon Sep 17 00:00:00 2001 From: Andrei Stingaceanu Date: Wed, 2 Nov 2016 17:32:41 +0000 Subject: [PATCH] AutoSize TextView (part 1) - minimal end-to-end Introduced the minimal number of attributes needed to do autosizing and the autosize functions bundled in TextView (for now). Note that in this first version the autosizing can only be controlled via construction. Next: introduce getters/setters for the new attributes. Bug: 32221168 Test: added a minimal smoke-ish CTS which exercises the new attributes. Change-Id: Idf2195f6a600bfb7908b703ea046209b5868c521 --- api/current.txt | 6 + api/system-current.txt | 6 + api/test-current.txt | 6 + core/java/android/widget/TextView.java | 176 +++++++++++++++++++++++++ core/res/res/values/attrs.xml | 17 +++ core/res/res/values/public.xml | 4 + 6 files changed, 215 insertions(+) diff --git a/api/current.txt b/api/current.txt index 10d37dba20b25..33564a28ddf33 100644 --- a/api/current.txt +++ b/api/current.txt @@ -289,6 +289,10 @@ package android { field public static final int autoLink = 16842928; // 0x10100b0 field public static final int autoMirrored = 16843754; // 0x10103ea field public static final int autoRemoveFromRecents = 16843847; // 0x1010447 + field public static final int autoSizeMinTextSize = 16844088; // 0x1010538 + field public static final int autoSizeStepGranularity = 16844086; // 0x1010536 + field public static final int autoSizeStepSizeSet = 16844087; // 0x1010537 + field public static final int autoSizeText = 16844085; // 0x1010535 field public static final int autoStart = 16843445; // 0x10102b5 field public static final deprecated int autoText = 16843114; // 0x101016a field public static final int autoUrlDetect = 16843404; // 0x101028c @@ -48746,6 +48750,8 @@ package android.widget { method public void setTypeface(android.graphics.Typeface, int); method public void setTypeface(android.graphics.Typeface); method public void setWidth(int); + field public static final int AUTO_SIZE_TYPE_NONE = 0; // 0x0 + field public static final int AUTO_SIZE_TYPE_XY = 1; // 0x1 } public static final class TextView.BufferType extends java.lang.Enum { diff --git a/api/system-current.txt b/api/system-current.txt index 0370287a1e54f..150d925158452 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -396,6 +396,10 @@ package android { field public static final int autoLink = 16842928; // 0x10100b0 field public static final int autoMirrored = 16843754; // 0x10103ea field public static final int autoRemoveFromRecents = 16843847; // 0x1010447 + field public static final int autoSizeMinTextSize = 16844088; // 0x1010538 + field public static final int autoSizeStepGranularity = 16844086; // 0x1010536 + field public static final int autoSizeStepSizeSet = 16844087; // 0x1010537 + field public static final int autoSizeText = 16844085; // 0x1010535 field public static final int autoStart = 16843445; // 0x10102b5 field public static final deprecated int autoText = 16843114; // 0x101016a field public static final int autoUrlDetect = 16843404; // 0x101028c @@ -52267,6 +52271,8 @@ package android.widget { method public void setTypeface(android.graphics.Typeface, int); method public void setTypeface(android.graphics.Typeface); method public void setWidth(int); + field public static final int AUTO_SIZE_TYPE_NONE = 0; // 0x0 + field public static final int AUTO_SIZE_TYPE_XY = 1; // 0x1 } public static final class TextView.BufferType extends java.lang.Enum { diff --git a/api/test-current.txt b/api/test-current.txt index 6b1d698f5eaaf..0730cc4a52167 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -289,6 +289,10 @@ package android { field public static final int autoLink = 16842928; // 0x10100b0 field public static final int autoMirrored = 16843754; // 0x10103ea field public static final int autoRemoveFromRecents = 16843847; // 0x1010447 + field public static final int autoSizeMinTextSize = 16844088; // 0x1010538 + field public static final int autoSizeStepGranularity = 16844086; // 0x1010536 + field public static final int autoSizeStepSizeSet = 16844087; // 0x1010537 + field public static final int autoSizeText = 16844085; // 0x1010535 field public static final int autoStart = 16843445; // 0x10102b5 field public static final deprecated int autoText = 16843114; // 0x101016a field public static final int autoUrlDetect = 16843404; // 0x101028c @@ -49004,6 +49008,8 @@ package android.widget { method public void setTypeface(android.graphics.Typeface, int); method public void setTypeface(android.graphics.Typeface); method public void setWidth(int); + field public static final int AUTO_SIZE_TYPE_NONE = 0; // 0x0 + field public static final int AUTO_SIZE_TYPE_XY = 1; // 0x1 } public static final class TextView.BufferType extends java.lang.Enum { diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index dba86556fd1d5..b85175d655f8a 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -159,6 +159,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.Locale; /** @@ -617,6 +618,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private Rect mTempRect; private long mLastScroll; private Scroller mScroller; + private TextPaint mTempTextPaint; private BoringLayout.Metrics mBoring, mHintBoring; private BoringLayout mSavedLayout, mSavedHintLayout; @@ -667,6 +669,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ private int mDeviceProvisionedState = DEVICE_PROVISIONED_UNKNOWN; + // The TextView does not auto-size text. + public static final int AUTO_SIZE_TYPE_NONE = 0; + // The TextView performs uniform horizontal and vertical text size scaling to fit within the + // container. + public static final int AUTO_SIZE_TYPE_XY = 1; + // Auto-size type. + private int mAutoSizeType = AUTO_SIZE_TYPE_NONE; + // Default value for the step size in pixels. + private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1; + // Contains the sorted set of desired text sizes in pixels to pick from when auto-sizing text. + private int[] mAutoSizeTextSizesInPx; + // Specifies if the current TextView needs to be auto-sized. + private boolean mNeedsTextAutoResize = false; + /** * Kick-start the font cache for the zygote process (to pay the cost of * initializing freetype for our default font only once). @@ -869,6 +885,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener CharSequence hint = null; boolean password = false; int inputType = EditorInfo.TYPE_NULL; + int autoSizeStepGranularityInPx = DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX; + int autoSizeMinTextSize = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics()); a = theme.obtainStyledAttributes( attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes); @@ -1223,6 +1242,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_hyphenationFrequency: mHyphenationFrequency = a.getInt(attr, Layout.HYPHENATION_FREQUENCY_NONE); break; + + case com.android.internal.R.styleable.TextView_autoSizeText: + mAutoSizeType = a.getInt(attr, AUTO_SIZE_TYPE_NONE); + break; + + case com.android.internal.R.styleable.TextView_autoSizeStepGranularity: + autoSizeStepGranularityInPx = a.getDimensionPixelSize( + attr, DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX); + break; + + case com.android.internal.R.styleable.TextView_autoSizeMinTextSize: + autoSizeMinTextSize = a.getDimensionPixelSize(attr, autoSizeMinTextSize); + break; } } a.recycle(); @@ -1500,6 +1532,43 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } + + // Setup auto-size. + if (mEditor == null) { + switch (mAutoSizeType) { + case AUTO_SIZE_TYPE_NONE: + // Nothing to do. + break; + case AUTO_SIZE_TYPE_XY: + // getTextSize() represents the maximum text size. + if (getTextSize() <= autoSizeMinTextSize) { + throw new IllegalStateException("Maximum text size is less then minimum " + + "text size"); + } + + if (autoSizeStepGranularityInPx <= 0) { + throw new IllegalStateException("Unexpected zero or negative value for auto" + + " size step granularity in pixels"); + } + + final int autoSizeValuesLength = (int) ((getTextSize() - autoSizeMinTextSize) + / autoSizeStepGranularityInPx); + mAutoSizeTextSizesInPx = new int[autoSizeValuesLength]; + int sizeToAdd = autoSizeMinTextSize; + for (int i = 0; i < autoSizeValuesLength; i++) { + mAutoSizeTextSizesInPx[i] = sizeToAdd; + sizeToAdd += autoSizeStepGranularityInPx; + } + + Arrays.sort(mAutoSizeTextSizesInPx); + mNeedsTextAutoResize = true; + break; + default: + throw new IllegalArgumentException( + "Unknown autoSizeText type: " + mAutoSizeType); + } + + } } private int[] parseDimensionArray(TypedArray dimens) { @@ -2954,6 +3023,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * @return the size (in pixels) of the default text size in this TextView. + * + *

Note: if this TextView has mAutoSizeType set to {@link TextView#AUTO_SIZE_TYPE_XY} than + * this function returns the maximum text size for auto-sizing. */ @ViewDebug.ExportedProperty(category = "text") public float getTextSize() { @@ -2986,6 +3058,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * pixel" units. This size is adjusted based on the current density and * user font size preference. * + *

Note: if this TextView has mAutoSizeType set to {@link TextView#AUTO_SIZE_TYPE_XY} than + * this function sets the maximum text size for auto-sizing. + * * @param size The scaled pixel size. * * @attr ref android.R.styleable#TextView_textSize @@ -2999,6 +3074,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Set the default text size to a given unit and value. See {@link * TypedValue} for the possible dimension units. * + *

Note: if this TextView has mAutoSizeType set to {@link TextView#AUTO_SIZE_TYPE_XY} than + * this function sets the maximum text size for auto-sizing. + * * @param unit The desired dimension unit. * @param size The desired size in the given units. * @@ -7446,9 +7524,107 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollTo(0, 0); } + if (mNeedsTextAutoResize) { + // Call auto-size after the width and height have been calculated. + autoSizeText(); + } + setMeasuredDimension(width, height); } + /** + * Automatically computes and sets the text size. + */ + private void autoSizeText() { + synchronized (TEMP_RECTF) { + TEMP_RECTF.setEmpty(); + final int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + final int maxHeight = getMeasuredHeight() - getPaddingBottom() - getPaddingTop(); + + if (maxWidth <= 0 || maxHeight <= 0) { + return; + } + + TEMP_RECTF.right = maxWidth; + TEMP_RECTF.bottom = maxHeight; + final float textSize = findLargestTextSizeWhichFits(TEMP_RECTF); + setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + mNeedsTextAutoResize = false; + } + } + + /** + * Performs a binary search to find the largest text size that will still fit within the size + * available to this view. + */ + private int findLargestTextSizeWhichFits(RectF availableSpace) { + final int sizesCount = mAutoSizeTextSizesInPx.length; + if (sizesCount == 0) { + throw new IllegalStateException("No available text sizes to choose from."); + } + + int bestSizeIndex = 0; + int lowIndex = bestSizeIndex + 1; + int highIndex = sizesCount - 1; + int sizeToTryIndex; + while (lowIndex <= highIndex) { + sizeToTryIndex = (lowIndex + highIndex) / 2; + if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) { + bestSizeIndex = lowIndex; + lowIndex = sizeToTryIndex + 1; + } else { + highIndex = sizeToTryIndex - 1; + bestSizeIndex = highIndex; + } + } + + return mAutoSizeTextSizesInPx[bestSizeIndex]; + } + + private boolean suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace) { + final CharSequence text = getText(); + final int maxLines = getMaxLines(); + if (mTempTextPaint == null) { + mTempTextPaint = new TextPaint(); + } else { + mTempTextPaint.reset(); + } + mTempTextPaint.set(getPaint()); + mTempTextPaint.setTextSize(suggestedSizeInPx); + + if ((mLayout instanceof BoringLayout) && BoringLayout.isBoring( + text, mTempTextPaint, getTextDirectionHeuristic(), mBoring) != null) { + return mTempTextPaint.getFontSpacing() + getPaddingTop() + getPaddingBottom() + <= availableSpace.bottom + && mTempTextPaint.measureText(text, 0, text.length()) + + getPaddingLeft() + getPaddingRight() <= availableSpace.right; + } else { + StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(text, 0, text.length(), + mTempTextPaint, getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + layoutBuilder.setAlignment(getLayoutAlignment()); + layoutBuilder.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier()); + layoutBuilder.setIncludePad(true); + StaticLayout layout = layoutBuilder.build(); + + // Lines overflow. + if (maxLines != -1 && layout.getLineCount() > maxLines) { + return false; + } + + // Width overflow. + if (layout.getWidth() > availableSpace.right) { + return false; + } + + // Height overflow. + if (layout.getHeight() > availableSpace.bottom) { + return false; + } + } + + return true; + } + private int getDesiredHeight() { return Math.max( getDesiredHeight(mLayout, true), diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index acafbcfd428ab..95f372c43a59a 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4624,6 +4624,23 @@ screens with limited space for text. --> + + + + + + + + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index ae82128b88cf8..200961f1c8a88 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2760,6 +2760,10 @@ + + + +