content: /*
   * Copyright 2017 Google Inc.
   *
   * 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.example.android.unsplash.transition;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.ArgbEvaluator;
  import android.animation.ObjectAnimator;
  import android.animation.PropertyValuesHolder;
  import android.content.Context;
  import android.content.res.ColorStateList;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.ColorFilter;
  import android.graphics.Paint;
  import android.graphics.PixelFormat;
  import android.graphics.PorterDuff;
  import android.graphics.drawable.Drawable;
  import android.transition.Transition;
  import android.transition.TransitionValues;
  import android.util.AttributeSet;
  import android.util.TypedValue;
  import android.view.Gravity;
  import android.view.View;
  import android.view.ViewGroup;
  import android.widget.TextView;
  
  /**
   * Transitions a TextView from one font size to another. This does not
   * do any animation of TextView content and if the text changes, this
   * transition will not run.
   * <p>
   * 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();
          }
      }
  }
  