content: package com.android.launcher3.pageindicators;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.ObjectAnimator;
  import android.animation.ValueAnimator;
  import android.content.Context;
  import android.content.res.Resources;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Paint;
  import android.graphics.Rect;
  import android.os.Handler;
  import android.os.Looper;
  import android.util.AttributeSet;
  import android.util.Property;
  import android.view.Gravity;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.widget.FrameLayout;
  
  import com.android.launcher3.DeviceProfile;
  import com.android.launcher3.Insettable;
  import com.android.launcher3.Launcher;
  import com.android.launcher3.R;
  import com.android.launcher3.Utilities;
  import com.android.launcher3.uioverrides.WallpaperColorInfo;
  
  /**
   * A PageIndicator that briefly shows a fraction of a line when moving between pages
   *
   * The fraction is 1 / number of pages and the position is based on the progress of the page scroll.
   */
  public class WorkspacePageIndicator extends View implements Insettable, PageIndicator {
  
      private static final int LINE_ANIMATE_DURATION = ViewConfiguration.getScrollBarFadeDuration();
      private static final int LINE_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay();
      public static final int WHITE_ALPHA = (int) (0.70f * 255);
      public static final int BLACK_ALPHA = (int) (0.65f * 255);
  
      private static final int LINE_ALPHA_ANIMATOR_INDEX = 0;
      private static final int NUM_PAGES_ANIMATOR_INDEX = 1;
      private static final int TOTAL_SCROLL_ANIMATOR_INDEX = 2;
      private static final int ANIMATOR_COUNT = 3;
  
      private ValueAnimator[] mAnimators = new ValueAnimator[ANIMATOR_COUNT];
  
      private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper());
      private final Launcher mLauncher;
  
      private boolean mShouldAutoHide = true;
  
      // The alpha of the line when it is showing.
      private int mActiveAlpha = 0;
      // The alpha that the line is being animated to or already at (either 0 or mActiveAlpha).
      private int mToAlpha;
      // A float value representing the number of pages, to allow for an animation when it changes.
      private float mNumPagesFloat;
      private int mCurrentScroll;
      private int mTotalScroll;
      private Paint mLinePaint;
      private final int mLineHeight;
  
      private static final Property<WorkspacePageIndicator, Integer> PAINT_ALPHA
              = new Property<WorkspacePageIndicator, Integer>(Integer.class, "paint_alpha") {
          @Override
          public Integer get(WorkspacePageIndicator obj) {
              return obj.mLinePaint.getAlpha();
          }
  
          @Override
          public void set(WorkspacePageIndicator obj, Integer alpha) {
              obj.mLinePaint.setAlpha(alpha);
              obj.invalidate();
          }
      };
  
      private static final Property<WorkspacePageIndicator, Float> NUM_PAGES
              = new Property<WorkspacePageIndicator, Float>(Float.class, "num_pages") {
          @Override
          public Float get(WorkspacePageIndicator obj) {
              return obj.mNumPagesFloat;
          }
  
          @Override
          public void set(WorkspacePageIndicator obj, Float numPages) {
              obj.mNumPagesFloat = numPages;
              obj.invalidate();
          }
      };
  
      private static final Property<WorkspacePageIndicator, Integer> TOTAL_SCROLL
              = new Property<WorkspacePageIndicator, Integer>(Integer.class, "total_scroll") {
          @Override
          public Integer get(WorkspacePageIndicator obj) {
              return obj.mTotalScroll;
          }
  
          @Override
          public void set(WorkspacePageIndicator obj, Integer totalScroll) {
              obj.mTotalScroll = totalScroll;
              obj.invalidate();
          }
      };
  
      private Runnable mHideLineRunnable = () -> animateLineToAlpha(0);
  
      public WorkspacePageIndicator(Context context) {
          this(context, null);
      }
  
      public WorkspacePageIndicator(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
  
          Resources res = context.getResources();
          mLinePaint = new Paint();
          mLinePaint.setAlpha(0);
  
          mLauncher = Launcher.getLauncher(context);
          mLineHeight = res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_line_height);
  
          boolean darkText = WallpaperColorInfo.getInstance(context).supportsDarkText();
          mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA;
          mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE);
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          if (mTotalScroll == 0 || mNumPagesFloat == 0) {
              return;
          }
  
          // Compute and draw line rect.
          float progress = Utilities.boundToRange(((float) mCurrentScroll) / mTotalScroll, 0f, 1f);
          int availableWidth = getWidth();
          int lineWidth = (int) (availableWidth / mNumPagesFloat);
          int lineLeft = (int) (progress * (availableWidth - lineWidth));
          int lineRight = lineLeft + lineWidth;
  
          canvas.drawRoundRect(lineLeft, getHeight() / 2 - mLineHeight / 2, lineRight,
                  getHeight() / 2 + mLineHeight / 2, mLineHeight, mLineHeight, mLinePaint);
      }
  
      @Override
      public void setScroll(int currentScroll, int totalScroll) {
          if (getAlpha() == 0) {
              return;
          }
          animateLineToAlpha(mActiveAlpha);
  
          mCurrentScroll = currentScroll;
          if (mTotalScroll == 0) {
              mTotalScroll = totalScroll;
          } else if (mTotalScroll != totalScroll) {
              animateToTotalScroll(totalScroll);
          } else {
              invalidate();
          }
  
          if (mShouldAutoHide) {
              hideAfterDelay();
          }
      }
  
      private void hideAfterDelay() {
          mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
          mDelayedLineFadeHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY);
      }
  
      @Override
      public void setActiveMarker(int activePage) { }
  
      @Override
      public void setMarkersCount(int numMarkers) {
          if (Float.compare(numMarkers, mNumPagesFloat) != 0) {
              setupAndRunAnimation(ObjectAnimator.ofFloat(this, NUM_PAGES, numMarkers),
                      NUM_PAGES_ANIMATOR_INDEX);
          } else {
              if (mAnimators[NUM_PAGES_ANIMATOR_INDEX] != null) {
                  mAnimators[NUM_PAGES_ANIMATOR_INDEX].cancel();
                  mAnimators[NUM_PAGES_ANIMATOR_INDEX] = null;
              }
          }
      }
  
      public void setShouldAutoHide(boolean shouldAutoHide) {
          mShouldAutoHide = shouldAutoHide;
          if (shouldAutoHide && mLinePaint.getAlpha() > 0) {
              hideAfterDelay();
          } else if (!shouldAutoHide) {
              mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
          }
      }
  
      private void animateLineToAlpha(int alpha) {
          if (alpha == mToAlpha) {
              // Ignore the new animation if it is going to the same alpha as the current animation.
              return;
          }
          mToAlpha = alpha;
          setupAndRunAnimation(ObjectAnimator.ofInt(this, PAINT_ALPHA, alpha),
                  LINE_ALPHA_ANIMATOR_INDEX);
      }
  
      private void animateToTotalScroll(int totalScroll) {
          setupAndRunAnimation(ObjectAnimator.ofInt(this, TOTAL_SCROLL, totalScroll),
                  TOTAL_SCROLL_ANIMATOR_INDEX);
      }
  
      /**
       * Starts the given animator and stores it in the provided index in {@link #mAnimators} until
       * the animation ends.
       *
       * If an animator is already at the index (i.e. it is already playing), it is canceled and
       * replaced with the new animator.
       */
      private void setupAndRunAnimation(ValueAnimator animator, final int animatorIndex) {
          if (mAnimators[animatorIndex] != null) {
              mAnimators[animatorIndex].cancel();
          }
          mAnimators[animatorIndex] = animator;
          mAnimators[animatorIndex].addListener(new AnimatorListenerAdapter() {
              @Override
              public void onAnimationEnd(Animator animation) {
                  mAnimators[animatorIndex] = null;
              }
          });
          mAnimators[animatorIndex].setDuration(LINE_ANIMATE_DURATION);
          mAnimators[animatorIndex].start();
      }
  
      /**
       * Pauses all currently running animations.
       */
      public void pauseAnimations() {
          for (int i = 0; i < ANIMATOR_COUNT; i++) {
              if (mAnimators[i] != null) {
                  mAnimators[i].pause();
              }
          }
      }
  
      /**
       * Force-ends all currently running or paused animations.
       */
      public void skipAnimationsToEnd() {
          for (int i = 0; i < ANIMATOR_COUNT; i++) {
              if (mAnimators[i] != null) {
                  mAnimators[i].end();
              }
          }
      }
  
      @Override
      public void setInsets(Rect insets) {
          DeviceProfile grid = mLauncher.getDeviceProfile();
          FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
  
          if (grid.isVerticalBarLayout()) {
              Rect padding = grid.workspacePadding;
              lp.leftMargin = padding.left + grid.workspaceCellPaddingXPx;
              lp.rightMargin = padding.right + grid.workspaceCellPaddingXPx;
              lp.bottomMargin = padding.bottom;
          } else {
              lp.leftMargin = lp.rightMargin = 0;
              lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
              lp.bottomMargin = grid.hotseatBarSizePx + insets.bottom;
          }
          setLayoutParams(lp);
      }
  }
  