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.FastBitmapDrawable; 23 import com.android.launcher3.util.Thunk; 24 25 import java.util.HashSet; 26 import java.util.List; 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. If we are scrolling to the 147 // first fast scroll section, then just scroll to the top of the list itself. 148 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = 149 mApps.getFastScrollerSections(); 150 int newPosition = info.fastScrollToItem.position; 151 int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info 152 ? 0 153 : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0)); 154 int numFrames = mFastScrollFrames.length; 155 int deltaY = newScrollY - scrollY; 156 float ySign = Math.signum(deltaY); 157 int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames)); 158 for (int i = 0; i < numFrames; i++) { 159 // TODO(winsonc): We can interpolate this as well. 160 mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY))); 161 deltaY -= step; 162 } 163 mFastScrollFrameIndex = 0; 164 mRv.postOnAnimation(mSmoothSnapNextFrameRunnable); 165 } 166 onFastScrollCompleted()167 public void onFastScrollCompleted() { 168 // TODO(winsonc): Handle the case when the user scrolls and releases before the animation 169 // runs 170 171 // Stop animating the fast scroll position and state 172 mRv.removeCallbacks(mSmoothSnapNextFrameRunnable); 173 mRv.removeCallbacks(mFastScrollToTargetSectionRunnable); 174 175 // Reset the tracking variables 176 mHasFastScrollTouchSettled = false; 177 mHasFastScrollTouchSettledAtLeastOnce = false; 178 mCurrentFastScrollSection = null; 179 mTargetFastScrollSection = null; 180 mTargetFastScrollPosition = -1; 181 182 updateTrackedViewsFastScrollFocusState(); 183 mTrackedFastScrollViews.clear(); 184 } 185 186 @Override onBindView(AllAppsGridAdapter.ViewHolder holder)187 public void onBindView(AllAppsGridAdapter.ViewHolder holder) { 188 // Update newly bound views to the current fast scroll state if we are fast scrolling 189 if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) { 190 if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { 191 BaseRecyclerViewFastScrollBar.FastScrollFocusableView v = 192 (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent; 193 updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */); 194 mTrackedFastScrollViews.add(v); 195 } 196 } 197 } 198 199 /** 200 * Starts tracking all the recycler view's children which are FastScrollFocusableViews. 201 */ trackAllChildViews()202 private void trackAllChildViews() { 203 int childCount = mRv.getChildCount(); 204 for (int i = 0; i < childCount; i++) { 205 View v = mRv.getChildAt(i); 206 if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { 207 mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v); 208 } 209 } 210 } 211 212 /** 213 * Updates the fast scroll focus on all the children. 214 */ updateTrackedViewsFastScrollFocusState()215 private void updateTrackedViewsFastScrollFocusState() { 216 for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) { 217 RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v); 218 int pos = (viewHolder != null) ? viewHolder.getPosition() : -1; 219 updateViewFastScrollFocusState(v, pos, true); 220 } 221 } 222 223 /** 224 * Updates the fast scroll focus on all a given view. 225 */ updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v, int pos, boolean animated)226 private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v, 227 int pos, boolean animated) { 228 FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL; 229 if (mCurrentFastScrollSection != null && pos > -1) { 230 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos); 231 boolean highlight = item.sectionName.equals(mCurrentFastScrollSection) && 232 item.position == mTargetFastScrollPosition; 233 newState = highlight ? 234 FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED : 235 FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED; 236 } 237 v.setFastScrollFocusState(newState, animated); 238 } 239 } 240