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 
17 package android.support.wearable.view;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.ObjectAnimator;
22 import android.annotation.TargetApi;
23 import android.content.Context;
24 import android.graphics.PointF;
25 import android.os.Build;
26 import android.os.Handler;
27 import android.support.v7.widget.LinearSmoothScroller;
28 import android.support.v7.widget.RecyclerView;
29 import android.util.AttributeSet;
30 import android.util.DisplayMetrics;
31 import android.util.Log;
32 import android.util.Property;
33 import android.view.KeyEvent;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewConfiguration;
37 import android.view.ViewGroup;
38 import android.widget.Scroller;
39 
40 import java.util.ArrayList;
41 import 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)
56 public 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 
WearableListView(Context context)176     public WearableListView(Context context) {
177         this(context, null);
178     }
179 
WearableListView(Context context, AttributeSet attrs)180     public WearableListView(Context context, AttributeSet attrs) {
181         this(context, attrs, 0);
182     }
183 
WearableListView(Context context, AttributeSet attrs, int defStyleAttr)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
setAdapter(RecyclerView.Adapter adapter)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
getBaseline()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      */
isAtTop()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      */
resetLayoutManager()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      */
setGreedyTouchMode(boolean greedy)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      */
setInitialOffset(int top)289     public void setInitialOffset(int top) {
290         mInitialOffset = top;
291     }
292 
293     @Override
onInterceptTouchEvent(MotionEvent event)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 
handlePossibleVerticalSwipe(MotionEvent event)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
onTouchEvent(MotionEvent event)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 
releasePressedItem()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 
onScroll(int dy)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      */
addOnScrollListener(OnScrollListener listener)387     public void addOnScrollListener(OnScrollListener listener) {
388         mOnScrollListeners.add(listener);
389     }
390 
391     /**
392      * Removes listener for scroll events.
393      */
removeOnScrollListener(OnScrollListener listener)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      */
addOnCentralPositionChangedListener(OnCentralPositionChangedListener listener)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      */
removeOnCentralPositionChangedListener(OnCentralPositionChangedListener listener)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      */
isGestureNavigationEnabled()415     public boolean isGestureNavigationEnabled() {
416         return mGestureNavigationEnabled;
417     }
418 
419     /**
420      * Sets whether navigation of list with wrist gestures is enabled.
421      */
setEnableGestureNavigation(boolean enabled)422     public void setEnableGestureNavigation(boolean enabled) {
423         mGestureNavigationEnabled = enabled;
424     }
425 
426     @Override /* KeyEvent.Callback */
onKeyDown(int keyCode, KeyEvent event)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      */
tapCenterView()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 
checkForTap(MotionEvent event)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 
animateToMiddle(int newCenterIndex, int oldCenterIndex)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 
startScrollAnimation(List<Animator> animators, int scroll, long duration)512     private void startScrollAnimation(List<Animator> animators, int scroll, long duration) {
513         startScrollAnimation(animators, scroll, duration, 0);
514     }
515 
startScrollAnimation(List<Animator> animators, int scroll, long duration, long delay)516     private void startScrollAnimation(List<Animator> animators, int scroll, long duration,
517             long  delay) {
518         startScrollAnimation(animators, scroll, duration, delay, null);
519     }
520 
startScrollAnimation( int scroll, long duration, long delay, Animator.AnimatorListener listener)521     private void startScrollAnimation(
522             int scroll, long duration, long  delay, Animator.AnimatorListener listener) {
523         startScrollAnimation(null, scroll, duration, delay, listener);
524     }
525 
startScrollAnimation(List<Animator> animators, int scroll, long duration, long delay, Animator.AnimatorListener listener)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
fling(int velocityX, int velocityY)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 
smoothScrollToPosition(int position, RecyclerView.SmoothScroller smoothScroller)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
getChildViewHolder(View child)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      */
setClickListener(ClickListener clickListener)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      */
setOverScrollListener(OnOverScrollListener listener)617     public void setOverScrollListener(OnOverScrollListener listener) {
618         mOverScrollListener = listener;
619     }
620 
findCenterViewIndex()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 
getCenterYPos(View v)644     private static int getCenterYPos(View v) {
645         return v.getTop() + v.getPaddingTop() + getAdjustedHeight(v) / 2;
646     }
647 
handleTouchUp(MotionEvent event, int scrollState)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 
isOverScrolling()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 
getTopViewMaxTop()680     private int getTopViewMaxTop() {
681         return getHeight() / 2;
682     }
683 
getItemHeight()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      */
getCentralViewTop()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      */
animateToCenter()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      */
animateToInitialPosition(final Runnable endAction)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 
handleTouchDown(MotionEvent event)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 
setScrollVertically(int scroll)756     private void setScrollVertically(int scroll) {
757         scrollBy(0, scroll - mLastScrollChange);
758         mLastScrollChange = scroll;
759     }
760 
getAdjustedHeight()761     private int getAdjustedHeight() {
762         return getAdjustedHeight(this);
763     }
764 
getAdjustedHeight(View v)765     private static int getAdjustedHeight(View v) {
766         return v.getHeight() - v.getPaddingBottom() - v.getPaddingTop();
767     }
768 
computeTapRegions(float[] tapRegions)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      */
getMaximizeSingleItem()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      */
setMaximizeSingleItem(boolean maximizeSingleItem)791     public void setMaximizeSingleItem(boolean maximizeSingleItem) {
792         mMaximizeSingleItem = maximizeSingleItem;
793     }
794 
notifyChildrenAboutProximity(boolean animate)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.
findCenterViewIndex()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
onLayoutChildren(RecyclerView.Recycler recycler, State state)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 
performLayoutChildren(Recycler recycler, State state, int parentBottom, int top)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 
performLayoutOneChild(Recycler recycler, int parentBottom)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 
performLayoutMultipleChildren(Recycler recycler, State state, int parentBottom, int top)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 
setAbsoluteScroll(int absoluteScroll)1001         private void setAbsoluteScroll(int absoluteScroll) {
1002             mAbsoluteScroll = absoluteScroll;
1003             for (OnScrollListener listener : mOnScrollListeners) {
1004                 listener.onAbsoluteScrollChange(mAbsoluteScroll);
1005             }
1006         }
1007 
measureView(View v, int height)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 
measureThirdView(View v)1019         private void measureThirdView(View v) {
1020             measureView(v, (int) (1 + (float) getHeight() / THIRD));
1021         }
1022 
measureZoomView(View v)1023         private void measureZoomView(View v) {
1024             measureView(v, getHeight());
1025         }
1026 
1027         @Override
generateDefaultLayoutParams()1028         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
1029             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
1030                     ViewGroup.LayoutParams.WRAP_CONTENT);
1031         }
1032 
1033         @Override
canScrollVertically()1034         public boolean canScrollVertically() {
1035             // Disable vertical scrolling when zoomed.
1036             return getItemCount() != 1 || !mWasZoomedIn;
1037         }
1038 
1039         @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, State state)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
scrollToPosition(int position)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 
setCustomSmoothScroller(RecyclerView.SmoothScroller smoothScroller)1124         public void setCustomSmoothScroller(RecyclerView.SmoothScroller smoothScroller) {
1125             mSmoothScroller = smoothScroller;
1126         }
1127 
clearCustomSmoothScroller()1128         public void clearCustomSmoothScroller() {
1129             mSmoothScroller = null;
1130         }
1131 
getDefaultSmoothScroller(RecyclerView recyclerView)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
smoothScrollToPosition(RecyclerView recyclerView, State state, int position)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 
recycleViewsOutOfBounds(RecyclerView.Recycler recycler)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 
getFirstPosition()1184         public int getFirstPosition() {
1185             return mFirstPosition;
1186         }
1187 
1188         @Override
onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter)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          */
onCenterPosition(boolean animate)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          */
onNonCenterPosition(boolean animate)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          */
onClick(ViewHolder view)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          */
onTopEmptyRegionClick()1233         public void onTopEmptyRegionClick();
1234     }
1235 
1236     /**
1237      * @hide
1238      */
1239     public interface OnOverScrollListener {
onOverScroll()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          */
onScroll(int scroll)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
onAbsoluteScrollChange(int scroll)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          */
onScrollStateChanged(int scrollState)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          */
onCentralPositionChanged(int centralPosition)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          */
onCentralPositionChanged(int centralPosition)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 
SmoothScroller(Context context, WearableListView.LayoutManager manager)1307         public SmoothScroller(Context context, WearableListView.LayoutManager manager) {
1308             super(context);
1309             mLayoutManager = manager;
1310         }
1311 
1312         @Override
onStart()1313         protected void onStart() {
1314             super.onStart();
1315         }
1316 
1317         // TODO: (mindyp): when flinging, return the dydt that triggered the fling.
1318         @Override
calculateSpeedPerPixel(DisplayMetrics displayMetrics)1319         protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
1320             return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
1321         }
1322 
1323         @Override
calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference)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
computeScrollVectorForPosition(int targetPosition)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 {
ViewHolder(View itemView)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          */
onCenterProximity(boolean isCentralItem, boolean animate)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> {
SetScrollVerticallyProperty()1373         public SetScrollVerticallyProperty() {
1374             super(Integer.class, "scrollVertically");
1375         }
1376 
1377         @Override
get(WearableListView wearableListView)1378         public Integer get(WearableListView wearableListView) {
1379             return wearableListView.mLastScrollChange;
1380         }
1381 
1382         @Override
set(WearableListView wearableListView, Integer value)1383         public void set(WearableListView wearableListView, Integer value) {
1384             wearableListView.setScrollVertically(value);
1385         }
1386     }
1387 }
1388