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