summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
blob: 76934af7ddf82822c6c372fd64144f3cb965df7a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.launcher3.allapps;

import android.support.v7.widget.RecyclerView;
import android.view.View;

import com.android.launcher3.BaseRecyclerViewFastScrollBar;
import com.android.launcher3.FastBitmapDrawable;
import com.android.launcher3.util.Thunk;

import java.util.HashSet;
import java.util.List;

public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {

    private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
    private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
    private static final float FAST_SCROLL_TOUCH_VELOCITY_BARRIER = 1900f;

    private AllAppsRecyclerView mRv;
    private AlphabeticalAppsList mApps;

    // Keeps track of the current and targetted fast scroll section (the section to scroll to after
    // the initial delay)
    int mTargetFastScrollPosition = -1;
    @Thunk String mCurrentFastScrollSection;
    @Thunk String mTargetFastScrollSection;

    // The settled states affect the delay before the fast scroll animation is applied
    private boolean mHasFastScrollTouchSettled;
    private boolean mHasFastScrollTouchSettledAtLeastOnce;

    // Set of all views animated during fast scroll.  We keep track of these ourselves since there
    // is no way to reset a view once it gets scrapped or recycled without other hacks
    private HashSet<BaseRecyclerViewFastScrollBar.FastScrollFocusableView> mTrackedFastScrollViews =
            new HashSet<>();

    // Smooth fast-scroll animation frames
    @Thunk int mFastScrollFrameIndex;
    @Thunk final int[] mFastScrollFrames = new int[10];

    /**
     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
     * if necessary.
     */
    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
        @Override
        public void run() {
            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
                mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
                mFastScrollFrameIndex++;
                mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
            }
        }
    };

    /**
     * This runnable updates the current fast scroll section to the target fastscroll section.
     */
    Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
        @Override
        public void run() {
            // Update to the target section
            mCurrentFastScrollSection = mTargetFastScrollSection;
            mHasFastScrollTouchSettled = true;
            mHasFastScrollTouchSettledAtLeastOnce = true;
            updateTrackedViewsFastScrollFocusState();
        }
    };

    public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
        mRv = rv;
        mApps = apps;
    }

    public void onSetAdapter(AllAppsGridAdapter adapter) {
        adapter.setBindViewCallback(this);
    }

    /**
     * Smooth scrolls the recycler view to the given section.
     *
     * @return whether the fastscroller can scroll to the new section.
     */
    public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
            AlphabeticalAppsList.FastScrollSectionInfo info) {
        if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
            mTargetFastScrollPosition = info.fastScrollToItem.position;
            smoothSnapToPosition(scrollY, availableScrollHeight, info);
            return true;
        }
        return false;
    }

    /**
     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
     * ourselves and animating the scroll on the recycler view.
     */
    private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
            AlphabeticalAppsList.FastScrollSectionInfo info) {
        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);

        trackAllChildViews();
        if (mHasFastScrollTouchSettled) {
            // In this case, the user has already settled once (and the fast scroll state has
            // animated) and they are just fine-tuning their section from the last section, so
            // we should make it feel fast and update immediately.
            mCurrentFastScrollSection = info.sectionName;
            mTargetFastScrollSection = null;
            updateTrackedViewsFastScrollFocusState();
        } else {
            // Otherwise, the user has scrubbed really far, and we don't want to distract the user
            // with the flashing fast scroll state change animation in addition to the fast scroll
            // section popup, so reset the views to normal, and wait for the touch to settle again
            // before animating the fast scroll state.
            mCurrentFastScrollSection = null;
            mTargetFastScrollSection = info.sectionName;
            mHasFastScrollTouchSettled = false;
            updateTrackedViewsFastScrollFocusState();

            // Delay scrolling to a new section until after some duration.  If the user has been
            // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
            // fast scroll to settle so it doesn't feel so long.
            mRv.postDelayed(mFastScrollToTargetSectionRunnable,
                    mHasFastScrollTouchSettledAtLeastOnce ?
                            REPEAT_TOUCH_SETTLING_DURATION :
                            INITIAL_TOUCH_SETTLING_DURATION);
        }

        // Calculate the full animation from the current scroll position to the final scroll
        // position, and then run the animation for the duration.  If we are scrolling to the
        // first fast scroll section, then just scroll to the top of the list itself.
        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                mApps.getFastScrollerSections();
        int newPosition = info.fastScrollToItem.position;
        int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
                        ? 0
                        : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
        int numFrames = mFastScrollFrames.length;
        int deltaY = newScrollY - scrollY;
        float ySign = Math.signum(deltaY);
        int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
        for (int i = 0; i < numFrames; i++) {
            // TODO(winsonc): We can interpolate this as well.
            mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
            deltaY -= step;
        }
        mFastScrollFrameIndex = 0;
        mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
    }

    public void onFastScrollCompleted() {
        // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
        //                runs

        // Stop animating the fast scroll position and state
        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);

        // Reset the tracking variables
        mHasFastScrollTouchSettled = false;
        mHasFastScrollTouchSettledAtLeastOnce = false;
        mCurrentFastScrollSection = null;
        mTargetFastScrollSection = null;
        mTargetFastScrollPosition = -1;

        updateTrackedViewsFastScrollFocusState();
        mTrackedFastScrollViews.clear();
    }

    @Override
    public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
        // Update newly bound views to the current fast scroll state if we are fast scrolling
        if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
            if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
                BaseRecyclerViewFastScrollBar.FastScrollFocusableView v =
                        (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent;
                updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */);
                mTrackedFastScrollViews.add(v);
            }
        }
    }

    /**
     * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
     */
    private void trackAllChildViews() {
        int childCount = mRv.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View v = mRv.getChildAt(i);
            if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
                mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v);
            }
        }
    }

    /**
     * Updates the fast scroll focus on all the children.
     */
    private void updateTrackedViewsFastScrollFocusState() {
        for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) {
            RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v);
            int pos = (viewHolder != null) ? viewHolder.getPosition() : -1;
            updateViewFastScrollFocusState(v, pos, true);
        }
    }

    /**
     * Updates the fast scroll focus on all a given view.
     */
    private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v,
                                                int pos, boolean animated) {
        FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL;
        if (mCurrentFastScrollSection != null && pos > -1) {
            AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
            boolean highlight = item.sectionName.equals(mCurrentFastScrollSection) &&
                    item.position == mTargetFastScrollPosition;
            newState = highlight ?
                    FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED :
                    FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED;
        }
        v.setFastScrollFocusState(newState, animated);
    }
}