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 AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps)85 public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) { 86 mRv = rv; 87 mApps = apps; 88 } 89 onSetAdapter(AllAppsGridAdapter adapter)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 */ smoothScrollToSection(int scrollY, int availableScrollHeight, AlphabeticalAppsList.FastScrollSectionInfo info)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 */ smoothSnapToPosition(int scrollY, int availableScrollHeight, AlphabeticalAppsList.FastScrollSectionInfo info)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 onFastScrollCompleted()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 onBindView(AllAppsGridAdapter.ViewHolder holder)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 */ trackAllChildViews()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 */ updateTrackedViewsFastScrollFocusState()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 */ updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v, int pos, boolean animated)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