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 import android.view.View;
     20 
     21 import com.android.launcher3.BaseRecyclerView;
     22 import com.android.launcher3.BaseRecyclerViewFastScrollBar;
     23 import com.android.launcher3.FastBitmapDrawable;
     24 import com.android.launcher3.util.Thunk;
     25 
     26 import java.util.HashSet;
     27 
     28 public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
     29 
     30     private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
     31     private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
     32     private static final float FAST_SCROLL_TOUCH_VELOCITY_BARRIER = 1900f;
     33 
     34     private AllAppsRecyclerView mRv;
     35     private AlphabeticalAppsList mApps;
     36 
     37     // Keeps track of the current and targetted fast scroll section (the section to scroll to after
     38     // the initial delay)
     39     int mTargetFastScrollPosition = -1;
     40     @Thunk String mCurrentFastScrollSection;
     41     @Thunk String mTargetFastScrollSection;
     42 
     43     // The settled states affect the delay before the fast scroll animation is applied
     44     private boolean mHasFastScrollTouchSettled;
     45     private boolean mHasFastScrollTouchSettledAtLeastOnce;
     46 
     47     // Set of all views animated during fast scroll.  We keep track of these ourselves since there
     48     // is no way to reset a view once it gets scrapped or recycled without other hacks
     49     private HashSet<BaseRecyclerViewFastScrollBar.FastScrollFocusableView> mTrackedFastScrollViews =
     50             new HashSet<>();
     51 
     52     // Smooth fast-scroll animation frames
     53     @Thunk int mFastScrollFrameIndex;
     54     @Thunk final int[] mFastScrollFrames = new int[10];
     55 
     56     /**
     57      * This runnable runs a single frame of the smooth scroll animation and posts the next frame
     58      * if necessary.
     59      */
     60     @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
     61         @Override
     62         public void run() {
     63             if (mFastScrollFrameIndex < mFastScrollFrames.length) {
     64                 mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
     65                 mFastScrollFrameIndex++;
     66                 mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
     67             }
     68         }
     69     };
     70 
     71     /**
     72      * This runnable updates the current fast scroll section to the target fastscroll section.
     73      */
     74     Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
     75         @Override
     76         public void run() {
     77             // Update to the target section
     78             mCurrentFastScrollSection = mTargetFastScrollSection;
     79             mHasFastScrollTouchSettled = true;
     80             mHasFastScrollTouchSettledAtLeastOnce = true;
     81             updateTrackedViewsFastScrollFocusState();
     82         }
     83     };
     84 
     85     public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
     86         mRv = rv;
     87         mApps = apps;
     88     }
     89 
     90     public void onSetAdapter(AllAppsGridAdapter adapter) {
     91         adapter.setBindViewCallback(this);
     92     }
     93 
     94     /**
     95      * Smooth scrolls the recycler view to the given section.
     96      *
     97      * @return whether the fastscroller can scroll to the new section.
     98      */
     99     public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
    100             AlphabeticalAppsList.FastScrollSectionInfo info) {
    101         if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
    102             mTargetFastScrollPosition = info.fastScrollToItem.position;
    103             smoothSnapToPosition(scrollY, availableScrollHeight, info);
    104             return true;
    105         }
    106         return false;
    107     }
    108 
    109     /**
    110      * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
    111      * ourselves and animating the scroll on the recycler view.
    112      */
    113     private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
    114             AlphabeticalAppsList.FastScrollSectionInfo info) {
    115         mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
    116         mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
    117 
    118         trackAllChildViews();
    119         if (mHasFastScrollTouchSettled) {
    120             // In this case, the user has already settled once (and the fast scroll state has
    121             // animated) and they are just fine-tuning their section from the last section, so
    122             // we should make it feel fast and update immediately.
    123             mCurrentFastScrollSection = info.sectionName;
    124             mTargetFastScrollSection = null;
    125             updateTrackedViewsFastScrollFocusState();
    126         } else {
    127             // Otherwise, the user has scrubbed really far, and we don't want to distract the user
    128             // with the flashing fast scroll state change animation in addition to the fast scroll
    129             // section popup, so reset the views to normal, and wait for the touch to settle again
    130             // before animating the fast scroll state.
    131             mCurrentFastScrollSection = null;
    132             mTargetFastScrollSection = info.sectionName;
    133             mHasFastScrollTouchSettled = false;
    134             updateTrackedViewsFastScrollFocusState();
    135 
    136             // Delay scrolling to a new section until after some duration.  If the user has been
    137             // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
    138             // fast scroll to settle so it doesn't feel so long.
    139             mRv.postDelayed(mFastScrollToTargetSectionRunnable,
    140                     mHasFastScrollTouchSettledAtLeastOnce ?
    141                             REPEAT_TOUCH_SETTLING_DURATION :
    142                             INITIAL_TOUCH_SETTLING_DURATION);
    143         }
    144 
    145         // Calculate the full animation from the current scroll position to the final scroll
    146         // position, and then run the animation for the duration.
    147         int newScrollY = Math.min(availableScrollHeight,
    148                 mRv.getPaddingTop() + mRv.getTop(info.fastScrollToItem.rowIndex));
    149         int numFrames = mFastScrollFrames.length;
    150         for (int i = 0; i < numFrames; i++) {
    151             // TODO(winsonc): We can interpolate this as well.
    152             mFastScrollFrames[i] = (newScrollY - scrollY) / numFrames;
    153         }
    154         mFastScrollFrameIndex = 0;
    155         mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
    156     }
    157 
    158     public void onFastScrollCompleted() {
    159         // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
    160         //                runs
    161 
    162         // Stop animating the fast scroll position and state
    163         mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
    164         mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
    165 
    166         // Reset the tracking variables
    167         mHasFastScrollTouchSettled = false;
    168         mHasFastScrollTouchSettledAtLeastOnce = false;
    169         mCurrentFastScrollSection = null;
    170         mTargetFastScrollSection = null;
    171         mTargetFastScrollPosition = -1;
    172 
    173         updateTrackedViewsFastScrollFocusState();
    174         mTrackedFastScrollViews.clear();
    175     }
    176 
    177     @Override
    178     public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
    179         // Update newly bound views to the current fast scroll state if we are fast scrolling
    180         if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
    181             if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
    182                 BaseRecyclerViewFastScrollBar.FastScrollFocusableView v =
    183                         (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent;
    184                 updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */);
    185                 mTrackedFastScrollViews.add(v);
    186             }
    187         }
    188     }
    189 
    190     /**
    191      * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
    192      */
    193     private void trackAllChildViews() {
    194         int childCount = mRv.getChildCount();
    195         for (int i = 0; i < childCount; i++) {
    196             View v = mRv.getChildAt(i);
    197             if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
    198                 mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v);
    199             }
    200         }
    201     }
    202 
    203     /**
    204      * Updates the fast scroll focus on all the children.
    205      */
    206     private void updateTrackedViewsFastScrollFocusState() {
    207         for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) {
    208             RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v);
    209             int pos = (viewHolder != null) ? viewHolder.getPosition() : -1;
    210             updateViewFastScrollFocusState(v, pos, true);
    211         }
    212     }
    213 
    214     /**
    215      * Updates the fast scroll focus on all a given view.
    216      */
    217     private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v,
    218                                                 int pos, boolean animated) {
    219         FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL;
    220         if (mCurrentFastScrollSection != null && pos > -1) {
    221             AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
    222             boolean highlight = item.sectionName.equals(mCurrentFastScrollSection) &&
    223                     item.position == mTargetFastScrollPosition;
    224             newState = highlight ?
    225                     FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED :
    226                     FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED;
    227         }
    228         v.setFastScrollFocusState(newState, animated);
    229     }
    230 }
    231