Home | History | Annotate | Download | only in view
      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 
    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