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
17package android.support.wearable.view;
18
19import android.animation.Animator;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.annotation.TargetApi;
23import android.content.Context;
24import android.graphics.PointF;
25import android.os.Build;
26import android.os.Handler;
27import android.support.v7.widget.LinearSmoothScroller;
28import android.support.v7.widget.RecyclerView;
29import android.util.AttributeSet;
30import android.util.DisplayMetrics;
31import android.util.Log;
32import android.util.Property;
33import android.view.KeyEvent;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.ViewConfiguration;
37import android.view.ViewGroup;
38import android.widget.Scroller;
39
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * An alternative version of ListView that is optimized for ease of use on small screen wearable
45 * devices. It displays a vertically scrollable list of items, and automatically snaps to the
46 * nearest item when the user stops scrolling.
47 *
48 * <p>
49 * For a quick start, you will need to implement a subclass of {@link .Adapter},
50 * which will create and bind your views to the {@link .ViewHolder} objects. If you want to add
51 * more visual treatment to your views when they become the central items of the
52 * WearableListView, have them implement the {@link .OnCenterProximityListener} interface.
53 * </p>
54 */
55@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
56public class WearableListView extends RecyclerView {
57    @SuppressWarnings("unused")
58    private static final String TAG = "WearableListView";
59
60    private static final long FLIP_ANIMATION_DURATION_MS = 150;
61    private static final long CENTERING_ANIMATION_DURATION_MS = 150;
62
63    private static final float TOP_TAP_REGION_PERCENTAGE = .33f;
64    private static final float BOTTOM_TAP_REGION_PERCENTAGE = .33f;
65
66    // Each item will occupy one third of the height.
67    private static final int THIRD = 3;
68
69    private final int mMinFlingVelocity;
70    private final int mMaxFlingVelocity;
71
72    private boolean mMaximizeSingleItem;
73    private boolean mCanClick = true;
74    // WristGesture navigation signals are delivered as KeyEvents. Allow developer to disable them
75    // for this specific View. It might be cleaner to simply have users re-implement onKeyDown().
76    // TOOD: Finalize the disabling mechanism here.
77    private boolean mGestureNavigationEnabled = true;
78    private int mTapPositionX;
79    private int mTapPositionY;
80    private ClickListener mClickListener;
81
82    private Animator mScrollAnimator;
83    // This is a little hacky due to the fact that animator provides incremental values instead of
84    // deltas and scrolling code requires deltas. We animate WearableListView directly and use this
85    // field to calculate deltas. Obviously this means that only one scrolling algorithm can run at
86    // a time, but I don't think it would be wise to have more than one running.
87    private int mLastScrollChange;
88
89    private SetScrollVerticallyProperty mSetScrollVerticallyProperty =
90            new SetScrollVerticallyProperty();
91
92    private final List<OnScrollListener> mOnScrollListeners = new ArrayList<OnScrollListener>();
93
94    private final List<OnCentralPositionChangedListener> mOnCentralPositionChangedListeners =
95            new ArrayList<OnCentralPositionChangedListener>();
96
97    private OnOverScrollListener mOverScrollListener;
98
99    private boolean mGreedyTouchMode;
100
101    private float mStartX;
102
103    private float mStartY;
104
105    private float mStartFirstTop;
106
107    private final int mTouchSlop;
108
109    private boolean mPossibleVerticalSwipe;
110
111    private int mInitialOffset = 0;
112
113    private Scroller mScroller;
114
115    // Top and bottom boundaries for tap checking.  Need to recompute by calling computeTapRegions
116    // before referencing.
117    private final float[] mTapRegions = new float[2];
118
119    private boolean mGestureDirectionLocked;
120    private int mPreviousCentral = 0;
121
122    // Temp variable for storing locations on screen.
123    private final int[] mLocation = new int[2];
124
125    // TODO: Consider clearing this when underlying data set changes. If the data set changes, you
126    // can't safely assume that this pressed view is in the same place as it was before and it will
127    // receive setPressed(false) unnecessarily. In theory it should be fine, but in practice we
128    // have places like this: mIconView.setCircleColor(pressed ? mPressedColor : mSelectedColor);
129    // This might set selected color on non selected item. Our logic should be: if you change
130    // underlying data set, all best are off and you need to preserve the state; we will clear
131    // this field. However, I am not willing to introduce this so late in C development.
132    private View mPressedView = null;
133
134    private final Runnable mPressedRunnable = new Runnable() {
135        @Override
136        public void run() {
137            if (getChildCount() > 0) {
138                mPressedView = getChildAt(findCenterViewIndex());
139                mPressedView.setPressed(true);
140            } else {
141                Log.w(TAG, "mPressedRunnable: the children were removed, skipping.");
142            }
143        }
144    };
145
146    private final Runnable mReleasedRunnable = new Runnable() {
147        @Override
148        public void run() {
149            releasePressedItem();
150        }
151    };
152
153    private Runnable mNotifyChildrenPostLayoutRunnable = new Runnable() {
154        @Override
155        public void run() {
156            notifyChildrenAboutProximity(false);
157        }
158    };
159
160    private final AdapterDataObserver mObserver = new AdapterDataObserver() {
161        @Override
162        public void onChanged() {
163            WearableListView.this.addOnLayoutChangeListener(new OnLayoutChangeListener() {
164                @Override
165                public void onLayoutChange(View v, int left, int top, int right, int bottom,
166                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
167                    WearableListView.this.removeOnLayoutChangeListener(this);
168                    if (WearableListView.this.getChildCount() > 0) {
169                        WearableListView.this.animateToCenter();
170                    }
171                }
172            });
173        }
174    };
175
176    public WearableListView(Context context) {
177        this(context, null);
178    }
179
180    public WearableListView(Context context, AttributeSet attrs) {
181        this(context, attrs, 0);
182    }
183
184    public WearableListView(Context context, AttributeSet attrs, int defStyleAttr) {
185        super(context, attrs, defStyleAttr);
186        setHasFixedSize(true);
187        setOverScrollMode(View.OVER_SCROLL_NEVER);
188        setLayoutManager(new LayoutManager());
189
190        final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
191            @Override
192            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
193                if (newState == RecyclerView.SCROLL_STATE_IDLE && getChildCount() > 0) {
194                    handleTouchUp(null, newState);
195                }
196                for (OnScrollListener listener : mOnScrollListeners) {
197                    listener.onScrollStateChanged(newState);
198                }
199            }
200
201            @Override
202            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
203                onScroll(dy);
204            }
205        };
206        setOnScrollListener(onScrollListener);
207
208        final ViewConfiguration vc = ViewConfiguration.get(context);
209        mTouchSlop = vc.getScaledTouchSlop();
210
211        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
212        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
213    }
214
215    @Override
216    public void setAdapter(RecyclerView.Adapter adapter) {
217        RecyclerView.Adapter currentAdapter = getAdapter();
218        if (currentAdapter != null) {
219            currentAdapter.unregisterAdapterDataObserver(mObserver);
220        }
221
222        super.setAdapter(adapter);
223
224        if (adapter != null) {
225            adapter.registerAdapterDataObserver(mObserver);
226        }
227    }
228
229    /**
230     * @return the position of the center child's baseline; -1 if no center child exists or if
231     *      the center child does not return a valid baseline.
232     */
233    @Override
234    public int getBaseline() {
235        // No children implies there is no center child for which a baseline can be computed.
236        if (getChildCount() == 0) {
237            return super.getBaseline();
238        }
239
240        // Compute the baseline of the center child.
241        final int centerChildIndex = findCenterViewIndex();
242        final int centerChildBaseline = getChildAt(centerChildIndex).getBaseline();
243
244        // If the center child has no baseline, neither does this list view.
245        if (centerChildBaseline == -1) {
246            return super.getBaseline();
247        }
248
249        return getCentralViewTop() + centerChildBaseline;
250    }
251
252    /**
253     * @return true if the list is scrolled all the way to the top.
254     */
255    public boolean isAtTop() {
256        if (getChildCount() == 0) {
257            return true;
258        }
259
260        int centerChildIndex = findCenterViewIndex();
261        View centerView = getChildAt(centerChildIndex);
262        return getChildAdapterPosition(centerView) == 0 &&
263                getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
264    }
265
266    /**
267     * Clears the state of the layout manager that positions list items.
268     */
269    public void resetLayoutManager() {
270        setLayoutManager(new LayoutManager());
271    }
272
273    /**
274     * Controls whether WearableListView should intercept all touch events and also prevent the
275     * parent from receiving them.
276     * @param greedy If true it will intercept all touch events.
277     */
278    public void setGreedyTouchMode(boolean greedy) {
279        mGreedyTouchMode = greedy;
280    }
281
282    /**
283     * By default the first element of the list is initially positioned in the center of the screen.
284     * This method allows the developer to specify a different offset, e.g. to hide the
285     * WearableListView before the user is allowed to use it.
286     *
287     * @param top How far the elements should be pushed down.
288     */
289    public void setInitialOffset(int top) {
290        mInitialOffset = top;
291    }
292
293    @Override
294    public boolean onInterceptTouchEvent(MotionEvent event) {
295        if (!isEnabled()) {
296            return false;
297        }
298
299        if (mGreedyTouchMode && getChildCount() > 0) {
300            int action = event.getActionMasked();
301            if (action == MotionEvent.ACTION_DOWN) {
302                mStartX = event.getX();
303                mStartY = event.getY();
304                mStartFirstTop = getChildCount() > 0 ? getChildAt(0).getTop() : 0;
305                mPossibleVerticalSwipe = true;
306                mGestureDirectionLocked = false;
307            } else if (action == MotionEvent.ACTION_MOVE && mPossibleVerticalSwipe) {
308                handlePossibleVerticalSwipe(event);
309            }
310            getParent().requestDisallowInterceptTouchEvent(mPossibleVerticalSwipe);
311        }
312        return super.onInterceptTouchEvent(event);
313    }
314
315    private boolean handlePossibleVerticalSwipe(MotionEvent event) {
316        if (mGestureDirectionLocked) {
317            return mPossibleVerticalSwipe;
318        }
319        float deltaX = Math.abs(mStartX - event.getX());
320        float deltaY = Math.abs(mStartY - event.getY());
321        float distance = (deltaX * deltaX) + (deltaY * deltaY);
322        // Verify that the distance moved in the combined x/y direction is at
323        // least touch slop before determining the gesture direction.
324        if (distance > (mTouchSlop * mTouchSlop)) {
325            if (deltaX > deltaY) {
326                mPossibleVerticalSwipe = false;
327            }
328            mGestureDirectionLocked = true;
329        }
330        return mPossibleVerticalSwipe;
331    }
332
333    @Override
334    public boolean onTouchEvent(MotionEvent event) {
335        if (!isEnabled()) {
336            return false;
337        }
338
339        // super.onTouchEvent can change the state of the scroll, keep a copy so that handleTouchUp
340        // can exit early if scrollState != IDLE when the touch event started.
341        int scrollState = getScrollState();
342        boolean result = super.onTouchEvent(event);
343        if (getChildCount() > 0) {
344            int action = event.getActionMasked();
345            if (action == MotionEvent.ACTION_DOWN) {
346                handleTouchDown(event);
347            } else if (action == MotionEvent.ACTION_UP) {
348                handleTouchUp(event, scrollState);
349                getParent().requestDisallowInterceptTouchEvent(false);
350            } else if (action == MotionEvent.ACTION_MOVE) {
351                if (Math.abs(mTapPositionX - (int) event.getX()) >= mTouchSlop ||
352                        Math.abs(mTapPositionY - (int) event.getY()) >= mTouchSlop) {
353                    releasePressedItem();
354                    mCanClick = false;
355                }
356                result |= handlePossibleVerticalSwipe(event);
357                getParent().requestDisallowInterceptTouchEvent(mPossibleVerticalSwipe);
358            } else if (action == MotionEvent.ACTION_CANCEL) {
359                getParent().requestDisallowInterceptTouchEvent(false);
360                mCanClick = true;
361            }
362        }
363        return result;
364    }
365
366    private void releasePressedItem() {
367        if (mPressedView != null) {
368            mPressedView.setPressed(false);
369            mPressedView = null;
370        }
371        Handler handler = getHandler();
372        if (handler != null) {
373            handler.removeCallbacks(mPressedRunnable);
374        }
375    }
376
377    private void onScroll(int dy) {
378        for (OnScrollListener listener : mOnScrollListeners) {
379            listener.onScroll(dy);
380        }
381        notifyChildrenAboutProximity(true);
382    }
383
384    /**
385     * Adds a listener that will be called when the content of the list view is scrolled.
386     */
387    public void addOnScrollListener(OnScrollListener listener) {
388        mOnScrollListeners.add(listener);
389    }
390
391    /**
392     * Removes listener for scroll events.
393     */
394    public void removeOnScrollListener(OnScrollListener listener) {
395        mOnScrollListeners.remove(listener);
396    }
397
398    /**
399     * Adds a listener that will be called when the central item of the list changes.
400     */
401    public void addOnCentralPositionChangedListener(OnCentralPositionChangedListener listener) {
402        mOnCentralPositionChangedListeners.add(listener);
403    }
404
405    /**
406     * Removes a listener that would be called when the central item of the list changes.
407     */
408    public void removeOnCentralPositionChangedListener(OnCentralPositionChangedListener listener) {
409        mOnCentralPositionChangedListeners.remove(listener);
410    }
411
412    /**
413     * Determines if navigation of list with wrist gestures is enabled.
414     */
415    public boolean isGestureNavigationEnabled() {
416        return mGestureNavigationEnabled;
417    }
418
419    /**
420     * Sets whether navigation of list with wrist gestures is enabled.
421     */
422    public void setEnableGestureNavigation(boolean enabled) {
423        mGestureNavigationEnabled = enabled;
424    }
425
426    @Override /* KeyEvent.Callback */
427    public boolean onKeyDown(int keyCode, KeyEvent event) {
428        // Respond to keycodes (at least originally generated and injected by wrist gestures).
429        if (mGestureNavigationEnabled) {
430            switch (keyCode) {
431                case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS:
432                    fling(0, -mMinFlingVelocity);
433                    return true;
434                case KeyEvent.KEYCODE_NAVIGATE_NEXT:
435                    fling(0, mMinFlingVelocity);
436                    return true;
437                case KeyEvent.KEYCODE_NAVIGATE_IN:
438                    return tapCenterView();
439                case KeyEvent.KEYCODE_NAVIGATE_OUT:
440                    // Returing false leaves the action to the container of this WearableListView
441                    // (e.g. finishing the activity containing this WearableListView).
442                    return false;
443            }
444        }
445        return super.onKeyDown(keyCode, event);
446    }
447
448    /**
449     * Simulate tapping the child view at the center of this list.
450     */
451    private boolean tapCenterView() {
452        if (!isEnabled() || getVisibility() != View.VISIBLE) {
453            return false;
454        }
455        int index = findCenterViewIndex();
456        View view = getChildAt(index);
457        ViewHolder holder = getChildViewHolder(view);
458        if (mClickListener != null) {
459            mClickListener.onClick(holder);
460            return true;
461        }
462        return false;
463    }
464
465    private boolean checkForTap(MotionEvent event) {
466        // No taps are accepted if this view is disabled.
467        if (!isEnabled()) {
468            return false;
469        }
470
471        float rawY = event.getRawY();
472        int index = findCenterViewIndex();
473        View view = getChildAt(index);
474        ViewHolder holder = getChildViewHolder(view);
475        computeTapRegions(mTapRegions);
476        if (rawY > mTapRegions[0] && rawY < mTapRegions[1]) {
477            if (mClickListener != null) {
478                mClickListener.onClick(holder);
479            }
480            return true;
481        }
482        if (index > 0 && rawY <= mTapRegions[0]) {
483            animateToMiddle(index - 1, index);
484            return true;
485        }
486        if (index < getChildCount() - 1 && rawY >= mTapRegions[1]) {
487            animateToMiddle(index + 1, index);
488            return true;
489        }
490        if (index == 0 && rawY <= mTapRegions[0] && mClickListener != null) {
491            // Special case: if the top third of the screen is empty and the touch event happens
492            // there, we don't want to immediately disallow the parent from using it. We tell
493            // parent to disallow intercept only after we locked a gesture. Before that he
494            // might do something with the action.
495            mClickListener.onTopEmptyRegionClick();
496            return true;
497        }
498        return false;
499    }
500
501    private void animateToMiddle(int newCenterIndex, int oldCenterIndex) {
502        if (newCenterIndex == oldCenterIndex) {
503            throw new IllegalArgumentException(
504                    "newCenterIndex must be different from oldCenterIndex");
505        }
506        List<Animator> animators = new ArrayList<Animator>();
507        View child = getChildAt(newCenterIndex);
508        int scrollToMiddle = getCentralViewTop() - child.getTop();
509        startScrollAnimation(animators, scrollToMiddle, FLIP_ANIMATION_DURATION_MS);
510    }
511
512    private void startScrollAnimation(List<Animator> animators, int scroll, long duration) {
513        startScrollAnimation(animators, scroll, duration, 0);
514    }
515
516    private void startScrollAnimation(List<Animator> animators, int scroll, long duration,
517            long  delay) {
518        startScrollAnimation(animators, scroll, duration, delay, null);
519    }
520
521    private void startScrollAnimation(
522            int scroll, long duration, long  delay, Animator.AnimatorListener listener) {
523        startScrollAnimation(null, scroll, duration, delay, listener);
524    }
525
526    private void startScrollAnimation(List<Animator> animators, int scroll, long duration,
527            long  delay, Animator.AnimatorListener listener) {
528        if (mScrollAnimator != null) {
529            mScrollAnimator.cancel();
530        }
531
532        mLastScrollChange = 0;
533        ObjectAnimator scrollAnimator = ObjectAnimator.ofInt(this, mSetScrollVerticallyProperty,
534                0, -scroll);
535
536        if (animators != null) {
537            animators.add(scrollAnimator);
538            AnimatorSet animatorSet = new AnimatorSet();
539            animatorSet.playTogether(animators);
540            mScrollAnimator = animatorSet;
541        } else {
542            mScrollAnimator = scrollAnimator;
543        }
544        mScrollAnimator.setDuration(duration);
545        if (listener != null) {
546            mScrollAnimator.addListener(listener);
547        }
548        if (delay > 0) {
549            mScrollAnimator.setStartDelay(delay);
550        }
551        mScrollAnimator.start();
552    }
553
554    @Override
555    public boolean fling(int velocityX, int velocityY) {
556        if (getChildCount() == 0) {
557            return false;
558        }
559        // If we are flinging towards empty space (before first element or after last), we reuse
560        // original flinging mechanism.
561        final int index = findCenterViewIndex();
562        final View child = getChildAt(index);
563        int currentPosition = getChildPosition(child);
564        if ((currentPosition == 0 && velocityY < 0) ||
565                (currentPosition == getAdapter().getItemCount() - 1 && velocityY > 0)) {
566            return super.fling(velocityX, velocityY);
567        }
568
569        if (Math.abs(velocityY) < mMinFlingVelocity) {
570            return false;
571        }
572        velocityY = Math.max(Math.min(velocityY, mMaxFlingVelocity), -mMaxFlingVelocity);
573
574        if (mScroller == null) {
575            mScroller = new Scroller(getContext(), null, true);
576        }
577        mScroller.fling(0, 0, 0, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE,
578                Integer.MIN_VALUE, Integer.MAX_VALUE);
579        int finalY = mScroller.getFinalY();
580        int delta = finalY / (getPaddingTop() + getAdjustedHeight() / 2);
581        if (delta == 0) {
582            // If the fling would not be enough to change position, we increase it to satisfy user's
583            // intent of switching current position.
584            delta = velocityY > 0 ? 1 : -1;
585        }
586        int finalPosition = Math.max(
587                0, Math.min(getAdapter().getItemCount() - 1, currentPosition + delta));
588        smoothScrollToPosition(finalPosition);
589        return true;
590    }
591
592    public void smoothScrollToPosition(int position, RecyclerView.SmoothScroller smoothScroller) {
593        LayoutManager layoutManager = (LayoutManager) getLayoutManager();
594        layoutManager.setCustomSmoothScroller(smoothScroller);
595        smoothScrollToPosition(position);
596        layoutManager.clearCustomSmoothScroller();
597    }
598
599    @Override
600    public ViewHolder getChildViewHolder(View child) {
601        return (ViewHolder) super.getChildViewHolder(child);
602    }
603
604    /**
605     * Adds a listener that will be called when the user taps on the WearableListView or its items.
606     */
607    public void setClickListener(ClickListener clickListener) {
608        mClickListener = clickListener;
609    }
610
611    /**
612     * Adds a listener that will be called when the user drags the top element below its allowed
613     * bottom position.
614     *
615     * @hide
616     */
617    public void setOverScrollListener(OnOverScrollListener listener) {
618        mOverScrollListener = listener;
619    }
620
621    private int findCenterViewIndex() {
622        // TODO(gruszczy): This could be easily optimized, so that we stop looking when we the
623        // distance starts growing again, instead of finding the closest. It would safe half of
624        // the loop.
625        int count = getChildCount();
626        int index = -1;
627        int closest = Integer.MAX_VALUE;
628        int centerY = getCenterYPos(this);
629        for (int i = 0; i < count; ++i) {
630            final View child = getChildAt(i);
631            int childCenterY = getTop() + getCenterYPos(child);
632            final int distance = Math.abs(centerY - childCenterY);
633            if (distance < closest) {
634                closest = distance;
635                index = i;
636            }
637        }
638        if (index == -1) {
639            throw new IllegalStateException("Can't find central view.");
640        }
641        return index;
642    }
643
644    private static int getCenterYPos(View v) {
645        return v.getTop() + v.getPaddingTop() + getAdjustedHeight(v) / 2;
646    }
647
648    private void handleTouchUp(MotionEvent event, int scrollState) {
649        if (mCanClick && event != null && checkForTap(event)) {
650            Handler handler = getHandler();
651            if (handler != null) {
652                handler.postDelayed(mReleasedRunnable, ViewConfiguration.getTapTimeout());
653            }
654            return;
655        }
656
657        if (scrollState != RecyclerView.SCROLL_STATE_IDLE) {
658            // We are flinging, so let's not start animations just yet. Instead we will start them
659            // when the fling finishes.
660            return;
661        }
662
663        if (isOverScrolling()) {
664            mOverScrollListener.onOverScroll();
665        } else {
666            animateToCenter();
667        }
668    }
669
670    private boolean isOverScrolling() {
671        return getChildCount() > 0
672                // If first view top was below the central top, it means it was never centered.
673                // Don't allow overscroll, otherwise a simple touch (instead of a drag) will be
674                // enough to trigger overscroll.
675                && mStartFirstTop <= getCentralViewTop()
676                && getChildAt(0).getTop() >= getTopViewMaxTop()
677                && mOverScrollListener != null;
678    }
679
680    private int getTopViewMaxTop() {
681        return getHeight() / 2;
682    }
683
684    private int getItemHeight() {
685        // Round up so that the screen is fully occupied by 3 items.
686        return getAdjustedHeight() / THIRD + 1;
687    }
688
689    /**
690     * Returns top of the central {@code View} in the list when such view is fully centered.
691     *
692     * This is a more or a less a static value that you can use to align other views with the
693     * central one.
694     */
695    public int getCentralViewTop() {
696        return getPaddingTop() + getItemHeight();
697    }
698
699    /**
700     * Automatically starts an animation that snaps the list to center on the element closest to the
701     * middle.
702     */
703    public void animateToCenter() {
704        final int index = findCenterViewIndex();
705        final View child = getChildAt(index);
706        final int scrollToMiddle = getCentralViewTop() - child.getTop();
707        startScrollAnimation(scrollToMiddle, CENTERING_ANIMATION_DURATION_MS, 0,
708                new SimpleAnimatorListener() {
709                    @Override
710                    public void onAnimationEnd(Animator animator) {
711                        if (!wasCanceled()) {
712                            mCanClick = true;
713                        }
714                    }
715                });
716    }
717
718    /**
719     * Animate the list so that the first view is back to its initial position.
720     * @param endAction Action to execute when the animation is done.
721     * @hide
722     */
723    public void animateToInitialPosition(final Runnable endAction) {
724        final View child = getChildAt(0);
725        final int scrollToMiddle = getCentralViewTop() + mInitialOffset - child.getTop();
726        startScrollAnimation(scrollToMiddle, CENTERING_ANIMATION_DURATION_MS, 0,
727                new SimpleAnimatorListener() {
728                    @Override
729                    public void onAnimationEnd(Animator animator) {
730                        if (endAction != null) {
731                            endAction.run();
732                        }
733                    }
734                });
735    }
736
737    private void handleTouchDown(MotionEvent event) {
738        if (mCanClick) {
739            mTapPositionX = (int) event.getX();
740            mTapPositionY = (int) event.getY();
741            float rawY = event.getRawY();
742            computeTapRegions(mTapRegions);
743            if (rawY > mTapRegions[0] && rawY < mTapRegions[1]) {
744                View view = getChildAt(findCenterViewIndex());
745                if (view instanceof OnCenterProximityListener) {
746                    Handler handler = getHandler();
747                    if (handler != null) {
748                        handler.removeCallbacks(mReleasedRunnable);
749                        handler.postDelayed(mPressedRunnable, ViewConfiguration.getTapTimeout());
750                    }
751                }
752            }
753        }
754    }
755
756    private void setScrollVertically(int scroll) {
757        scrollBy(0, scroll - mLastScrollChange);
758        mLastScrollChange = scroll;
759    }
760
761    private int getAdjustedHeight() {
762        return getAdjustedHeight(this);
763    }
764
765    private static int getAdjustedHeight(View v) {
766        return v.getHeight() - v.getPaddingBottom() - v.getPaddingTop();
767    }
768
769    private void computeTapRegions(float[] tapRegions) {
770        mLocation[0] = mLocation[1] = 0;
771        getLocationOnScreen(mLocation);
772        int mScreenTop = mLocation[1];
773        int height = getHeight();
774        tapRegions[0] = mScreenTop + height * TOP_TAP_REGION_PERCENTAGE;
775        tapRegions[1] = mScreenTop + height * (1 - BOTTOM_TAP_REGION_PERCENTAGE);
776    }
777
778    /**
779     * Determines if, when there is only one item in the WearableListView, that the single item
780     * is laid out so that it's height fills the entire WearableListView.
781     */
782    public boolean getMaximizeSingleItem() {
783        return mMaximizeSingleItem;
784    }
785
786    /**
787     * When set to true, if there is only one item in the WearableListView, it will fill the entire
788     * WearableListView. When set to false, the default behavior will be used and the single item
789     * will fill only a third of the screen.
790     */
791    public void setMaximizeSingleItem(boolean maximizeSingleItem) {
792        mMaximizeSingleItem = maximizeSingleItem;
793    }
794
795    private void notifyChildrenAboutProximity(boolean animate) {
796        LayoutManager layoutManager = (LayoutManager) getLayoutManager();
797        int count = layoutManager.getChildCount();
798
799        if (count == 0) {
800            return;
801        }
802
803        int index = layoutManager.findCenterViewIndex();
804        for (int i = 0; i < count; ++i) {
805            final View view = layoutManager.getChildAt(i);
806            ViewHolder holder = getChildViewHolder(view);
807            holder.onCenterProximity(i == index, animate);
808        }
809        final int position = getChildViewHolder(getChildAt(index)).getPosition();
810        if (position != mPreviousCentral) {
811            for (OnScrollListener listener : mOnScrollListeners) {
812                listener.onCentralPositionChanged(position);
813            }
814            for (OnCentralPositionChangedListener listener :
815                    mOnCentralPositionChangedListeners) {
816                listener.onCentralPositionChanged(position);
817            }
818            mPreviousCentral = position;
819        }
820    }
821
822    // TODO: Move this to a separate class, so it can't directly interact with the WearableListView.
823    private class LayoutManager extends RecyclerView.LayoutManager {
824        private int mFirstPosition;
825
826        private boolean mPushFirstHigher;
827
828        private int mAbsoluteScroll;
829
830        private boolean mUseOldViewTop = true;
831
832        private boolean mWasZoomedIn = false;
833
834        private RecyclerView.SmoothScroller mSmoothScroller;
835
836        private RecyclerView.SmoothScroller mDefaultSmoothScroller;
837
838        // We need to have another copy of the same method, because this one uses
839        // LayoutManager.getChildCount/getChildAt instead of View.getChildCount/getChildAt and
840        // they return different values.
841        private int findCenterViewIndex() {
842            // TODO(gruszczy): This could be easily optimized, so that we stop looking when we the
843            // distance starts growing again, instead of finding the closest. It would safe half of
844            // the loop.
845            int count = getChildCount();
846            int index = -1;
847            int closest = Integer.MAX_VALUE;
848            int centerY = getCenterYPos(WearableListView.this);
849            for (int i = 0; i < count; ++i) {
850                final View child = getLayoutManager().getChildAt(i);
851                int childCenterY = getTop() + getCenterYPos(child);
852                final int distance = Math.abs(centerY - childCenterY);
853                if (distance < closest) {
854                    closest = distance;
855                    index = i;
856                }
857            }
858            if (index == -1) {
859                throw new IllegalStateException("Can't find central view.");
860            }
861            return index;
862        }
863
864        @Override
865        public void onLayoutChildren(RecyclerView.Recycler recycler, State state) {
866            final int parentBottom = getHeight() - getPaddingBottom();
867            // By default we assume this is the first run and the first element will be centered
868            // with optional initial offset.
869            int oldTop = getCentralViewTop() + mInitialOffset;
870            // Here we handle any other situation where we relayout or we want to achieve a
871            // specific layout of children.
872            if (mUseOldViewTop && getChildCount() > 0) {
873                // We are performing a relayout after we already had some children, because e.g. the
874                // contents of an adapter has changed. First we want to check, if the central item
875                // from before the layout is still here, because we want to preserve it.
876                int index = findCenterViewIndex();
877                int position = getPosition(getChildAt(index));
878                if (position == NO_POSITION) {
879                    // Central item was removed. Let's find the first surviving item and use it
880                    // as an anchor.
881                    for (int i = 0, N = getChildCount(); index + i < N || index - i >= 0; ++i) {
882                        View child = getChildAt(index + i);
883                        if (child != null) {
884                            position = getPosition(child);
885                            if (position != NO_POSITION) {
886                                index = index + i;
887                                break;
888                            }
889                        }
890                        child = getChildAt(index - i);
891                        if (child != null) {
892                            position = getPosition(child);
893                            if (position != NO_POSITION) {
894                                index = index - i;
895                                break;
896                            }
897                        }
898                    }
899                }
900                if (position == NO_POSITION) {
901                    // None of the children survives the relayout, let's just use the top of the
902                    // first one.
903                    oldTop = getChildAt(0).getTop();
904                    int count = state.getItemCount();
905                    // Lets first make sure that the first position is not above the last element,
906                    // which can happen if elements were removed.
907                    while (mFirstPosition >= count && mFirstPosition > 0) {
908                        mFirstPosition--;
909                    }
910                } else {
911                    // Some of the children survived the relayout. We will keep it in its place,
912                    // but go through previous children and maybe add them.
913                    if (!mWasZoomedIn) {
914                        // If we were previously zoomed-in on a single item, ignore this and just
915                        // use the default value set above. Reasoning: if we are still zoomed-in,
916                        // oldTop will be ignored when laying out the single child element. If we
917                        // are no longer zoomed in, then we want to position items using the top
918                        // of the single item as if the single item was not zoomed in, which is
919                        // equal to the default value.
920                        oldTop = getChildAt(index).getTop();
921                    }
922                    while (oldTop > getPaddingTop() && position > 0) {
923                        position--;
924                        oldTop -= getItemHeight();
925                    }
926                    if (position == 0 && oldTop > getCentralViewTop()) {
927                        // We need to handle special case where the first, central item was removed
928                        // and now the first element is hanging below, instead of being nicely
929                        // centered.
930                        oldTop = getCentralViewTop();
931                    }
932                    mFirstPosition = position;
933                }
934            } else if (mPushFirstHigher) {
935                // We are trying to position elements ourselves, so we force position of the first
936                // one.
937                oldTop = getCentralViewTop() - getItemHeight();
938            }
939
940            performLayoutChildren(recycler, state, parentBottom, oldTop);
941
942            // Since the content might have changed, we need to adjust the absolute scroll in case
943            // some elements have disappeared or were added.
944            if (getChildCount() == 0) {
945                setAbsoluteScroll(0);
946            } else {
947                View child = getChildAt(findCenterViewIndex());
948                setAbsoluteScroll(child.getTop() - getCentralViewTop() + getPosition(child) *
949                        getItemHeight());
950            }
951
952            mUseOldViewTop = true;
953            mPushFirstHigher = false;
954        }
955
956        private void performLayoutChildren(Recycler recycler, State state, int parentBottom,
957                                           int top) {
958            detachAndScrapAttachedViews(recycler);
959
960            if (mMaximizeSingleItem && state.getItemCount() == 1) {
961                performLayoutOneChild(recycler, parentBottom);
962                mWasZoomedIn = true;
963            } else {
964                performLayoutMultipleChildren(recycler, state, parentBottom, top);
965                mWasZoomedIn = false;
966            }
967
968            if (getChildCount() > 0) {
969                post(mNotifyChildrenPostLayoutRunnable);
970            }
971        }
972
973        private void performLayoutOneChild(Recycler recycler, int parentBottom) {
974            final int right = getWidth() - getPaddingRight();
975            View v = recycler.getViewForPosition(getFirstPosition());
976            addView(v, 0);
977            measureZoomView(v);
978            v.layout(getPaddingLeft(), getPaddingTop(), right, parentBottom);
979        }
980
981        private void performLayoutMultipleChildren(Recycler recycler, State state, int parentBottom,
982                                                   int top) {
983            int bottom;
984            final int left = getPaddingLeft();
985            final int right = getWidth() - getPaddingRight();
986            final int count = state.getItemCount();
987            // If we are laying out children with center element being different than the first, we
988            // need to start with previous child which appears half visible at the top.
989            for (int i = 0; getFirstPosition() + i < count; i++, top = bottom) {
990                if (top >= parentBottom) {
991                    break;
992                }
993                View v = recycler.getViewForPosition(getFirstPosition() + i);
994                addView(v, i);
995                measureThirdView(v);
996                bottom = top + getItemHeight();
997                v.layout(left, top, right, bottom);
998            }
999        }
1000
1001        private void setAbsoluteScroll(int absoluteScroll) {
1002            mAbsoluteScroll = absoluteScroll;
1003            for (OnScrollListener listener : mOnScrollListeners) {
1004                listener.onAbsoluteScrollChange(mAbsoluteScroll);
1005            }
1006        }
1007
1008        private void measureView(View v, int height) {
1009            final LayoutParams lp = (LayoutParams) v.getLayoutParams();
1010            final int widthSpec = getChildMeasureSpec(getWidth(),
1011                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width,
1012                canScrollHorizontally());
1013            final int heightSpec = getChildMeasureSpec(getHeight(),
1014                getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin,
1015                height, canScrollVertically());
1016            v.measure(widthSpec, heightSpec);
1017        }
1018
1019        private void measureThirdView(View v) {
1020            measureView(v, (int) (1 + (float) getHeight() / THIRD));
1021        }
1022
1023        private void measureZoomView(View v) {
1024            measureView(v, getHeight());
1025        }
1026
1027        @Override
1028        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
1029            return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
1030                    ViewGroup.LayoutParams.WRAP_CONTENT);
1031        }
1032
1033        @Override
1034        public boolean canScrollVertically() {
1035            // Disable vertical scrolling when zoomed.
1036            return getItemCount() != 1 || !mWasZoomedIn;
1037        }
1038
1039        @Override
1040        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, State state) {
1041            // TODO(gruszczy): This code is shit, needs to be rewritten.
1042            if (getChildCount() == 0) {
1043                return 0;
1044            }
1045            int scrolled = 0;
1046            final int left = getPaddingLeft();
1047            final int right = getWidth() - getPaddingRight();
1048            if (dy < 0) {
1049                while (scrolled > dy) {
1050                    final View topView = getChildAt(0);
1051                    if (getFirstPosition() > 0) {
1052                        final int hangingTop = Math.max(-topView.getTop(), 0);
1053                        final int scrollBy = Math.min(scrolled - dy, hangingTop);
1054                        scrolled -= scrollBy;
1055                        offsetChildrenVertical(scrollBy);
1056                        if (getFirstPosition() > 0 && scrolled > dy) {
1057                            mFirstPosition--;
1058                            View v = recycler.getViewForPosition(getFirstPosition());
1059                            addView(v, 0);
1060                            measureThirdView(v);
1061                            final int bottom = topView.getTop();
1062                            final int top = bottom - getItemHeight();
1063                            v.layout(left, top, right, bottom);
1064                        } else {
1065                            break;
1066                        }
1067                    } else {
1068                        mPushFirstHigher = false;
1069                        int maxScroll = mOverScrollListener!= null ?
1070                                getHeight() : getTopViewMaxTop();
1071                        final int scrollBy = Math.min(-dy + scrolled, maxScroll - topView.getTop());
1072                        scrolled -= scrollBy;
1073                        offsetChildrenVertical(scrollBy);
1074                        break;
1075                    }
1076                }
1077            } else if (dy > 0) {
1078                final int parentHeight = getHeight();
1079                while (scrolled < dy) {
1080                    final View bottomView = getChildAt(getChildCount() - 1);
1081                    if (state.getItemCount() > mFirstPosition + getChildCount()) {
1082                        final int hangingBottom =
1083                                Math.max(bottomView.getBottom() - parentHeight, 0);
1084                        final int scrollBy = -Math.min(dy - scrolled, hangingBottom);
1085                        scrolled -= scrollBy;
1086                        offsetChildrenVertical(scrollBy);
1087                        if (scrolled < dy) {
1088                            View v = recycler.getViewForPosition(mFirstPosition + getChildCount());
1089                            final int top = getChildAt(getChildCount() - 1).getBottom();
1090                            addView(v);
1091                            measureThirdView(v);
1092                            final int bottom = top + getItemHeight();
1093                            v.layout(left, top, right, bottom);
1094                        } else {
1095                            break;
1096                        }
1097                    } else {
1098                        final int scrollBy =
1099                                Math.max(-dy + scrolled, getHeight() / 2 - bottomView.getBottom());
1100                        scrolled -= scrollBy;
1101                        offsetChildrenVertical(scrollBy);
1102                        break;
1103                    }
1104                }
1105            }
1106            recycleViewsOutOfBounds(recycler);
1107            setAbsoluteScroll(mAbsoluteScroll + scrolled);
1108            return scrolled;
1109        }
1110
1111        @Override
1112        public void scrollToPosition(int position) {
1113            mUseOldViewTop = false;
1114            if (position > 0) {
1115                mFirstPosition = position - 1;
1116                mPushFirstHigher = true;
1117            } else {
1118                mFirstPosition = position;
1119                mPushFirstHigher = false;
1120            }
1121            requestLayout();
1122        }
1123
1124        public void setCustomSmoothScroller(RecyclerView.SmoothScroller smoothScroller) {
1125            mSmoothScroller = smoothScroller;
1126        }
1127
1128        public void clearCustomSmoothScroller() {
1129            mSmoothScroller = null;
1130        }
1131
1132        public RecyclerView.SmoothScroller getDefaultSmoothScroller(RecyclerView recyclerView) {
1133            if (mDefaultSmoothScroller == null) {
1134                mDefaultSmoothScroller = new SmoothScroller(
1135                        recyclerView.getContext(), this);
1136            }
1137            return mDefaultSmoothScroller;
1138        }
1139        @Override
1140        public void smoothScrollToPosition(RecyclerView recyclerView, State state,
1141                int position) {
1142            RecyclerView.SmoothScroller scroller = mSmoothScroller;
1143            if (scroller == null) {
1144                scroller = getDefaultSmoothScroller(recyclerView);
1145            }
1146            scroller.setTargetPosition(position);
1147            startSmoothScroll(scroller);
1148        }
1149
1150        private void recycleViewsOutOfBounds(RecyclerView.Recycler recycler) {
1151            final int childCount = getChildCount();
1152            final int parentWidth = getWidth();
1153            // Here we want to use real height, so we don't remove views that are only visible in
1154            // padded section.
1155            final int parentHeight = getHeight();
1156            boolean foundFirst = false;
1157            int first = 0;
1158            int last = 0;
1159            for (int i = 0; i < childCount; i++) {
1160                final View v = getChildAt(i);
1161                if (v.hasFocus() || (v.getRight() >= 0 && v.getLeft() <= parentWidth &&
1162                        v.getBottom() >= 0 && v.getTop() <= parentHeight)) {
1163                    if (!foundFirst) {
1164                        first = i;
1165                        foundFirst = true;
1166                    }
1167                    last = i;
1168                }
1169            }
1170            for (int i = childCount - 1; i > last; i--) {
1171                removeAndRecycleViewAt(i, recycler);
1172            }
1173            for (int i = first - 1; i >= 0; i--) {
1174                removeAndRecycleViewAt(i, recycler);
1175            }
1176            if (getChildCount() == 0) {
1177                mFirstPosition = 0;
1178            } else if (first > 0) {
1179                mPushFirstHigher = true;
1180                mFirstPosition += first;
1181            }
1182        }
1183
1184        public int getFirstPosition() {
1185            return mFirstPosition;
1186        }
1187
1188        @Override
1189        public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
1190                RecyclerView.Adapter newAdapter) {
1191            removeAllViews();
1192        }
1193    }
1194
1195    /**
1196     * Interface for receiving callbacks when WearableListView children become or cease to be the
1197     * central item.
1198     */
1199    public interface OnCenterProximityListener {
1200        /**
1201         * Called when this view becomes central item of the WearableListView.
1202         *
1203         * @param animate Whether you should animate your transition of the View to become the
1204         *                central item. If false, this is the initial setting and you should
1205         *                transition immediately.
1206         */
1207        void onCenterPosition(boolean animate);
1208
1209        /**
1210         * Called when this view stops being the central item of the WearableListView.
1211         * @param animate Whether you should animate your transition of the View to being
1212         *                non central item. If false, this is the initial setting and you should
1213         *                transition immediately.
1214         */
1215        void onNonCenterPosition(boolean animate);
1216    }
1217
1218    /**
1219     * Interface for listening for click events on WearableListView.
1220     */
1221    public interface ClickListener {
1222        /**
1223         * Called when the central child of the WearableListView is tapped.
1224         * @param view View that was clicked.
1225         */
1226        public void onClick(ViewHolder view);
1227
1228        /**
1229         * Called when the user taps the top third of the WearableListView and no item is present
1230         * there. This can happen when you are in initial state and the first, top-most item of the
1231         * WearableListView is centered.
1232         */
1233        public void onTopEmptyRegionClick();
1234    }
1235
1236    /**
1237     * @hide
1238     */
1239    public interface OnOverScrollListener {
1240        public void onOverScroll();
1241    }
1242
1243    /**
1244     * Interface for listening to WearableListView content scrolling.
1245     */
1246    public interface OnScrollListener {
1247        /**
1248         * Called when the content is scrolled, reporting the relative scroll value.
1249         * @param scroll Amount the content was scrolled. This is a delta from the previous
1250         *               position to the new position.
1251         */
1252        public void onScroll(int scroll);
1253
1254        /**
1255         * Called when the content is scrolled, reporting the absolute scroll value.
1256         *
1257         * @deprecated BE ADVISED DO NOT USE THIS This might provide wrong values when contents
1258         * of a RecyclerView change.
1259         *
1260         * @param scroll Absolute scroll position of the content inside the WearableListView.
1261         */
1262        @Deprecated
1263        public void onAbsoluteScrollChange(int scroll);
1264
1265        /**
1266         * Called when WearableListView's scroll state changes.
1267         *
1268         * @param scrollState The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
1269         *                    {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
1270         */
1271        public void onScrollStateChanged(int scrollState);
1272
1273        /**
1274         * Called when the central item of the WearableListView changes.
1275         *
1276         * @param centralPosition Position of the item in the Adapter.
1277         */
1278        public void onCentralPositionChanged(int centralPosition);
1279    }
1280
1281    /**
1282     * A listener interface that can be added to the WearableListView to get notified when the
1283     * central item is changed.
1284     */
1285    public interface OnCentralPositionChangedListener {
1286        /**
1287         * Called when the central item of the WearableListView changes.
1288         *
1289         * @param centralPosition Position of the item in the Adapter.
1290         */
1291        void onCentralPositionChanged(int centralPosition);
1292    }
1293
1294    /**
1295     * Base class for adapters providing data for the WearableListView. For details refer to
1296     * RecyclerView.Adapter.
1297     */
1298    public static abstract class Adapter extends RecyclerView.Adapter<ViewHolder> {
1299    }
1300
1301    private static class SmoothScroller extends LinearSmoothScroller {
1302
1303        private static final float MILLISECONDS_PER_INCH = 100f;
1304
1305        private final LayoutManager mLayoutManager;
1306
1307        public SmoothScroller(Context context, WearableListView.LayoutManager manager) {
1308            super(context);
1309            mLayoutManager = manager;
1310        }
1311
1312        @Override
1313        protected void onStart() {
1314            super.onStart();
1315        }
1316
1317        // TODO: (mindyp): when flinging, return the dydt that triggered the fling.
1318        @Override
1319        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
1320            return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
1321        }
1322
1323        @Override
1324        public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
1325                snapPreference) {
1326            // Snap to center.
1327            return (boxStart + boxEnd) / 2 - (viewStart + viewEnd) / 2;
1328        }
1329
1330        @Override
1331        public PointF computeScrollVectorForPosition(int targetPosition) {
1332            if (targetPosition < mLayoutManager.getFirstPosition()) {
1333                return new PointF(0, -1);
1334            } else {
1335                return new PointF(0, 1);
1336            }
1337        }
1338    }
1339
1340    /**
1341     * Wrapper around items displayed in the list view. {@link .Adapter} must return objects that
1342     * are instances of this class. Consider making the wrapped View implement
1343     * {@link .OnCenterProximityListener} if you want to receive a callback when it becomes or
1344     * ceases to be the central item in the WearableListView.
1345     */
1346    public static class ViewHolder extends RecyclerView.ViewHolder {
1347        public ViewHolder(View itemView) {
1348            super(itemView);
1349        }
1350
1351        /**
1352         * Called when the wrapped view is becoming or ceasing to be the central item of the
1353         * WearableListView.
1354         *
1355         * Retained as protected for backwards compatibility.
1356         *
1357         * @hide
1358         */
1359        protected void onCenterProximity(boolean isCentralItem, boolean animate) {
1360            if (!(itemView instanceof OnCenterProximityListener)) {
1361                return;
1362            }
1363            OnCenterProximityListener item = (OnCenterProximityListener) itemView;
1364            if (isCentralItem) {
1365                item.onCenterPosition(animate);
1366            } else {
1367                item.onNonCenterPosition(animate);
1368            }
1369        }
1370    }
1371
1372    private class SetScrollVerticallyProperty extends Property<WearableListView, Integer> {
1373        public SetScrollVerticallyProperty() {
1374            super(Integer.class, "scrollVertically");
1375        }
1376
1377        @Override
1378        public Integer get(WearableListView wearableListView) {
1379            return wearableListView.mLastScrollChange;
1380        }
1381
1382        @Override
1383        public void set(WearableListView wearableListView, Integer value) {
1384            wearableListView.setScrollVertically(value);
1385        }
1386    }
1387}
1388