[go: nahoru, domu]

1/*
2 * Copyright (C) 2014 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
17package android.support.v7.widget;
18
19import android.content.Context;
20import android.graphics.PointF;
21import android.util.DisplayMetrics;
22import android.util.Log;
23import android.view.View;
24import android.view.animation.DecelerateInterpolator;
25import android.view.animation.LinearInterpolator;
26
27/**
28 * {@link RecyclerView.SmoothScroller} implementation which uses
29 * {@link android.view.animation.LinearInterpolator} until the target position becames a child of
30 * the RecyclerView and then uses
31 * {@link android.view.animation.DecelerateInterpolator} to slowly approach to target position.
32 */
33abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
34
35    private static final String TAG = "LinearSmoothScroller";
36
37    private static final boolean DEBUG = false;
38
39    private static final float MILLISECONDS_PER_INCH = 25f;
40
41    private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
42
43    /**
44     * Align child view's left or top with parent view's left or top
45     *
46     * @see #calculateDtToFit(int, int, int, int, int)
47     * @see #calculateDxToMakeVisible(android.view.View, int)
48     * @see #calculateDyToMakeVisible(android.view.View, int)
49     */
50    public static final int SNAP_TO_START = -1;
51
52    /**
53     * Align child view's right or bottom with parent view's right or bottom
54     *
55     * @see #calculateDtToFit(int, int, int, int, int)
56     * @see #calculateDxToMakeVisible(android.view.View, int)
57     * @see #calculateDyToMakeVisible(android.view.View, int)
58     */
59    public static final int SNAP_TO_END = 1;
60
61    /**
62     * <p>Decides if the child should be snapped from start or end, depending on where it
63     * currently is in relation to its parent.</p>
64     * <p>For instance, if the view is virtually on the left of RecyclerView, using
65     * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
66     *
67     * @see #calculateDtToFit(int, int, int, int, int)
68     * @see #calculateDxToMakeVisible(android.view.View, int)
69     * @see #calculateDyToMakeVisible(android.view.View, int)
70     */
71    public static final int SNAP_TO_ANY = 0;
72
73    // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
74    // view is not laid out until interim target position is reached, we can detect the case before
75    // scrolling slows down and reschedule another interim target scroll
76    private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
77
78    protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
79
80    protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
81
82    protected PointF mTargetVector;
83
84    private final float MILLISECONDS_PER_PX;
85
86    // Temporary variables to keep track of the interim scroll target. These values do not
87    // point to a real item position, rather point to an estimated location pixels.
88    protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
89
90    public LinearSmoothScroller(Context context) {
91        MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
92    }
93
94    /**
95     * {@inheritDoc}
96     */
97    @Override
98    protected void onStart() {
99
100    }
101
102    /**
103     * {@inheritDoc}
104     */
105    @Override
106    protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
107        final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
108        final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
109        final int distance = (int) Math.sqrt(dx * dx + dy * dy);
110        final int time = calculateTimeForDeceleration(distance);
111        if (time > 0) {
112            action.update(-dx, -dy, time, mDecelerateInterpolator);
113        }
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    @Override
120    protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
121        if (getChildCount() == 0) {
122            stop();
123            return;
124        }
125        //noinspection PointlessBooleanExpression
126        if (DEBUG && mTargetVector != null
127                && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
128            throw new IllegalStateException("Scroll happened in the opposite direction"
129                    + " of the target. Some calculations are wrong");
130        }
131        mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
132        mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
133
134        if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
135            updateActionForInterimTarget(action);
136        } // everything is valid, keep going
137
138    }
139
140    /**
141     * {@inheritDoc}
142     */
143    @Override
144    protected void onStop() {
145        mInterimTargetDx = mInterimTargetDy = 0;
146        mTargetVector = null;
147    }
148
149    /**
150     * Calculates the scroll speed.
151     *
152     * @param displayMetrics DisplayMetrics to be used for real dimension calculations
153     * @return The time (in ms) it should take for each pixel. For instance, if returned value is
154     * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
155     */
156    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
157        return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
158    }
159
160    /**
161     * <p>Calculates the time for deceleration so that transition from LinearInterpolator to
162     * DecelerateInterpolator looks smooth.</p>
163     *
164     * @param dx Distance to scroll
165     * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
166     * from LinearInterpolation
167     */
168    protected int calculateTimeForDeceleration(int dx) {
169        // we want to cover same area with the linear interpolator for the first 10% of the
170        // interpolation. After that, deceleration will take control.
171        // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
172        // which gives 0.100028 when x = .3356
173        // this is why we divide linear scrolling time with .3356
174        return  (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
175    }
176
177    /**
178     * Calculates the time it should take to scroll the given distance (in pixels)
179     *
180     * @param dx Distance in pixels that we want to scroll
181     * @return Time in milliseconds
182     * @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
183     */
184    protected int calculateTimeForScrolling(int dx) {
185        // In a case where dx is very small, rounding may return 0 although dx > 0.
186        // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
187        // time.
188        return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
189    }
190
191    /**
192     * When scrolling towards a child view, this method defines whether we should align the left
193     * or the right edge of the child with the parent RecyclerView.
194     *
195     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
196     * @see #SNAP_TO_START
197     * @see #SNAP_TO_END
198     * @see #SNAP_TO_ANY
199     */
200    protected int getHorizontalSnapPreference() {
201        return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
202                mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
203    }
204
205    /**
206     * When scrolling towards a child view, this method defines whether we should align the top
207     * or the bottom edge of the child with the parent RecyclerView.
208     *
209     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
210     * @see #SNAP_TO_START
211     * @see #SNAP_TO_END
212     * @see #SNAP_TO_ANY
213     */
214    protected int getVerticalSnapPreference() {
215        return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
216                mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
217    }
218
219    /**
220     * When the target scroll position is not a child of the RecyclerView, this method calculates
221     * a direction vector towards that child and triggers a smooth scroll.
222     *
223     * @see #computeScrollVectorForPosition(int)
224     */
225    protected void updateActionForInterimTarget(Action action) {
226        // find an interim target position
227        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
228        if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
229            Log.e(TAG, "To support smooth scrolling, you should override \n"
230                    + "LayoutManager#computeScrollVectorForPosition.\n"
231                    + "Falling back to instant scroll");
232            final int target = getTargetPosition();
233            action.jumpTo(target);
234            stop();
235            return;
236        }
237        normalize(scrollVector);
238        mTargetVector = scrollVector;
239
240        mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
241        mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
242        final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
243        // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
244        // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
245        // won't actually scroll more than what we need.
246        action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
247                , (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
248                , (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
249    }
250
251    private int clampApplyScroll(int tmpDt, int dt) {
252        final int before = tmpDt;
253        tmpDt -= dt;
254        if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
255            return 0;
256        }
257        return tmpDt;
258    }
259
260    /**
261     * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
262     * {@link #calculateDyToMakeVisible(android.view.View, int)}
263     */
264    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
265            snapPreference) {
266        switch (snapPreference) {
267            case SNAP_TO_START:
268                return boxStart - viewStart;
269            case SNAP_TO_END:
270                return boxEnd - viewEnd;
271            case SNAP_TO_ANY:
272                final int dtStart = boxStart - viewStart;
273                if (dtStart > 0) {
274                    return dtStart;
275                }
276                final int dtEnd = boxEnd - viewEnd;
277                if (dtEnd < 0) {
278                    return dtEnd;
279                }
280                break;
281            default:
282                throw new IllegalArgumentException("snap preference should be one of the"
283                        + " constants defined in SmoothScroller, starting with SNAP_");
284        }
285        return 0;
286    }
287
288    /**
289     * Calculates the vertical scroll amount necessary to make the given view fully visible
290     * inside the RecyclerView.
291     *
292     * @param view           The view which we want to make fully visible
293     * @param snapPreference The edge which the view should snap to when entering the visible
294     *                       area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
295     *                       {@link #SNAP_TO_ANY}.
296     * @return The vertical scroll amount necessary to make the view visible with the given
297     * snap preference.
298     */
299    public int calculateDyToMakeVisible(View view, int snapPreference) {
300        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
301        if (layoutManager == null || !layoutManager.canScrollVertically()) {
302            return 0;
303        }
304        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
305                view.getLayoutParams();
306        final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
307        final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
308        final int start = layoutManager.getPaddingTop();
309        final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
310        return calculateDtToFit(top, bottom, start, end, snapPreference);
311    }
312
313    /**
314     * Calculates the horizontal scroll amount necessary to make the given view fully visible
315     * inside the RecyclerView.
316     *
317     * @param view           The view which we want to make fully visible
318     * @param snapPreference The edge which the view should snap to when entering the visible
319     *                       area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
320     *                       {@link #SNAP_TO_END}
321     * @return The vertical scroll amount necessary to make the view visible with the given
322     * snap preference.
323     */
324    public int calculateDxToMakeVisible(View view, int snapPreference) {
325        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
326        if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
327            return 0;
328        }
329        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
330                view.getLayoutParams();
331        final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
332        final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
333        final int start = layoutManager.getPaddingLeft();
334        final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
335        return calculateDtToFit(left, right, start, end, snapPreference);
336    }
337
338    abstract public PointF computeScrollVectorForPosition(int targetPosition);
339}
340