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