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