* The animation works by capturing a bitmap of the text at the start * and end states. It then scales the start bitmap until it reaches * a threshold and switches to the scaled end bitmap for the remainder * of the animation. This keeps the jump in bitmaps in the middle of * the animation, where it is less noticeable than at the beginning * or end of the animation. This transition does not work well with * cropped text. TextResize also does not work with changes in * TextView gravity. */ public class TextResize extends Transition { private static final String FONT_SIZE = "TextResize:fontSize"; private static final String DATA = "TextResize:data"; private static final String[] PROPERTIES = { // We only care about FONT_SIZE. If anything else changes, we don't // want this transition to be called to create an Animator. FONT_SIZE, }; public TextResize() { addTarget(TextView.class); } /** * Constructor used from XML. */ public TextResize(Context context, AttributeSet attrs) { super(context, attrs); addTarget(TextView.class); } @Override public String[] getTransitionProperties() { return PROPERTIES; } @Override public void captureStartValues(TransitionValues transitionValues) { captureValues(transitionValues); } @Override public void captureEndValues(TransitionValues transitionValues) { captureValues(transitionValues); } private void captureValues(TransitionValues transitionValues) { if (!(transitionValues.view instanceof TextView)) { return; } final TextView view = (TextView) transitionValues.view; final float fontSize = view.getTextSize(); transitionValues.values.put(FONT_SIZE, fontSize); final TextResizeData data = new TextResizeData(view); transitionValues.values.put(DATA, data); } @Override public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { if (startValues == null || endValues == null) { return null; } final TextResizeData startData = (TextResizeData) startValues.values.get(DATA); final TextResizeData endData = (TextResizeData) endValues.values.get(DATA); if (startData.gravity != endData.gravity) { return null; // Can't deal with changes in gravity } final TextView textView = (TextView) endValues.view; float startFontSize = (Float) startValues.values.get(FONT_SIZE); // Capture the start bitmap -- we need to set the values to the start values first setTextViewData(textView, startData, startFontSize); final float startWidth = textView.getPaint().measureText(textView.getText().toString()); final Bitmap startBitmap = captureTextBitmap(textView); if (startBitmap == null) { startFontSize = 0; } float endFontSize = (Float) endValues.values.get(FONT_SIZE); // Set the values to the end values setTextViewData(textView, endData, endFontSize); final float endWidth = textView.getPaint().measureText(textView.getText().toString()); // Capture the end bitmap final Bitmap endBitmap = captureTextBitmap(textView); if (endBitmap == null) { endFontSize = 0; } if (startFontSize == 0 && endFontSize == 0) { return null; // Can't animate null bitmaps } // Set the colors of the TextView so that nothing is drawn. // Only draw the bitmaps in the overlay. final ColorStateList textColors = textView.getTextColors(); final ColorStateList hintColors = textView.getHintTextColors(); final int highlightColor = textView.getHighlightColor(); final ColorStateList linkColors = textView.getLinkTextColors(); textView.setTextColor(Color.TRANSPARENT); textView.setHintTextColor(Color.TRANSPARENT); textView.setHighlightColor(Color.TRANSPARENT); textView.setLinkTextColor(Color.TRANSPARENT); // Create the drawable that will be animated in the TextView's overlay. // Ensure that it is showing the start state now. final SwitchBitmapDrawable drawable = new SwitchBitmapDrawable(textView, startData.gravity, startBitmap, startFontSize, startWidth, endBitmap, endFontSize, endWidth); textView.getOverlay().add(drawable); // Properties: left, top, font size, text color final PropertyValuesHolder leftProp = PropertyValuesHolder.ofFloat("left", startData.paddingLeft, endData.paddingLeft); final PropertyValuesHolder topProp = PropertyValuesHolder.ofFloat("top", startData.paddingTop, endData.paddingTop); final PropertyValuesHolder rightProp = PropertyValuesHolder.ofFloat("right", startData.width - startData.paddingRight, endData.width - endData.paddingRight); final PropertyValuesHolder bottomProp = PropertyValuesHolder.ofFloat("bottom", startData.height - startData.paddingBottom, endData.height - endData.paddingBottom); final PropertyValuesHolder fontSizeProp = PropertyValuesHolder.ofFloat("fontSize", startFontSize, endFontSize); final ObjectAnimator animator; if (startData.textColor != endData.textColor) { final PropertyValuesHolder textColorProp = PropertyValuesHolder.ofObject("textColor", new ArgbEvaluator(), startData.textColor, endData.textColor); animator = ObjectAnimator.ofPropertyValuesHolder(drawable, leftProp, topProp, rightProp, bottomProp, fontSizeProp, textColorProp); } else { animator = ObjectAnimator.ofPropertyValuesHolder(drawable, leftProp, topProp, rightProp, bottomProp, fontSizeProp); } final float finalFontSize = endFontSize; AnimatorListenerAdapter listener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { textView.getOverlay().remove(drawable); textView.setTextColor(textColors); textView.setHintTextColor(hintColors); textView.setHighlightColor(highlightColor); textView.setLinkTextColor(linkColors); } @Override public void onAnimationPause(Animator animation) { textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawable.getFontSize()); final int paddingLeft = Math.round(drawable.getLeft()); final int paddingTop = Math.round(drawable.getTop()); final float fraction = animator.getAnimatedFraction(); final int paddingRight = Math.round(interpolate(startData.paddingRight, endData.paddingRight, fraction)); final int paddingBottom = Math.round(interpolate(startData.paddingBottom, endData.paddingBottom, fraction)); textView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); textView.setTextColor(drawable.getTextColor()); } @Override public void onAnimationResume(Animator animation) { textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, finalFontSize); textView.setPadding(endData.paddingLeft, endData.paddingTop, endData.paddingRight, endData.paddingBottom); textView.setTextColor(endData.textColor); } }; animator.addListener(listener); animator.addPauseListener(listener); return animator; } private static void setTextViewData(TextView view, TextResizeData data, float fontSize) { view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); view.setPadding(data.paddingLeft, data.paddingTop, data.paddingRight, data.paddingBottom); view.setRight(view.getLeft() + data.width); view.setBottom(view.getTop() + data.height); view.setTextColor(data.textColor); int widthSpec = View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY); int heightSpec = View.MeasureSpec.makeMeasureSpec(view.getHeight(), View.MeasureSpec.EXACTLY); view.measure(widthSpec, heightSpec); view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); } private static Bitmap captureTextBitmap(TextView textView) { Drawable background = textView.getBackground(); textView.setBackground(null); int width = textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight(); int height = textView.getHeight() - textView.getPaddingTop() - textView.getPaddingBottom(); if (width == 0 || height == 0) { return null; } Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.translate(-textView.getPaddingLeft(), -textView.getPaddingTop()); textView.draw(canvas); textView.setBackground(background); return bitmap; } private static float interpolate(float start, float end, float fraction) { return start + (fraction * (end - start)); } /** * This Drawable is used to scale the start and end bitmaps and switch between them * at the appropriate progress. */ private static class SwitchBitmapDrawable extends Drawable { private final TextView view; private final int horizontalGravity; private final int verticalGravity; private final Bitmap startBitmap; private final Bitmap endBitmap; private final Paint paint = new Paint(); private final float startFontSize; private final float endFontSize; private final float startWidth; private final float endWidth; private float fontSize; private float left; private float top; private float right; private float bottom; private int textColor; public SwitchBitmapDrawable(TextView view, int gravity, Bitmap startBitmap, float startFontSize, float startWidth, Bitmap endBitmap, float endFontSize, float endWidth) { this.view = view; this.horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; this.verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; this.startBitmap = startBitmap; this.endBitmap = endBitmap; this.startFontSize = startFontSize; this.endFontSize = endFontSize; this.startWidth = startWidth; this.endWidth = endWidth; } @Override public void invalidateSelf() { super.invalidateSelf(); view.invalidate(); } /** * Sets the font size that the text should be displayed at. * * @param fontSize The font size in pixels of the scaled bitmap text. */ public void setFontSize(float fontSize) { this.fontSize = fontSize; invalidateSelf(); } /** * Sets the color of the text to be displayed. * * @param textColor The color of the text to be displayed. */ public void setTextColor(int textColor) { this.textColor = textColor; setColorFilter(textColor, PorterDuff.Mode.SRC_IN); invalidateSelf(); } /** * Sets the left side of the text. This should be the same as the left padding. * * @param left The left side of the text in pixels. */ public void setLeft(float left) { this.left = left; invalidateSelf(); } /** * Sets the top of the text. This should be the same as the top padding. * * @param top The top of the text in pixels. */ public void setTop(float top) { this.top = top; invalidateSelf(); } /** * Sets the right of the drawable. * * @param right The right pixel of the drawn area. */ public void setRight(float right) { this.right = right; invalidateSelf(); } /** * Sets the bottom of the drawable. * * @param bottom The bottom pixel of the drawn area. */ public void setBottom(float bottom) { this.bottom = bottom; invalidateSelf(); } /** * @return The left side of the text. */ public float getLeft() { return left; } /** * @return The top of the text. */ public float getTop() { return top; } /** * @return The right side of the text. */ public float getRight() { return right; } /** * @return The bottom of the text. */ public float getBottom() { return bottom; } /** * @return The font size of the text in the displayed bitmap. */ public float getFontSize() { return fontSize; } /** * @return The color of the text being displayed. */ public int getTextColor() { return textColor; } @Override public void draw(Canvas canvas) { int saveCount = canvas.save(); // The threshold changes depending on the target font sizes. Because scaled-up // fonts look bad, we want to switch when closer to the smaller font size. This // algorithm ensures that null bitmaps (font size = 0) are never used. final float threshold = startFontSize / (startFontSize + endFontSize); final float fontSize = getFontSize(); final float progress = (fontSize - startFontSize)/(endFontSize - startFontSize); // The drawn text width is a more accurate scale than font size. This avoids // jump when switching bitmaps. final float expectedWidth = interpolate(startWidth, endWidth, progress); if (progress < threshold) { // draw start bitmap final float scale = expectedWidth / startWidth; float tx = getTranslationPoint(horizontalGravity, left, right, startBitmap.getWidth(), scale); float ty = getTranslationPoint(verticalGravity, top, bottom, startBitmap.getHeight(), scale); canvas.translate(tx, ty); canvas.scale(scale, scale); canvas.drawBitmap(startBitmap, 0, 0, paint); } else { // draw end bitmap final float scale = expectedWidth / endWidth; float tx = getTranslationPoint(horizontalGravity, left, right, endBitmap.getWidth(), scale); float ty = getTranslationPoint(verticalGravity, top, bottom, endBitmap.getHeight(), scale); canvas.translate(tx, ty); canvas.scale(scale, scale); canvas.drawBitmap(endBitmap, 0, 0, paint); } canvas.restoreToCount(saveCount); } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter colorFilter) { paint.setColorFilter(colorFilter); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } private float getTranslationPoint(int gravity, float start, float end, float dim, float scale) { switch (gravity) { case Gravity.CENTER_HORIZONTAL: case Gravity.CENTER_VERTICAL: return ((start + end) - (dim * scale))/2f; case Gravity.RIGHT: case Gravity.BOTTOM: return end - (dim * scale); case Gravity.LEFT: case Gravity.TOP: default: return start; } } } /** * Contains all the non-font-size data used by the TextResize transition. * None of these values should trigger the transition, so they are not listed * in PROPERTIES. These are captured together to avoid boxing of all the * primitives while adding to TransitionValues. */ static class TextResizeData { public final int paddingLeft; public final int paddingTop; public final int paddingRight; public final int paddingBottom; public final int width; public final int height; public final int gravity; public final int textColor; public TextResizeData(TextView textView) { this.paddingLeft = textView.getPaddingLeft(); this.paddingTop = textView.getPaddingTop(); this.paddingRight = textView.getPaddingRight(); this.paddingBottom = textView.getPaddingBottom(); this.width = textView.getWidth(); this.height = textView.getHeight(); this.gravity = textView.getGravity(); this.textColor = textView.getCurrentTextColor(); } } }