Home | History | Annotate | Download | only in allapps
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.launcher3.allapps;
     17 
     18 import android.support.v7.widget.RecyclerView;
     19 
     20 import com.android.launcher3.util.Thunk;
     21 
     22 import java.util.HashSet;
     23 import java.util.List;
     24 
     25 public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
     26 
     27     private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
     28     private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
     29 
     30     private AllAppsRecyclerView mRv;
     31     private AlphabeticalAppsList mApps;
     32 
     33     // Keeps track of the current and targeted fast scroll section (the section to scroll to after
     34     // the initial delay)
     35     int mTargetFastScrollPosition = -1;
     36     @Thunk String mCurrentFastScrollSection;
     37     @Thunk String mTargetFastScrollSection;
     38 
     39     // The settled states affect the delay before the fast scroll animation is applied
     40     private boolean mHasFastScrollTouchSettled;
     41     private boolean mHasFastScrollTouchSettledAtLeastOnce;
     42 
     43     // Set of all views animated during fast scroll.  We keep track of these ourselves since there
     44     // is no way to reset a view once it gets scrapped or recycled without other hacks
     45     private HashSet<RecyclerView.ViewHolder> mTrackedFastScrollViews = new HashSet<>();
     46 
     47     // Smooth fast-scroll animation frames
     48     @Thunk int mFastScrollFrameIndex;
     49     @Thunk final int[] mFastScrollFrames = new int[10];
     50 
     51     /**
     52      * This runnable runs a single frame of the smooth scroll animation and posts the next frame
     53      * if necessary.
     54      */
     55     @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
     56         @Override
     57         public void run() {
     58             if (mFastScrollFrameIndex < mFastScrollFrames.length) {
     59                 mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
     60                 mFastScrollFrameIndex++;
     61                 mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
     62             }
     63         }
     64     };
     65 
     66     /**
     67      * This runnable updates the current fast scroll section to the target fastscroll section.
     68      */
     69     Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
     70         @Override
     71         public void run() {
     72             // Update to the target section
     73             mCurrentFastScrollSection = mTargetFastScrollSection;
     74             mHasFastScrollTouchSettled = true;
     75             mHasFastScrollTouchSettledAtLeastOnce = true;
     76             updateTrackedViewsFastScrollFocusState();
     77         }
     78     };
     79 
     80     public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
     81         mRv = rv;
     82         mApps = apps;
     83     }
     84 
     85     public void onSetAdapter(AllAppsGridAdapter adapter) {
     86         adapter.setBindViewCallback(this);
     87     }
     88 
     89     /**
     90      * Smooth scrolls the recycler view to the given section.
     91      *
     92      * @return whether the fastscroller can scroll to the new section.
     93      */
     94     public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
     95             AlphabeticalAppsList.FastScrollSectionInfo info) {
     96         if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
     97             mTargetFastScrollPosition = info.fastScrollToItem.position;
     98             smoothSnapToPosition(scrollY, availableScrollHeight, info);
     99             return true;
    100         }
    101         return false;
    102     }
    103 
    104     /**
    105      * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
    106      * ourselves and animating the scroll on the recycler view.
    107      */
    108     private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
    109             AlphabeticalAppsList.FastScrollSectionInfo info) {
    110         mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
    111         mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
    112 
    113         trackAllChildViews();
    114         if (mHasFastScrollTouchSettled) {
    115             // In this case, the user has already settled once (and the fast scroll state has
    116             // animated) and they are just fine-tuning their section from the last section, so
    117             // we should make it feel fast and update immediately.
    118             mCurrentFastScrollSection = info.sectionName;
    119             mTargetFastScrollSection = null;
    120             updateTrackedViewsFastScrollFocusState();
    121         } else {
    122             // Otherwise, the user has scrubbed really far, and we don't want to distract the user
    123             // with the flashing fast scroll state change animation in addition to the fast scroll
    124             // section popup, so reset the views to normal, and wait for the touch to settle again
    125             // before animating the fast scroll state.
    126             mCurrentFastScrollSection = null;
    127             mTargetFastScrollSection = info.sectionName;
    128             mHasFastScrollTouchSettled = false;
    129             updateTrackedViewsFastScrollFocusState();
    130 
    131             // Delay scrolling to a new section until after some duration.  If the user has been
    132             // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
    133             // fast scroll to settle so it doesn't feel so long.
    134             mRv.postDelayed(mFastScrollToTargetSectionRunnable,
    135                     mHasFastScrollTouchSettledAtLeastOnce ?
    136                             REPEAT_TOUCH_SETTLING_DURATION :
    137                             INITIAL_TOUCH_SETTLING_DURATION);
    138         }
    139 
    140         // Calculate the full animation from the current scroll position to the final scroll
    141         // position, and then run the animation for the duration.  If we are scrolling to the
    142         // first fast scroll section, then just scroll to the top of the list itself.
    143         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
    144                 mApps.getFastScrollerSections();
    145         int newPosition = info.fastScrollToItem.position;
    146         int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
    147                         ? 0
    148                         : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
    149         int numFrames = mFastScrollFrames.length;
    150         int deltaY = newScrollY - scrollY;
    151         float ySign = Math.signum(deltaY);
    152         int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
    153         for (int i = 0; i < numFrames; i++) {
    154             // TODO(winsonc): We can interpolate this as well.
    155             mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
    156             deltaY -= step;
    157         }
    158         mFastScrollFrameIndex = 0;
    159         mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
    160     }
    161 
    162     public void onFastScrollCompleted() {
    163         // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
    164         //                runs
    165 
    166         // Stop animating the fast scroll position and state
    167         mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
    168         mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
    169 
    170         // Reset the tracking variables
    171         mHasFastScrollTouchSettled = false;
    172         mHasFastScrollTouchSettledAtLeastOnce = false;
    173         mCurrentFastScrollSection = null;
    174         mTargetFastScrollSection = null;
    175         mTargetFastScrollPosition = -1;
    176 
    177         updateTrackedViewsFastScrollFocusState();
    178         mTrackedFastScrollViews.clear();
    179     }
    180 
    181     @Override
    182     public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
    183         // Update newly bound views to the current fast scroll state if we are fast scrolling
    184         if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
    185             mTrackedFastScrollViews.add(holder);
    186         }
    187     }
    188 
    189     /**
    190      * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
    191      */
    192     private void trackAllChildViews() {
    193         int childCount = mRv.getChildCount();
    194         for (int i = 0; i < childCount; i++) {
    195             RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder(mRv.getChildAt(i));
    196             if (viewHolder != null) {
    197                 mTrackedFastScrollViews.add(viewHolder);
    198             }
    199         }
    200     }
    201 
    202     /**
    203      * Updates the fast scroll focus on all the children.
    204      */
    205     private void updateTrackedViewsFastScrollFocusState() {
    206         for (RecyclerView.ViewHolder viewHolder : mTrackedFastScrollViews) {
    207             int pos = viewHolder.getAdapterPosition();
    208             boolean isActive = false;
    209             if (mCurrentFastScrollSection != null
    210                     && pos > RecyclerView.NO_POSITION
    211                     && pos < mApps.getAdapterItems().size()) {
    212                 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
    213                 isActive = item != null &&
    214                         mCurrentFastScrollSection.equals(item.sectionName) &&
    215                         item.position == mTargetFastScrollPosition;
    216             }
    217             viewHolder.itemView.setActivated(isActive);
    218         }
    219     }
    220 }
    221