content: /*
   * Copyright (C) 2017 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.launcher3.graphics;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.ObjectAnimator;
  import android.content.Context;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Matrix;
  import android.graphics.Paint;
  import android.graphics.Path;
  import android.graphics.PathMeasure;
  import android.graphics.Rect;
  import android.util.Property;
  import android.util.SparseArray;
  import android.view.animation.LinearInterpolator;
  
  import com.android.launcher3.FastBitmapDrawable;
  
  import java.lang.ref.WeakReference;
  
  /**
   * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
   */
  public class PreloadIconDrawable extends FastBitmapDrawable {
  
      private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
              new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
                  @Override
                  public Float get(PreloadIconDrawable object) {
                      return object.mInternalStateProgress;
                  }
  
                  @Override
                  public void set(PreloadIconDrawable object, Float value) {
                      object.setInternalProgress(value);
                  }
              };
  
      public static final int PATH_SIZE = 100;
  
      private static final float PROGRESS_WIDTH = 7;
      private static final float PROGRESS_GAP = 2;
      private static final int MAX_PAINT_ALPHA = 255;
  
      private static final long DURATION_SCALE = 500;
  
      // The smaller the number, the faster the animation would be.
      // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
      private static final float COMPLETE_ANIM_FRACTION = 0.3f;
  
      private static final int COLOR_TRACK = 0x77EEEEEE;
      private static final int COLOR_SHADOW = 0x55000000;
  
      private static final float SMALL_SCALE = 0.6f;
  
      private static final SparseArray<WeakReference<Bitmap>> sShadowCache = new SparseArray<>();
  
      private final Matrix mTmpMatrix = new Matrix();
      private final PathMeasure mPathMeasure = new PathMeasure();
  
      private final Context mContext;
  
      // Path in [0, 100] bounds.
      private final Path mProgressPath;
  
      private final Path mScaledTrackPath;
      private final Path mScaledProgressPath;
      private final Paint mProgressPaint;
  
      private Bitmap mShadowBitmap;
      private int mIndicatorColor = 0;
  
      private int mTrackAlpha;
      private float mTrackLength;
      private float mIconScale;
  
      private boolean mRanFinishAnimation;
  
      // Progress of the internal state. [0, 1] indicates the fraction of completed progress,
      // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation.
      private float mInternalStateProgress;
  
      private ObjectAnimator mCurrentAnim;
  
      /**
       * @param progressPath fixed path in the bounds [0, 0, 100, 100] representing a progress bar.
       */
      public PreloadIconDrawable(Bitmap b, Path progressPath, Context context) {
          super(b);
          mContext = context;
          mProgressPath = progressPath;
          mScaledTrackPath = new Path();
          mScaledProgressPath = new Path();
  
          mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
          mProgressPaint.setStyle(Paint.Style.STROKE);
          mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
  
          setInternalProgress(0);
      }
  
      @Override
      protected void onBoundsChange(Rect bounds) {
          super.onBoundsChange(bounds);
          mTmpMatrix.setScale(
                  (bounds.width() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE,
                  (bounds.height() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE);
          mTmpMatrix.postTranslate(
                  bounds.left + PROGRESS_WIDTH + PROGRESS_GAP,
                  bounds.top + PROGRESS_WIDTH + PROGRESS_GAP);
  
          mProgressPath.transform(mTmpMatrix, mScaledTrackPath);
          float scale = bounds.width() / PATH_SIZE;
          mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale);
  
          mShadowBitmap = getShadowBitmap(bounds.width(), bounds.height(),
                  (PROGRESS_GAP ) * scale);
          mPathMeasure.setPath(mScaledTrackPath, true);
          mTrackLength = mPathMeasure.getLength();
  
          setInternalProgress(mInternalStateProgress);
      }
  
      private Bitmap getShadowBitmap(int width, int height, float shadowRadius) {
          int key = (width << 16) | height;
          WeakReference<Bitmap> shadowRef = sShadowCache.get(key);
          Bitmap shadow = shadowRef != null ? shadowRef.get() : null;
          if (shadow != null) {
              return shadow;
          }
          shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
          Canvas c = new Canvas(shadow);
          mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW);
          mProgressPaint.setColor(COLOR_TRACK);
          mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
          c.drawPath(mScaledTrackPath, mProgressPaint);
          mProgressPaint.clearShadowLayer();
          c.setBitmap(null);
  
          sShadowCache.put(key, new WeakReference<>(shadow));
          return shadow;
      }
  
      @Override
      public void draw(Canvas canvas) {
          if (mRanFinishAnimation) {
              super.draw(canvas);
              return;
          }
  
          // Draw track.
          mProgressPaint.setColor(mIndicatorColor);
          mProgressPaint.setAlpha(mTrackAlpha);
          if (mShadowBitmap != null) {
              canvas.drawBitmap(mShadowBitmap, getBounds().left, getBounds().top, mProgressPaint);
          }
          canvas.drawPath(mScaledProgressPath, mProgressPaint);
  
          int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
          Rect bounds = getBounds();
  
          canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY());
          super.draw(canvas);
          canvas.restoreToCount(saveCount);
      }
  
      /**
       * Updates the install progress based on the level
       */
      @Override
      protected boolean onLevelChange(int level) {
          // Run the animation if we have already been bound.
          updateInternalState(level * 0.01f,  getBounds().width() > 0, false);
          return true;
      }
  
      /**
       * Runs the finish animation if it is has not been run after last call to
       * {@link #onLevelChange}
       */
      public void maybePerformFinishedAnimation() {
          // If the drawable was recently initialized, skip the progress animation.
          if (mInternalStateProgress == 0) {
              mInternalStateProgress = 1;
          }
          updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true);
      }
  
      public boolean hasNotCompleted() {
          return !mRanFinishAnimation;
      }
  
      private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
          if (mCurrentAnim != null) {
              mCurrentAnim.cancel();
              mCurrentAnim = null;
          }
  
          if (Float.compare(finalProgress, mInternalStateProgress) == 0) {
              return;
          }
          if (finalProgress < mInternalStateProgress) {
              shouldAnimate = false;
          }
          if (!shouldAnimate || mRanFinishAnimation) {
              setInternalProgress(finalProgress);
          } else {
              mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
              mCurrentAnim.setDuration(
                      (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
              mCurrentAnim.setInterpolator(new LinearInterpolator());
              if (isFinish) {
                  mCurrentAnim.addListener(new AnimatorListenerAdapter() {
                      @Override
                      public void onAnimationEnd(Animator animation) {
                          mRanFinishAnimation = true;
                      }
                  });
              }
              mCurrentAnim.start();
          }
  
      }
  
      /**
       * Sets the internal progress and updates the UI accordingly
       *   for progress <= 0:
       *     - icon in the small scale and disabled state
       *     - progress track is visible
       *     - progress bar is not visible
       *   for 0 < progress < 1
       *     - icon in the small scale and disabled state
       *     - progress track is visible
       *     - progress bar is visible with dominant color. Progress bar is drawn as a fraction of
       *       {@link #mScaledTrackPath}.
       *       @see PathMeasure#getSegment(float, float, Path, boolean)
       *   for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION)
       *     - we calculate fraction of progress in the above range
       *     - progress track is drawn with alpha based on fraction
       *     - progress bar is drawn at 100% with alpha based on fraction
       *     - icon is scaled up based on fraction and is drawn in enabled state
       *   for progress >= (1 + COMPLETE_ANIM_FRACTION)
       *     - only icon is drawn in normal state
       */
      private void setInternalProgress(float progress) {
          mInternalStateProgress = progress;
          if (progress <= 0) {
              mIconScale = SMALL_SCALE;
              mScaledTrackPath.reset();
              mTrackAlpha = MAX_PAINT_ALPHA;
              setIsDisabled(true);
          } else if (mIndicatorColor == 0) {
              // Update the indicator color
              mIndicatorColor = getIconPalette().getPreloadProgressColor(mContext);
          }
  
          if (progress < 1 && progress > 0) {
              mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
              mIconScale = SMALL_SCALE;
              mTrackAlpha = MAX_PAINT_ALPHA;
              setIsDisabled(true);
          } else if (progress >= 1) {
              setIsDisabled(false);
              mScaledTrackPath.set(mScaledProgressPath);
              float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;
  
              if (fraction >= 1) {
                  // Animation has completed
                  mIconScale = 1;
                  mTrackAlpha = 0;
              } else {
                  mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA);
                  mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction;
              }
          }
          invalidateSelf();
      }
  }
  