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