content: /*
   * Copyright (C) 2015 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.allapps;
  
  import android.support.v7.widget.RecyclerView;
  
  import com.android.launcher3.util.Thunk;
  
  import java.util.HashSet;
  import java.util.List;
  
  public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
  
      private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
      private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
  
      private AllAppsRecyclerView mRv;
      private AlphabeticalAppsList mApps;
  
      // Keeps track of the current and targeted fast scroll section (the section to scroll to after
      // the initial delay)
      int mTargetFastScrollPosition = -1;
      @Thunk String mCurrentFastScrollSection;
      @Thunk String mTargetFastScrollSection;
  
      // The settled states affect the delay before the fast scroll animation is applied
      private boolean mHasFastScrollTouchSettled;
      private boolean mHasFastScrollTouchSettledAtLeastOnce;
  
      // Set of all views animated during fast scroll.  We keep track of these ourselves since there
      // is no way to reset a view once it gets scrapped or recycled without other hacks
      private HashSet<RecyclerView.ViewHolder> mTrackedFastScrollViews = new HashSet<>();
  
      // Smooth fast-scroll animation frames
      @Thunk int mFastScrollFrameIndex;
      @Thunk final int[] mFastScrollFrames = new int[10];
  
      /**
       * This runnable runs a single frame of the smooth scroll animation and posts the next frame
       * if necessary.
       */
      @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
          @Override
          public void run() {
              if (mFastScrollFrameIndex < mFastScrollFrames.length) {
                  mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
                  mFastScrollFrameIndex++;
                  mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
              }
          }
      };
  
      /**
       * This runnable updates the current fast scroll section to the target fastscroll section.
       */
      Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
          @Override
          public void run() {
              // Update to the target section
              mCurrentFastScrollSection = mTargetFastScrollSection;
              mHasFastScrollTouchSettled = true;
              mHasFastScrollTouchSettledAtLeastOnce = true;
              updateTrackedViewsFastScrollFocusState();
          }
      };
  
      public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
          mRv = rv;
          mApps = apps;
      }
  
      public void onSetAdapter(AllAppsGridAdapter adapter) {
          adapter.setBindViewCallback(this);
      }
  
      /**
       * Smooth scrolls the recycler view to the given section.
       *
       * @return whether the fastscroller can scroll to the new section.
       */
      public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
              AlphabeticalAppsList.FastScrollSectionInfo info) {
          if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
              mTargetFastScrollPosition = info.fastScrollToItem.position;
              smoothSnapToPosition(scrollY, availableScrollHeight, info);
              return true;
          }
          return false;
      }
  
      /**
       * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
       * ourselves and animating the scroll on the recycler view.
       */
      private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
              AlphabeticalAppsList.FastScrollSectionInfo info) {
          mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
          mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
  
          trackAllChildViews();
          if (mHasFastScrollTouchSettled) {
              // In this case, the user has already settled once (and the fast scroll state has
              // animated) and they are just fine-tuning their section from the last section, so
              // we should make it feel fast and update immediately.
              mCurrentFastScrollSection = info.sectionName;
              mTargetFastScrollSection = null;
              updateTrackedViewsFastScrollFocusState();
          } else {
              // Otherwise, the user has scrubbed really far, and we don't want to distract the user
              // with the flashing fast scroll state change animation in addition to the fast scroll
              // section popup, so reset the views to normal, and wait for the touch to settle again
              // before animating the fast scroll state.
              mCurrentFastScrollSection = null;
              mTargetFastScrollSection = info.sectionName;
              mHasFastScrollTouchSettled = false;
              updateTrackedViewsFastScrollFocusState();
  
              // Delay scrolling to a new section until after some duration.  If the user has been
              // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
              // fast scroll to settle so it doesn't feel so long.
              mRv.postDelayed(mFastScrollToTargetSectionRunnable,
                      mHasFastScrollTouchSettledAtLeastOnce ?
                              REPEAT_TOUCH_SETTLING_DURATION :
                              INITIAL_TOUCH_SETTLING_DURATION);
          }
  
          // Calculate the full animation from the current scroll position to the final scroll
          // position, and then run the animation for the duration.  If we are scrolling to the
          // first fast scroll section, then just scroll to the top of the list itself.
          List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                  mApps.getFastScrollerSections();
          int newPosition = info.fastScrollToItem.position;
          int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
                          ? 0
                          : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
          int numFrames = mFastScrollFrames.length;
          int deltaY = newScrollY - scrollY;
          float ySign = Math.signum(deltaY);
          int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
          for (int i = 0; i < numFrames; i++) {
              // TODO(winsonc): We can interpolate this as well.
              mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
              deltaY -= step;
          }
          mFastScrollFrameIndex = 0;
          mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
      }
  
      public void onFastScrollCompleted() {
          // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
          //                runs
  
          // Stop animating the fast scroll position and state
          mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
          mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
  
          // Reset the tracking variables
          mHasFastScrollTouchSettled = false;
          mHasFastScrollTouchSettledAtLeastOnce = false;
          mCurrentFastScrollSection = null;
          mTargetFastScrollSection = null;
          mTargetFastScrollPosition = -1;
  
          updateTrackedViewsFastScrollFocusState();
          mTrackedFastScrollViews.clear();
      }
  
      @Override
      public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
          // Update newly bound views to the current fast scroll state if we are fast scrolling
          if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
              mTrackedFastScrollViews.add(holder);
          }
      }
  
      /**
       * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
       */
      private void trackAllChildViews() {
          int childCount = mRv.getChildCount();
          for (int i = 0; i < childCount; i++) {
              RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder(mRv.getChildAt(i));
              if (viewHolder != null) {
                  mTrackedFastScrollViews.add(viewHolder);
              }
          }
      }
  
      /**
       * Updates the fast scroll focus on all the children.
       */
      private void updateTrackedViewsFastScrollFocusState() {
          for (RecyclerView.ViewHolder viewHolder : mTrackedFastScrollViews) {
              int pos = viewHolder.getAdapterPosition();
              boolean isActive = false;
              if (mCurrentFastScrollSection != null
                      && pos > RecyclerView.NO_POSITION
                      && pos < mApps.getAdapterItems().size()) {
                  AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
                  isActive = item != null &&
                          mCurrentFastScrollSection.equals(item.sectionName) &&
                          item.position == mTargetFastScrollPosition;
              }
              viewHolder.itemView.setActivated(isActive);
          }
      }
  }
  