1 /* 2 * Copyright (C) 2014 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 com.android.tv.settings.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorInflater; 21 import android.animation.ObjectAnimator; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.database.DataSetObserver; 25 import android.graphics.Canvas; 26 import android.graphics.Rect; 27 import android.os.Bundle; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.view.FocusFinder; 33 import android.view.accessibility.AccessibilityEvent; 34 import android.view.KeyEvent; 35 import android.view.MotionEvent; 36 import android.view.SoundEffectConstants; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.view.ViewParent; 40 import android.widget.AdapterView; 41 import android.widget.Adapter; 42 43 import com.android.tv.settings.R; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 48 /** 49 * A scrollable AdapterView, similar to {@link android.widget.Gallery}. Features include: 50 * <p> 51 * Supports "expandable" views by supplying a Adapter that implements 52 * {@link ScrollAdapter#getExpandAdapter()}. Generally you could see two expanded views at most: one 53 * fade in, one fade out. 54 * <p> 55 * Supports {@link #HORIZONTAL} and {@link #VERTICAL} set by {@link #setOrientation(int)}. 56 * So you could have a vertical ScrollAdapterView with a nested expanding Horizontal ScrollAdapterView. 57 * <p> 58 * Supports Grid view style, see {@link #setGridSetting(int)}. 59 * <p> 60 * Supports Different strategies of scrolling viewport, see 61 * {@link ScrollController#SCROLL_CENTER_IN_MIDDLE}, 62 * {@link ScrollController#SCROLL_CENTER_FIXED}, and 63 * {@link ScrollController#SCROLL_CENTER_FIXED_PERCENT}. 64 * Also take a look of {@link #adjustSystemScrollPos()} for better understanding how Center 65 * is translated to android View scroll position. 66 * <p> 67 * Expandable items animation is based on distance to the center. Motivation behind not using two 68 * time based animations for focusing/onfocusing is that in a fast scroll, there is no better way to 69 * synchronize these two animations with scroller animation; so you will end up with situation that 70 * scale animated item cannot be kept in the center because scroll animation is too fast/too slow. 71 * By using distance to the scroll center, the animation of focus/unfocus will be accurately synced 72 * with scroller animation. {@link #setLowItemTransform(Animator)} transforms items that are left or 73 * up to scroll center position; {@link #setHighItemTransform(Animator)} transforms items that are 74 * right or down to the scroll center position. It's recommended to use xml resource ref 75 * "highItemTransform" and "lowItemTransform" attributes to load the animation from xml. The 76 * animation duration which android by default is a duration of milliseconds is interpreted as dip 77 * to the center. Here is an example that scales the center item to "1.2" of original size, any item 78 * far from 60dip to scroll center has normal scale (scale = 1): 79 * <pre>{@code 80 * <set xmlns:android="http://schemas.android.com/apk/res/android" > 81 * <objectAnimator 82 * android:startOffset="0" 83 * android:duration="60" 84 * android:valueFrom="1.2" 85 * android:valueTo="1" 86 * android:valueType="floatType" 87 * android:propertyName="scaleX" /> 88 * <objectAnimator 89 * android:startOffset="0" 90 * android:duration="60" 91 * android:valueFrom="1.2" 92 * android:valueTo="1" 93 * android:valueType="floatType" 94 * android:propertyName="scaleY"/> 95 * </set> 96 * } </pre> 97 * When using an animation that expands the selected item room has to be made in the view for 98 * the scale animation. To accomplish this set right/left and/or top/bottom padding values 99 * for the ScrollAdapterView and also set its clipToPadding value to false. Another option is 100 * to include padding in the item view itself. 101 * <p> 102 * Expanded items animation uses "normal" animation: duration is duration. Use xml attribute 103 * expandedItemInAnim and expandedItemOutAnim for animation. A best practice is specify startOffset 104 * for expandedItemInAnim to avoid showing half loaded expanded items during a fast scroll of 105 * expandable items. 106 */ 107 public final class ScrollAdapterView extends AdapterView<Adapter> { 108 109 /** Callback interface for changing state of selected item */ 110 public static interface OnItemChangeListener { 111 /** 112 * In contrast to standard onFocusChange, the event is fired only when scrolling stops 113 * @param view the view focusing to 114 * @param position index in ScrollAdapter 115 * @param targetCenter final center position of view to the left edge of ScrollAdapterView 116 */ onItemSelected(View view, int position, int targetCenter)117 public void onItemSelected(View view, int position, int targetCenter); 118 } 119 120 /** 121 * Callback interface when there is scrolling happened, this function is called before 122 * applying transformations ({@link ScrollAdapterTransform}). This listener can be a 123 * replacement of {@link ScrollAdapterTransform}. The difference is that this listener 124 * is called once when scroll position changes, {@link ScrollAdapterTransform} is called 125 * on each child view. 126 */ 127 public static interface OnScrollListener { 128 /** 129 * @param view the view focusing to 130 * @param position index in ScrollAdapter 131 * @param mainPosition position in the main axis 0(inclusive) ~ 1(exclusive) 132 * @param secondPosition position in the second axis 0(inclusive) ~ 1(exclusive) 133 */ onScrolled(View view, int position, float mainPosition, float secondPosition)134 public void onScrolled(View view, int position, float mainPosition, float secondPosition); 135 } 136 137 // Hardcoded from InputDevice in sdk 18. 138 private static final int SOURCE_TOUCH_NAV = 0x00200000; 139 140 private static final String TAG = "ScrollAdapterView"; 141 142 private static final boolean DBG = false; 143 private static final boolean DEBUG_FOCUS = false; 144 145 private static final int MAX_RECYCLED_VIEWS = 10; 146 private static final int MAX_RECYCLED_EXPANDED_VIEWS = 3; 147 148 // search range for stable id, see {@link #heuristicGetPersistentIndex()} 149 private static final int SEARCH_ID_RANGE = 30; 150 151 /** 152 * {@link ScrollAdapterView} fills horizontally 153 */ 154 public static final int HORIZONTAL = 0; 155 156 /** 157 * {@link ScrollAdapterView} fills vertically 158 */ 159 public static final int VERTICAL = 1; 160 161 /** calculate number of items on second axis by "parentSize / childSize" */ 162 public static final int GRID_SETTING_AUTO = 0; 163 /** single item on second axis (i.e. not a grid view) */ 164 public static final int GRID_SETTING_SINGLE = 1; 165 166 private int mOrientation = HORIZONTAL; 167 168 /** saved measuredSpec to pass to child views */ 169 private int mMeasuredSpec = -1; 170 171 /** the Adapter used to create views */ 172 private ScrollAdapter mAdapter; 173 private ScrollAdapterCustomSize mAdapterCustomSize; 174 private ScrollAdapterCustomAlign mAdapterCustomAlign; 175 private ScrollAdapterErrorHandler mAdapterErrorHandler; 176 private int mSelectedSize; 177 178 // flag that we have made initial selection during refreshing ScrollAdapterView 179 private boolean mMadeInitialSelection = false; 180 181 /** allow animate expanded size change when Scroller is stopped */ 182 private boolean mAnimateLayoutChange = true; 183 184 private static class RecycledViews { 185 List<View>[] mViews; 186 int mMaxRecycledViews; 187 ScrollAdapterBase mAdapter; 188 RecycledViews(int max)189 RecycledViews(int max) { 190 mMaxRecycledViews = max; 191 } 192 updateAdapter(ScrollAdapterBase adapter)193 void updateAdapter(ScrollAdapterBase adapter) { 194 if (adapter != null) { 195 int typeCount = adapter.getViewTypeCount(); 196 if (mViews == null || typeCount != mViews.length) { 197 mViews = new List[typeCount]; 198 for (int i = 0; i < typeCount; i++) { 199 mViews[i] = new ArrayList<View>(); 200 } 201 } 202 } 203 mAdapter = adapter; 204 } 205 recycleView(View child, int type)206 void recycleView(View child, int type) { 207 if (mAdapter != null) { 208 mAdapter.viewRemoved(child); 209 } 210 if (mViews != null && type >=0 && type < mViews.length 211 && mViews[type].size() < mMaxRecycledViews) { 212 mViews[type].add(child); 213 } 214 } 215 getView(int type)216 View getView(int type) { 217 if (mViews != null && type >= 0 && type < mViews.length) { 218 List<View> array = mViews[type]; 219 return array.size() > 0 ? array.remove(array.size() - 1) : null; 220 } 221 return null; 222 } 223 } 224 225 private RecycledViews mRecycleViews = new RecycledViews(MAX_RECYCLED_VIEWS); 226 227 private RecycledViews mRecycleExpandedViews = new RecycledViews(MAX_RECYCLED_EXPANDED_VIEWS); 228 229 /** exclusive index of view on the left */ 230 private int mLeftIndex; 231 /** exclusive index of view on the right */ 232 private int mRightIndex; 233 234 /** space between two items */ 235 private int mSpace; 236 private int mSpaceLow; 237 private int mSpaceHigh; 238 239 private int mGridSetting = GRID_SETTING_SINGLE; 240 /** effective number of items on 2nd axis, calculated in {@link #onMeasure} */ 241 private int mItemsOnOffAxis; 242 243 /** latch on centered item automatically when scroller velocity is less than LATCH_THRESHOLD*/ 244 private static final float LATCH_THRESHOLD = 1000f; 245 246 /** maintains the scroller information */ 247 private ScrollController mScroll; 248 249 private ArrayList<OnItemChangeListener> mOnItemChangeListeners = 250 new ArrayList<OnItemChangeListener>(); 251 private ArrayList<OnScrollListener> mOnScrollListeners = 252 new ArrayList<OnScrollListener>(); 253 254 private final static boolean DEFAULT_NAVIGATE_OUT_ALLOWED = true; 255 private final static boolean DEFAULT_NAVIGATE_OUT_OF_OFF_AXIS_ALLOWED = true; 256 257 private final static boolean DEFAULT_NAVIGATE_IN_ANIMATION_ALLOWED = true; 258 259 final class ExpandableChildStates extends ViewsStateBundle { ExpandableChildStates()260 ExpandableChildStates() { 261 super(SAVE_NO_CHILD, 0); 262 } 263 @Override saveVisibleViewsUnchecked()264 protected void saveVisibleViewsUnchecked() { 265 for (int i = firstExpandableIndex(), last = lastExpandableIndex(); i < last; i++) { 266 saveViewUnchecked(getChildAt(i), getAdapterIndex(i)); 267 } 268 } 269 } 270 final class ExpandedChildStates extends ViewsStateBundle { ExpandedChildStates()271 ExpandedChildStates() { 272 super(SAVE_LIMITED_CHILD, SAVE_LIMITED_CHILD_DEFAULT_VALUE); 273 } 274 @Override saveVisibleViewsUnchecked()275 protected void saveVisibleViewsUnchecked() { 276 for (int i = 0, size = mExpandedViews.size(); i < size; i++) { 277 ExpandedView v = mExpandedViews.get(i); 278 saveViewUnchecked(v.expandedView, v.index); 279 } 280 } 281 } 282 283 private static class ChildViewHolder { 284 int mItemViewType; 285 int mMaxSize; // max size in mainAxis of the same offaxis 286 int mExtraSpaceLow; // extra space added before the view 287 float mLocationInParent; // temp variable used in animating expanded view size change 288 float mLocation; // temp variable used in animating expanded view size change 289 int mScrollCenter; // cached scroll center 290 ChildViewHolder(int t)291 ChildViewHolder(int t) { 292 mItemViewType = t; 293 } 294 } 295 296 /** 297 * set in {@link #onRestoreInstanceState(Parcelable)} which triggers a re-layout 298 * and ScrollAdapterView restores states in {@link #onLayout} 299 */ 300 private AdapterViewState mLoadingState; 301 302 /** saves all expandable child states */ 303 final private ExpandableChildStates mExpandableChildStates = new ExpandableChildStates(); 304 305 /** saves all expanded child states */ 306 final private ExpandedChildStates mExpandedChildStates = new ExpandedChildStates(); 307 308 private ScrollAdapterTransform mItemTransform; 309 310 /** flag for data changed, {@link #onLayout} will cleaning the whole view */ 311 private boolean mDataSetChangedFlag; 312 313 // current selected view adapter index, this is the final position to scroll to 314 private int mSelectedIndex; 315 316 private static class ScrollInfo { 317 int index; 318 long id; 319 float mainPos; 320 float secondPos; 321 int viewLocation; ScrollInfo()322 ScrollInfo() { 323 clear(); 324 } isValid()325 boolean isValid() { 326 return index >= 0; 327 } clear()328 void clear() { 329 index = -1; 330 id = INVALID_ROW_ID; 331 } copyFrom(ScrollInfo other)332 void copyFrom(ScrollInfo other) { 333 index = other.index; 334 id = other.id; 335 mainPos = other.mainPos; 336 secondPos = other.secondPos; 337 viewLocation = other.viewLocation; 338 } 339 } 340 341 // positions that current scrolled to 342 private final ScrollInfo mCurScroll = new ScrollInfo(); 343 private int mItemSelected = -1; 344 345 private int mPendingSelection = -1; 346 private float mPendingScrollPosition = 0f; 347 348 private final ScrollInfo mScrollBeforeReset = new ScrollInfo(); 349 350 private boolean mScrollTaskRunning; 351 352 private ScrollAdapterBase mExpandAdapter; 353 354 /** used for measuring the size of {@link ScrollAdapterView} */ 355 private int mScrapWidth; 356 private int mScrapHeight; 357 358 /** Animator for showing expanded item */ 359 private Animator mExpandedItemInAnim = null; 360 361 /** Animator for hiding expanded item */ 362 private Animator mExpandedItemOutAnim = null; 363 364 private boolean mNavigateOutOfOffAxisAllowed = DEFAULT_NAVIGATE_OUT_OF_OFF_AXIS_ALLOWED; 365 private boolean mNavigateOutAllowed = DEFAULT_NAVIGATE_OUT_ALLOWED; 366 367 private boolean mNavigateInAnimationAllowed = DEFAULT_NAVIGATE_IN_ANIMATION_ALLOWED; 368 369 /** 370 * internal structure maintaining status of expanded views 371 */ 372 final class ExpandedView { 373 private static final int ANIM_DURATION = 450; ExpandedView(View v, int i, int t)374 ExpandedView(View v, int i, int t) { 375 expandedView = v; 376 index = i; 377 viewType = t; 378 } 379 380 int index; // "Adapter index" of the expandable view 381 int viewType; 382 View expandedView; // expanded view 383 float progress = 0f; // 0 ~ 1, indication if it's expanding or shrinking 384 Animator grow_anim; 385 Animator shrink_anim; 386 createFadeInAnimator()387 Animator createFadeInAnimator() { 388 if (mExpandedItemInAnim == null) { 389 expandedView.setAlpha(0); 390 ObjectAnimator anim1 = ObjectAnimator.ofFloat(null, "alpha", 1); 391 anim1.setStartDelay(ANIM_DURATION / 2); 392 anim1.setDuration(ANIM_DURATION * 2); 393 return anim1; 394 } else { 395 return mExpandedItemInAnim.clone(); 396 } 397 } 398 createFadeOutAnimator()399 Animator createFadeOutAnimator() { 400 if (mExpandedItemOutAnim == null) { 401 ObjectAnimator anim1 = ObjectAnimator.ofFloat(null, "alpha", 0); 402 anim1.setDuration(ANIM_DURATION); 403 return anim1; 404 } else { 405 return mExpandedItemOutAnim.clone(); 406 } 407 } 408 setProgress(float p)409 void setProgress(float p) { 410 boolean growing = p > progress; 411 boolean shrinking = p < progress; 412 progress = p; 413 if (growing) { 414 if (shrink_anim != null) { 415 shrink_anim.cancel(); 416 shrink_anim = null; 417 } 418 if (grow_anim == null) { 419 grow_anim = createFadeInAnimator(); 420 grow_anim.setTarget(expandedView); 421 grow_anim.start(); 422 } 423 if (!mAnimateLayoutChange) { 424 grow_anim.end(); 425 } 426 } else if (shrinking) { 427 if (grow_anim != null) { 428 grow_anim.cancel(); 429 grow_anim = null; 430 } 431 if (shrink_anim == null) { 432 shrink_anim = createFadeOutAnimator(); 433 shrink_anim.setTarget(expandedView); 434 shrink_anim.start(); 435 } 436 if (!mAnimateLayoutChange) { 437 shrink_anim.end(); 438 } 439 } 440 } 441 442 void close() { 443 if (shrink_anim != null) { 444 shrink_anim.cancel(); 445 shrink_anim = null; 446 } 447 if (grow_anim != null) { 448 grow_anim.cancel(); 449 grow_anim = null; 450 } 451 } 452 } 453 454 /** list of ExpandedView structure */ 455 private final ArrayList<ExpandedView> mExpandedViews = new ArrayList<ExpandedView>(4); 456 457 /** no scrolling */ 458 private static final int NO_SCROLL = 0; 459 /** scrolling and centering a known focused view */ 460 private static final int SCROLL_AND_CENTER_FOCUS = 3; 461 462 /** 463 * internal state machine for scrolling, typical scenario: <br> 464 * DPAD up/down is pressed: -> {@link #SCROLL_AND_CENTER_FOCUS} -> {@link #NO_SCROLL} <br> 465 */ 466 private int mScrollerState; 467 468 Rect mTempRect = new Rect(); // temp variable used in UI thread 469 470 // Controls whether or not sounds should be played when scrolling/clicking 471 private boolean mPlaySoundEffects = true; 472 473 public ScrollAdapterView(Context context, AttributeSet attrs) { 474 super(context, attrs); 475 mScroll = new ScrollController(getContext()); 476 setChildrenDrawingOrderEnabled(true); 477 setSoundEffectsEnabled(true); 478 setWillNotDraw(true); 479 initFromAttributes(context, attrs); 480 reset(); 481 } 482 483 private void initFromAttributes(Context context, AttributeSet attrs) { 484 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScrollAdapterView); 485 486 setOrientation(a.getInt(R.styleable.ScrollAdapterView_orientation, HORIZONTAL)); 487 488 mScroll.setScrollItemAlign(a.getInt(R.styleable.ScrollAdapterView_scrollItemAlign, 489 ScrollController.SCROLL_ITEM_ALIGN_CENTER)); 490 491 setGridSetting(a.getInt(R.styleable.ScrollAdapterView_gridSetting, 1)); 492 493 if (a.hasValue(R.styleable.ScrollAdapterView_lowItemTransform)) { 494 setLowItemTransform(AnimatorInflater.loadAnimator(getContext(), 495 a.getResourceId(R.styleable.ScrollAdapterView_lowItemTransform, -1))); 496 } 497 498 if (a.hasValue(R.styleable.ScrollAdapterView_highItemTransform)) { 499 setHighItemTransform(AnimatorInflater.loadAnimator(getContext(), 500 a.getResourceId(R.styleable.ScrollAdapterView_highItemTransform, -1))); 501 } 502 503 if (a.hasValue(R.styleable.ScrollAdapterView_expandedItemInAnim)) { 504 mExpandedItemInAnim = AnimatorInflater.loadAnimator(getContext(), 505 a.getResourceId(R.styleable.ScrollAdapterView_expandedItemInAnim, -1)); 506 } 507 508 if (a.hasValue(R.styleable.ScrollAdapterView_expandedItemOutAnim)) { 509 mExpandedItemOutAnim = AnimatorInflater.loadAnimator(getContext(), 510 a.getResourceId(R.styleable.ScrollAdapterView_expandedItemOutAnim, -1)); 511 } 512 513 setSpace(a.getDimensionPixelSize(R.styleable.ScrollAdapterView_space, 0)); 514 515 setSelectedTakesMoreSpace(a.getBoolean( 516 R.styleable.ScrollAdapterView_selectedTakesMoreSpace, false)); 517 518 setSelectedSize(a.getDimensionPixelSize( 519 R.styleable.ScrollAdapterView_selectedSize, 0)); 520 521 setScrollCenterStrategy(a.getInt(R.styleable.ScrollAdapterView_scrollCenterStrategy, 0)); 522 523 setScrollCenterOffset(a.getDimensionPixelSize( 524 R.styleable.ScrollAdapterView_scrollCenterOffset, 0)); 525 526 setScrollCenterOffsetPercent(a.getInt( 527 R.styleable.ScrollAdapterView_scrollCenterOffsetPercent, 0)); 528 529 setNavigateOutAllowed(a.getBoolean( 530 R.styleable.ScrollAdapterView_navigateOutAllowed, DEFAULT_NAVIGATE_OUT_ALLOWED)); 531 532 setNavigateOutOfOffAxisAllowed(a.getBoolean( 533 R.styleable.ScrollAdapterView_navigateOutOfOffAxisAllowed, 534 DEFAULT_NAVIGATE_OUT_OF_OFF_AXIS_ALLOWED)); 535 536 setNavigateInAnimationAllowed(a.getBoolean( 537 R.styleable.ScrollAdapterView_navigateInAnimationAllowed, 538 DEFAULT_NAVIGATE_IN_ANIMATION_ALLOWED)); 539 540 mScroll.lerper().setDivisor(a.getFloat( 541 R.styleable.ScrollAdapterView_lerperDivisor, Lerper.DEFAULT_DIVISOR)); 542 543 a.recycle(); 544 } 545 546 public void setOrientation(int orientation) { 547 mOrientation = orientation; 548 mScroll.setOrientation(orientation); 549 } 550 551 public int getOrientation() { 552 return mOrientation; 553 } 554 555 @SuppressWarnings("unchecked") 556 private void reset() { 557 mScrollBeforeReset.copyFrom(mCurScroll); 558 mLeftIndex = -1; 559 mRightIndex = 0; 560 mDataSetChangedFlag = false; 561 for (int i = 0, c = mExpandedViews.size(); i < c; i++) { 562 ExpandedView v = mExpandedViews.get(i); 563 v.close(); 564 removeViewInLayout(v.expandedView); 565 mRecycleExpandedViews.recycleView(v.expandedView, v.viewType); 566 } 567 mExpandedViews.clear(); 568 for (int i = getChildCount() - 1; i >= 0; i--) { 569 View child = getChildAt(i); 570 removeViewInLayout(child); 571 recycleExpandableView(child); 572 } 573 mRecycleViews.updateAdapter(mAdapter); 574 mRecycleExpandedViews.updateAdapter(mExpandAdapter); 575 mSelectedIndex = -1; 576 mCurScroll.clear(); 577 mMadeInitialSelection = false; 578 } 579 580 /** find the view that containing scrollCenter or the next view */ 581 private int findViewIndexContainingScrollCenter(int scrollCenter, int scrollCenterOffAxis, 582 boolean findNext) { 583 final int lastExpandable = lastExpandableIndex(); 584 for (int i = firstExpandableIndex(); i < lastExpandable; i ++) { 585 View view = getChildAt(i); 586 int centerOffAxis = getCenterInOffAxis(view); 587 int viewSizeOffAxis; 588 if (mOrientation == HORIZONTAL) { 589 viewSizeOffAxis = view.getHeight(); 590 } else { 591 viewSizeOffAxis = view.getWidth(); 592 } 593 int centerMain = getScrollCenter(view); 594 if (hasScrollPosition(centerMain, getSize(view), scrollCenter) 595 && (mItemsOnOffAxis == 1 || hasScrollPositionSecondAxis( 596 scrollCenterOffAxis, viewSizeOffAxis, centerOffAxis))) { 597 if (findNext) { 598 if (mScroll.isMainAxisMovingForward() && centerMain < scrollCenter) { 599 if (i + mItemsOnOffAxis < lastExpandableIndex()) { 600 i = i + mItemsOnOffAxis; 601 } 602 } else if (!mScroll.isMainAxisMovingForward() && centerMain > scrollCenter) { 603 if (i - mItemsOnOffAxis >= firstExpandableIndex()) { 604 i = i - mItemsOnOffAxis; 605 } 606 } 607 if (mItemsOnOffAxis == 1) { 608 // don't look in second axis if it's not grid 609 } else if (mScroll.isSecondAxisMovingForward() && 610 centerOffAxis < scrollCenterOffAxis) { 611 if (i + 1 < lastExpandableIndex()) { 612 i += 1; 613 } 614 } else if (!mScroll.isSecondAxisMovingForward() && 615 centerOffAxis < scrollCenterOffAxis) { 616 if (i - 1 >= firstExpandableIndex()) { 617 i -= 1; 618 } 619 } 620 } 621 return i; 622 } 623 } 624 return -1; 625 } 626 627 private int findViewIndexContainingScrollCenter() { 628 return findViewIndexContainingScrollCenter(mScroll.mainAxis().getScrollCenter(), 629 mScroll.secondAxis().getScrollCenter(), false); 630 } 631 632 @Override 633 public int getFirstVisiblePosition() { 634 int first = firstExpandableIndex(); 635 return lastExpandableIndex() == first ? -1 : getAdapterIndex(first); 636 } 637 638 @Override 639 public int getLastVisiblePosition() { 640 int last = lastExpandableIndex(); 641 return firstExpandableIndex() == last ? -1 : getAdapterIndex(last - 1); 642 } 643 644 @Override 645 public void setSelection(int position) { 646 setSelectionInternal(position, 0f, true); 647 } 648 649 public void setSelection(int position, float offset) { 650 setSelectionInternal(position, offset, true); 651 } 652 653 public int getCurrentAnimationDuration() { 654 return mScroll.getCurrentAnimationDuration(); 655 } 656 657 public void setSelectionSmooth(int index) { 658 setSelectionSmooth(index, 0); 659 } 660 661 /** set selection using animation with a given duration, use 0 duration for auto */ 662 public void setSelectionSmooth(int index, int duration) { 663 int currentExpandableIndex = indexOfChild(getSelectedView()); 664 if (currentExpandableIndex < 0) { 665 return; 666 } 667 int adapterIndex = getAdapterIndex(currentExpandableIndex); 668 if (index == adapterIndex) { 669 return; 670 } 671 boolean isGrowing = index > adapterIndex; 672 View nextTop = null; 673 if (isGrowing) { 674 do { 675 if (index < getAdapterIndex(lastExpandableIndex())) { 676 nextTop = getChildAt(expandableIndexFromAdapterIndex(index)); 677 break; 678 } 679 } while (fillOneRightChildView(false)); 680 } else { 681 do { 682 if (index >= getAdapterIndex(firstExpandableIndex())) { 683 nextTop = getChildAt(expandableIndexFromAdapterIndex(index)); 684 break; 685 } 686 } while (fillOneLeftChildView(false)); 687 } 688 if (nextTop == null) { 689 return; 690 } 691 int direction = isGrowing ? 692 (mOrientation == HORIZONTAL ? View.FOCUS_RIGHT : View.FOCUS_DOWN) : 693 (mOrientation == HORIZONTAL ? View.FOCUS_LEFT : View.FOCUS_UP); 694 scrollAndFocusTo(nextTop, direction, false, duration, false); 695 } 696 697 private void fireDataSetChanged() { 698 // set flag and trigger a scroll task 699 mDataSetChangedFlag = true; 700 scheduleScrollTask(); 701 } 702 703 private DataSetObserver mDataObserver = new DataSetObserver() { 704 705 @Override 706 public void onChanged() { 707 fireDataSetChanged(); 708 } 709 710 @Override 711 public void onInvalidated() { 712 fireDataSetChanged(); 713 } 714 715 }; 716 717 @Override 718 public Adapter getAdapter() { 719 return mAdapter; 720 } 721 722 /** 723 * Adapter must be an implementation of {@link ScrollAdapter}. 724 */ 725 @Override 726 public void setAdapter(Adapter adapter) { 727 if (mAdapter != null) { 728 mAdapter.unregisterDataSetObserver(mDataObserver); 729 } 730 mAdapter = (ScrollAdapter) adapter; 731 mExpandAdapter = mAdapter.getExpandAdapter(); 732 mAdapter.registerDataSetObserver(mDataObserver); 733 mAdapterCustomSize = adapter instanceof ScrollAdapterCustomSize ? 734 (ScrollAdapterCustomSize) adapter : null; 735 mAdapterCustomAlign = adapter instanceof ScrollAdapterCustomAlign ? 736 (ScrollAdapterCustomAlign) adapter : null; 737 mMeasuredSpec = -1; 738 mLoadingState = null; 739 mPendingSelection = -1; 740 mExpandableChildStates.clear(); 741 mExpandedChildStates.clear(); 742 mCurScroll.clear(); 743 mScrollBeforeReset.clear(); 744 fireDataSetChanged(); 745 } 746 747 public void setErrorHandler(ScrollAdapterErrorHandler errorHandler) { 748 mAdapterErrorHandler = errorHandler; 749 } 750 751 @Override 752 public View getSelectedView() { 753 return mSelectedIndex >= 0 ? 754 getChildAt(expandableIndexFromAdapterIndex(mSelectedIndex)) : null; 755 } 756 757 public View getSelectedExpandedView() { 758 ExpandedView ev = findExpandedView(mExpandedViews, getSelectedItemPosition()); 759 return ev == null ? null : ev.expandedView; 760 } 761 762 public View getViewContainingScrollCenter() { 763 return getChildAt(findViewIndexContainingScrollCenter()); 764 } 765 766 public int getIndexContainingScrollCenter() { 767 return getAdapterIndex(findViewIndexContainingScrollCenter()); 768 } 769 770 @Override 771 public int getSelectedItemPosition() { 772 return mSelectedIndex; 773 } 774 775 @Override 776 public Object getSelectedItem() { 777 int index = getSelectedItemPosition(); 778 if (index < 0) return null; 779 return getAdapter().getItem(index); 780 } 781 782 @Override 783 public long getSelectedItemId() { 784 if (mAdapter != null) { 785 int index = getSelectedItemPosition(); 786 if (index < 0) return INVALID_ROW_ID; 787 return mAdapter.getItemId(index); 788 } 789 return INVALID_ROW_ID; 790 } 791 792 public View getItemView(int position) { 793 int index = expandableIndexFromAdapterIndex(position); 794 if (index >= firstExpandableIndex() && index < lastExpandableIndex()) { 795 return getChildAt(index); 796 } 797 return null; 798 } 799 800 /** 801 * set system scroll position from our scroll position, 802 */ 803 private void adjustSystemScrollPos() { 804 scrollTo(mScroll.horizontal.getSystemScrollPos(), mScroll.vertical.getSystemScrollPos()); 805 } 806 807 @Override 808 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 809 mScroll.horizontal.setSize(w); 810 mScroll.vertical.setSize(h); 811 scheduleScrollTask(); 812 } 813 814 /** 815 * called from onLayout() to adjust all children's transformation based on how far they are from 816 * {@link ScrollController.Axis#getScrollCenter()} 817 */ 818 private void applyTransformations() { 819 if (mItemTransform == null) { 820 return; 821 } 822 int lastExpandable = lastExpandableIndex(); 823 for (int i = firstExpandableIndex(); i < lastExpandable; i++) { 824 View child = getChildAt(i); 825 mItemTransform.transform(child, getScrollCenter(child) 826 - mScroll.mainAxis().getScrollCenter(), mItemsOnOffAxis == 1 ? 0 827 : getCenterInOffAxis(child) - mScroll.secondAxis().getScrollCenter()); 828 } 829 } 830 831 @Override 832 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 833 super.onLayout(changed, left, top, right, bottom); 834 updateViewsLocations(true); 835 } 836 837 private void scheduleScrollTask() { 838 if (!mScrollTaskRunning) { 839 mScrollTaskRunning = true; 840 postOnAnimation(mScrollTask); 841 } 842 } 843 844 Runnable mScrollTask = new Runnable() { 845 @Override 846 public void run() { 847 try { 848 scrollTaskRunInternal(); 849 } catch (RuntimeException ex) { 850 reset(); 851 if (mAdapterErrorHandler != null) { 852 mAdapterErrorHandler.onError(ex); 853 } else { 854 ex.printStackTrace(); 855 } 856 } 857 } 858 }; 859 860 private void scrollTaskRunInternal() { 861 mScrollTaskRunning = false; 862 // 1. adjust mScrollController and system Scroll position 863 if (mDataSetChangedFlag) { 864 reset(); 865 } 866 if (mAdapter == null || mAdapter.getCount() == 0) { 867 invalidate(); 868 if (mAdapter != null) { 869 fireItemChange(); 870 } 871 return; 872 } 873 if (mMeasuredSpec == -1) { 874 // not layout yet 875 requestLayout(); 876 scheduleScrollTask(); 877 return; 878 } 879 restoreLoadingState(); 880 mScroll.computeAndSetScrollPosition(); 881 882 boolean noChildBeforeFill = getChildCount() == 0; 883 884 if (!noChildBeforeFill) { 885 updateViewsLocations(false); 886 adjustSystemScrollPos(); 887 } 888 889 // 2. prune views that scroll out of visible area 890 pruneInvisibleViewsInLayout(); 891 892 // 3. fill views in blank area 893 fillVisibleViewsInLayout(); 894 895 if (noChildBeforeFill && getChildCount() > 0) { 896 // if this is the first time add child(ren), we will get the initial value of 897 // mScrollCenter after fillVisibleViewsInLayout(), and we need initalize the system 898 // scroll position 899 updateViewsLocations(false); 900 adjustSystemScrollPos(); 901 } 902 903 // 4. perform scroll position based animation 904 // TODO remove vars once b/11602506 is fixed 905 int index = mCurScroll.index; 906 float mainPos = mCurScroll.mainPos; 907 float secondPos = mCurScroll.secondPos; 908 fireScrollChange(); 909 if (DEBUG_FOCUS && mScroll.isFinished()) { 910 Log.d(TAG, "Scroll event finished, index " + index + " -> " + mCurScroll.index 911 + " mainPos " + mainPos + " -> " + mCurScroll.mainPos + " secondPos " 912 + secondPos + " -> " + mCurScroll.secondPos); 913 } 914 applyTransformations(); 915 916 // 5. trigger another layout until the scroll stops 917 if (!mScroll.isFinished()) { 918 scheduleScrollTask(); 919 } else { 920 // force ScrollAdapterView to reorder child order and call getChildDrawingOrder() 921 invalidate(); 922 fireItemChange(); 923 } 924 } 925 926 @Override 927 public void requestChildFocus(View child, View focused) { 928 boolean receiveFocus = getFocusedChild() == null && child != null; 929 super.requestChildFocus(child, focused); 930 if (receiveFocus && mScroll.isFinished()) { 931 // schedule {@link #updateViewsLocations()} for focus transition into expanded view 932 scheduleScrollTask(); 933 } 934 } 935 936 private void recycleExpandableView(View child) { 937 ChildViewHolder holder = ((ChildViewHolder)child.getTag(R.id.ScrollAdapterViewChild)); 938 if (holder != null) { 939 mRecycleViews.recycleView(child, holder.mItemViewType); 940 } 941 } 942 943 private void pruneInvisibleViewsInLayout() { 944 View selectedView = getSelectedView(); 945 if (mScroll.isFinished() || mScroll.isMainAxisMovingForward()) { 946 while (true) { 947 int firstIndex = firstExpandableIndex(); 948 View child = getChildAt(firstIndex); 949 if (child == selectedView) { 950 break; 951 } 952 View nextChild = getChildAt(firstIndex + mItemsOnOffAxis); 953 if (nextChild == null) { 954 break; 955 } 956 View last = getChildAt(lastExpandableIndex() - 1); 957 if (mOrientation == HORIZONTAL) { 958 if (child.getRight() - getScrollX() > 0) { 959 // don't prune the first view if it's visible 960 break; 961 } 962 } else { 963 // VERTICAL is symmetric to HORIZONTAL, see comments above 964 if (child.getBottom() - getScrollY() > 0) { 965 break; 966 } 967 } 968 boolean foundFocus = false; 969 for (int i = 0; i < mItemsOnOffAxis; i++){ 970 int childIndex = firstIndex + i; 971 if (childHasFocus(childIndex)) { 972 foundFocus = true; 973 break; 974 } 975 } 976 if (foundFocus) { 977 break; 978 } 979 for (int i = 0; i < mItemsOnOffAxis; i++){ 980 child = getChildAt(firstExpandableIndex()); 981 mExpandableChildStates.saveInvisibleView(child, mLeftIndex + 1); 982 removeViewInLayout(child); 983 recycleExpandableView(child); 984 mLeftIndex++; 985 } 986 } 987 } 988 if (mScroll.isFinished() || !mScroll.isMainAxisMovingForward()) { 989 while (true) { 990 int count = mRightIndex % mItemsOnOffAxis; 991 if (count == 0) { 992 count = mItemsOnOffAxis; 993 } 994 if (count > mRightIndex - mLeftIndex - 1) { 995 break; 996 } 997 int lastIndex = lastExpandableIndex(); 998 View child = getChildAt(lastIndex - 1); 999 if (child == selectedView) { 1000 break; 1001 } 1002 View first = getChildAt(firstExpandableIndex()); 1003 if (mOrientation == HORIZONTAL) { 1004 if (child.getLeft() - getScrollX() < getWidth()) { 1005 // don't prune the last view if it's visible 1006 break; 1007 } 1008 } else { 1009 // VERTICAL is symmetric to HORIZONTAL, see comments above 1010 if (child.getTop() - getScrollY() < getHeight()) { 1011 break; 1012 } 1013 } 1014 boolean foundFocus = false; 1015 for (int i = 0; i < count; i++){ 1016 int childIndex = lastIndex - 1 - i; 1017 if (childHasFocus(childIndex)) { 1018 foundFocus = true; 1019 break; 1020 } 1021 } 1022 if (foundFocus) { 1023 break; 1024 } 1025 for (int i = 0; i < count; i++){ 1026 child = getChildAt(lastExpandableIndex() - 1); 1027 mExpandableChildStates.saveInvisibleView(child, mRightIndex - 1); 1028 removeViewInLayout(child); 1029 recycleExpandableView(child); 1030 mRightIndex--; 1031 } 1032 } 1033 } 1034 } 1035 1036 /** check if expandable view or related expanded view has focus */ 1037 private boolean childHasFocus(int expandableViewIndex) { 1038 View child = getChildAt(expandableViewIndex); 1039 if (child.hasFocus()) { 1040 return true; 1041 } 1042 ExpandedView v = findExpandedView(mExpandedViews, getAdapterIndex(expandableViewIndex)); 1043 if (v != null && v.expandedView.hasFocus()) { 1044 return true; 1045 } 1046 return false; 1047 } 1048 1049 /** 1050 * @param gridSetting <br> 1051 * {@link #GRID_SETTING_SINGLE}: single item on second axis, i.e. not a grid view <br> 1052 * {@link #GRID_SETTING_AUTO}: auto calculate number of items on second axis <br> 1053 * >1: shown as a grid view, with given fixed number of items on second axis <br> 1054 */ 1055 public void setGridSetting(int gridSetting) { 1056 mGridSetting = gridSetting; 1057 requestLayout(); 1058 } 1059 1060 public int getGridSetting() { 1061 return mGridSetting; 1062 } 1063 1064 private void fillVisibleViewsInLayout() { 1065 while (fillOneRightChildView(true)) { 1066 } 1067 while (fillOneLeftChildView(true)) { 1068 } 1069 if (mRightIndex >= 0 && mLeftIndex == -1) { 1070 // first child available 1071 View child = getChildAt(firstExpandableIndex()); 1072 int scrollCenter = getScrollCenter(child); 1073 mScroll.mainAxis().updateScrollMin(scrollCenter, getScrollLow(scrollCenter, child)); 1074 } else { 1075 mScroll.mainAxis().invalidateScrollMin(); 1076 } 1077 if (mRightIndex == mAdapter.getCount()) { 1078 // last child available 1079 View child = getChildAt(lastExpandableIndex() - 1); 1080 int scrollCenter = getScrollCenter(child); 1081 mScroll.mainAxis().updateScrollMax(scrollCenter, getScrollHigh(scrollCenter, child)); 1082 } else { 1083 mScroll.mainAxis().invalidateScrollMax(); 1084 } 1085 } 1086 1087 /** 1088 * try to add one left/top child view, returning false tells caller can stop loop 1089 */ 1090 private boolean fillOneLeftChildView(boolean stopOnInvisible) { 1091 // 1. check if we still need add view 1092 if (mLeftIndex < 0) { 1093 return false; 1094 } 1095 int left = Integer.MAX_VALUE; 1096 int top = Integer.MAX_VALUE; 1097 if (lastExpandableIndex() - firstExpandableIndex() > 0) { 1098 int childIndex = firstExpandableIndex(); 1099 int last = Math.min(lastExpandableIndex(), childIndex + mItemsOnOffAxis); 1100 for (int i = childIndex; i < last; i++) { 1101 View v = getChildAt(i); 1102 if (mOrientation == HORIZONTAL) { 1103 if (v.getLeft() < left) { 1104 left = v.getLeft(); 1105 } 1106 } else { 1107 if (v.getTop() < top) { 1108 top = v.getTop(); 1109 } 1110 } 1111 } 1112 boolean itemInvisible; 1113 if (mOrientation == HORIZONTAL) { 1114 left -= mSpace; 1115 itemInvisible = left - getScrollX() <= 0; 1116 top = getPaddingTop(); 1117 } else { 1118 top -= mSpace; 1119 itemInvisible = top - getScrollY() <= 0; 1120 left = getPaddingLeft(); 1121 } 1122 if (itemInvisible && stopOnInvisible) { 1123 return false; 1124 } 1125 } else { 1126 return false; 1127 } 1128 // 2. create view and layout 1129 return fillOneAxis(left, top, false, true); 1130 } 1131 1132 private View addAndMeasureExpandableView(int adapterIndex, int insertIndex) { 1133 int type = mAdapter.getItemViewType(adapterIndex); 1134 View recycleView = mRecycleViews.getView(type); 1135 View child = mAdapter.getView(adapterIndex, recycleView, this); 1136 if (child == null) { 1137 return null; 1138 } 1139 child.setTag(R.id.ScrollAdapterViewChild, new ChildViewHolder(type)); 1140 addViewInLayout(child, insertIndex, child.getLayoutParams(), true); 1141 measureChild(child); 1142 return child; 1143 } 1144 1145 private void measureScrapChild(View child, int widthMeasureSpec, int heightMeasureSpec) { 1146 LayoutParams p = child.getLayoutParams(); 1147 if (p == null) { 1148 p = generateDefaultLayoutParams(); 1149 child.setLayoutParams(p); 1150 } 1151 1152 int childWidthSpec, childHeightSpec; 1153 if (mOrientation == VERTICAL) { 1154 childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, 0, p.width); 1155 int lpHeight = p.height; 1156 if (lpHeight > 0) { 1157 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 1158 } else { 1159 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1160 } 1161 } else { 1162 childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec, 0, p.height); 1163 int lpWidth = p.width; 1164 if (lpWidth > 0) { 1165 childWidthSpec = MeasureSpec.makeMeasureSpec(lpWidth, MeasureSpec.EXACTLY); 1166 } else { 1167 childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1168 } 1169 } 1170 child.measure(childWidthSpec, childHeightSpec); 1171 } 1172 1173 @Override 1174 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1175 if (mAdapter == null) { 1176 Log.e(TAG, "onMeasure: Adapter not available "); 1177 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1178 return; 1179 } 1180 mScroll.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 1181 mScroll.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 1182 1183 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1184 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1185 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1186 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1187 int clientWidthSize = widthSize - getPaddingLeft() - getPaddingRight(); 1188 int clientHeightSize = heightSize - getPaddingTop() - getPaddingBottom(); 1189 1190 if (mMeasuredSpec == -1) { 1191 View scrapView = mAdapter.getScrapView(this); 1192 measureScrapChild(scrapView, MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1193 mScrapWidth = scrapView.getMeasuredWidth(); 1194 mScrapHeight = scrapView.getMeasuredHeight(); 1195 } 1196 1197 mItemsOnOffAxis = mGridSetting > 0 ? mGridSetting 1198 : mOrientation == HORIZONTAL ? 1199 (heightMode == MeasureSpec.UNSPECIFIED ? 1 : clientHeightSize / mScrapHeight) 1200 : (widthMode == MeasureSpec.UNSPECIFIED ? 1 : clientWidthSize / mScrapWidth); 1201 if (mItemsOnOffAxis == 0) { 1202 mItemsOnOffAxis = 1; 1203 } 1204 1205 if (mLoadingState != null && mItemsOnOffAxis != mLoadingState.itemsOnOffAxis) { 1206 mLoadingState = null; 1207 } 1208 1209 // see table below "height handling" 1210 if (widthMode == MeasureSpec.UNSPECIFIED || 1211 (widthMode == MeasureSpec.AT_MOST && mOrientation == VERTICAL)) { 1212 int size = mOrientation == VERTICAL ? mScrapWidth * mItemsOnOffAxis 1213 + mSpace * (mItemsOnOffAxis - 1) : mScrapWidth; 1214 size += getPaddingLeft() + getPaddingRight(); 1215 widthSize = widthMode == MeasureSpec.AT_MOST ? Math.min(size, widthSize) : size; 1216 } 1217 // table of height handling 1218 // heightMode: UNSPECIFIED AT_MOST EXACTLY 1219 // HOROZINTAL items*childHeight min(items * childHeight, height) height 1220 // VERTICAL childHeight height height 1221 if (heightMode == MeasureSpec.UNSPECIFIED || 1222 (heightMode == MeasureSpec.AT_MOST && mOrientation == HORIZONTAL)) { 1223 int size = mOrientation == HORIZONTAL ? 1224 mScrapHeight * mItemsOnOffAxis + mSpace * (mItemsOnOffAxis - 1) : mScrapHeight; 1225 size += getPaddingTop() + getPaddingBottom(); 1226 heightSize = heightMode == MeasureSpec.AT_MOST ? Math.min(size, heightSize) : size; 1227 } 1228 mMeasuredSpec = mOrientation == HORIZONTAL ? heightMeasureSpec : widthMeasureSpec; 1229 1230 setMeasuredDimension(widthSize, heightSize); 1231 1232 // we allow scroll from padding low to padding high in the second axis 1233 int scrollMin = mScroll.secondAxis().getPaddingLow(); 1234 int scrollMax = (mOrientation == HORIZONTAL ? heightSize : widthSize) - 1235 mScroll.secondAxis().getPaddingHigh(); 1236 mScroll.secondAxis().updateScrollMin(scrollMin, scrollMin); 1237 mScroll.secondAxis().updateScrollMax(scrollMax, scrollMax); 1238 1239 for (int j = 0, size = mExpandedViews.size(); j < size; j++) { 1240 ExpandedView v = mExpandedViews.get(j); 1241 measureChild(v.expandedView); 1242 } 1243 1244 for (int i = firstExpandableIndex(); i < lastExpandableIndex(); i++) { 1245 View v = getChildAt(i); 1246 if (v.isLayoutRequested()) { 1247 measureChild(v); 1248 } 1249 } 1250 } 1251 1252 /** 1253 * override to draw from two sides, center item is draw at last 1254 */ 1255 @Override 1256 protected int getChildDrawingOrder(int childCount, int i) { 1257 int minDistance = Integer.MAX_VALUE; 1258 int focusIndex = mSelectedIndex < 0 ? -1 : 1259 expandableIndexFromAdapterIndex(mSelectedIndex); 1260 if (focusIndex < 0) { 1261 return i; 1262 } 1263 // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item 1264 // drawing order is 0 1 2 3 9 8 7 6 5 4 1265 if (i < focusIndex) { 1266 return i; 1267 } else if (i < childCount - 1) { 1268 return focusIndex + childCount - 1 - i; 1269 } else { 1270 return focusIndex; 1271 } 1272 } 1273 1274 /** 1275 * fill one off-axis views, the left/top of main axis will be interpreted as right/bottom if 1276 * leftToRight is false 1277 */ 1278 private boolean fillOneAxis(int left, int top, boolean leftToRight, boolean setInitialPos) { 1279 // 2. create view and layout 1280 int viewIndex = lastExpandableIndex(); 1281 int itemsToAdd = leftToRight ? Math.min(mItemsOnOffAxis, mAdapter.getCount() - mRightIndex) 1282 : mItemsOnOffAxis; 1283 int maxSize = 0; 1284 int maxSelectedSize = 0; 1285 for (int i = 0; i < itemsToAdd; i++) { 1286 View child = leftToRight ? addAndMeasureExpandableView(mRightIndex + i, -1) : 1287 addAndMeasureExpandableView(mLeftIndex - i, firstExpandableIndex()); 1288 if (child == null) { 1289 return false; 1290 } 1291 maxSize = Math.max(maxSize, mOrientation == HORIZONTAL ? child.getMeasuredWidth() : 1292 child.getMeasuredHeight()); 1293 maxSelectedSize = Math.max( 1294 maxSelectedSize, getSelectedItemSize(mLeftIndex - i, child)); 1295 } 1296 if (!leftToRight) { 1297 viewIndex = firstExpandableIndex(); 1298 if (mOrientation == HORIZONTAL) { 1299 left = left - maxSize; 1300 } else { 1301 top = top - maxSize; 1302 } 1303 } 1304 for (int i = 0; i < itemsToAdd; i++) { 1305 View child = getChildAt(viewIndex + i); 1306 ChildViewHolder h = (ChildViewHolder) child.getTag(R.id.ScrollAdapterViewChild); 1307 h.mMaxSize = maxSize; 1308 if (mOrientation == HORIZONTAL) { 1309 switch (mScroll.getScrollItemAlign()) { 1310 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 1311 child.layout(left + maxSize / 2 - child.getMeasuredWidth() / 2, top, 1312 left + maxSize / 2 + child.getMeasuredWidth() / 2, 1313 top + child.getMeasuredHeight()); 1314 break; 1315 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 1316 child.layout(left, top, left + child.getMeasuredWidth(), 1317 top + child.getMeasuredHeight()); 1318 break; 1319 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 1320 child.layout(left + maxSize - child.getMeasuredWidth(), top, left + maxSize, 1321 top + child.getMeasuredHeight()); 1322 break; 1323 } 1324 top += child.getMeasuredHeight(); 1325 top += mSpace; 1326 } else { 1327 switch (mScroll.getScrollItemAlign()) { 1328 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 1329 child.layout(left, top + maxSize / 2 - child.getMeasuredHeight() / 2, 1330 left + child.getMeasuredWidth(), 1331 top + maxSize / 2 + child.getMeasuredHeight() / 2); 1332 break; 1333 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 1334 child.layout(left, top, left + child.getMeasuredWidth(), 1335 top + child.getMeasuredHeight()); 1336 break; 1337 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 1338 child.layout(left, top + maxSize - child.getMeasuredHeight(), 1339 left + getMeasuredWidth(), top + maxSize); 1340 break; 1341 } 1342 left += child.getMeasuredWidth(); 1343 left += mSpace; 1344 } 1345 if (leftToRight) { 1346 mExpandableChildStates.loadView(child, mRightIndex); 1347 mRightIndex++; 1348 } else { 1349 mExpandableChildStates.loadView(child, mLeftIndex); 1350 mLeftIndex--; 1351 } 1352 h.mScrollCenter = computeScrollCenter(viewIndex + i); 1353 if (setInitialPos && leftToRight && 1354 mAdapter.isEnabled(mRightIndex - 1) && !mMadeInitialSelection) { 1355 // this is the first child being added 1356 int centerMain = getScrollCenter(child); 1357 int centerSecond = getCenterInOffAxis(child); 1358 if (mOrientation == HORIZONTAL) { 1359 mScroll.setScrollCenter(centerMain, centerSecond); 1360 } else { 1361 mScroll.setScrollCenter(centerSecond, centerMain); 1362 } 1363 mMadeInitialSelection = true; 1364 transferFocusTo(child, 0); 1365 } 1366 } 1367 return true; 1368 } 1369 /** 1370 * try to add one right/bottom child views, returning false tells caller can stop loop 1371 */ 1372 private boolean fillOneRightChildView(boolean stopOnInvisible) { 1373 // 1. check if we still need add view 1374 if (mRightIndex >= mAdapter.getCount()) { 1375 return false; 1376 } 1377 int left = getPaddingLeft(); 1378 int top = getPaddingTop(); 1379 boolean checkedChild = false; 1380 if (lastExpandableIndex() - firstExpandableIndex() > 0) { 1381 // position of new view should starts from the last child or expanded view of last 1382 // child if it exists 1383 int childIndex = lastExpandableIndex() - 1; 1384 int gridPos = getAdapterIndex(childIndex) % mItemsOnOffAxis; 1385 for (int i = childIndex - gridPos; i < lastExpandableIndex(); i++) { 1386 View v = getChildAt(i); 1387 int adapterIndex = getAdapterIndex(i); 1388 ExpandedView expandedView = findExpandedView(mExpandedViews, adapterIndex); 1389 if (expandedView != null) { 1390 if (mOrientation == HORIZONTAL) { 1391 left = expandedView.expandedView.getRight(); 1392 } else { 1393 top = expandedView.expandedView.getBottom(); 1394 } 1395 checkedChild = true; 1396 break; 1397 } 1398 if (mOrientation == HORIZONTAL) { 1399 if (!checkedChild) { 1400 checkedChild = true; 1401 left = v.getRight(); 1402 } else if (v.getRight() > left) { 1403 left = v.getRight(); 1404 } 1405 } else { 1406 if (!checkedChild) { 1407 checkedChild = true; 1408 top = v.getBottom(); 1409 } else if (v.getBottom() > top) { 1410 top = v.getBottom(); 1411 } 1412 } 1413 } 1414 boolean itemInvisible; 1415 if (mOrientation == HORIZONTAL) { 1416 left += mSpace; 1417 itemInvisible = left - getScrollX() >= getWidth(); 1418 top = getPaddingTop(); 1419 } else { 1420 top += mSpace; 1421 itemInvisible = top - getScrollY() >= getHeight(); 1422 left = getPaddingLeft(); 1423 } 1424 if (itemInvisible && stopOnInvisible) { 1425 return false; 1426 } 1427 } 1428 // 2. create view and layout 1429 return fillOneAxis(left, top, true, true); 1430 } 1431 1432 private int heuristicGetPersistentIndex() { 1433 int selection = -1; 1434 int c = mAdapter.getCount(); 1435 if (mScrollBeforeReset.id != INVALID_ROW_ID) { 1436 if (mScrollBeforeReset.index < c 1437 && mAdapter.getItemId(mScrollBeforeReset.index) == mScrollBeforeReset.id) { 1438 return mScrollBeforeReset.index; 1439 } 1440 for (int i = 1; i <= SEARCH_ID_RANGE; i++) { 1441 int index = mScrollBeforeReset.index + i; 1442 if (index < c && mAdapter.getItemId(index) == mScrollBeforeReset.id) { 1443 return index; 1444 } 1445 index = mScrollBeforeReset.index - i; 1446 if (index >=0 && index < c && mAdapter.getItemId(index) == mScrollBeforeReset.id) { 1447 return index; 1448 } 1449 } 1450 } 1451 return mScrollBeforeReset.index >= c ? c - 1 : mScrollBeforeReset.index; 1452 } 1453 1454 private void restoreLoadingState() { 1455 int selection; 1456 int viewLoc = Integer.MIN_VALUE; 1457 float scrollPosition = 0f; 1458 int fillWindowLeft = -1; 1459 int fillWindowRight = -1; 1460 boolean hasFocus = hasFocus(); 1461 int centerX = 0, centerY = 0; 1462 Bundle expandableChildStates = null; 1463 Bundle expandedChildStates = null; 1464 if (mPendingSelection >= 0) { 1465 // got setSelection calls 1466 selection = mPendingSelection; 1467 scrollPosition = mPendingScrollPosition; 1468 } else if (mScrollBeforeReset.isValid()) { 1469 // data was refreshed, try to recover where we were 1470 selection = heuristicGetPersistentIndex(); 1471 viewLoc = mScrollBeforeReset.viewLocation; 1472 } else if (mLoadingState != null) { 1473 // scrollAdapterView is restoring from loading state 1474 selection = mLoadingState.index; 1475 expandableChildStates = mLoadingState.expandableChildStates; 1476 expandedChildStates = mLoadingState.expandedChildStates; 1477 } else { 1478 return; 1479 } 1480 mPendingSelection = -1; 1481 mScrollBeforeReset.clear(); 1482 mLoadingState = null; 1483 if (selection < 0 || selection >= mAdapter.getCount()) { 1484 Log.w(TAG, "invalid selection "+selection); 1485 return; 1486 } 1487 1488 // startIndex is the first child in the same offAxis of selection 1489 // We add this view first because we don't know "selection" position in offAxis 1490 int startIndex = selection - selection % mItemsOnOffAxis; 1491 int left, top; 1492 if (mOrientation == HORIZONTAL) { 1493 // estimation of left 1494 left = viewLoc != Integer.MIN_VALUE ? viewLoc: mScroll.horizontal.getPaddingLow() 1495 + mScrapWidth * (selection / mItemsOnOffAxis); 1496 top = mScroll.vertical.getPaddingLow(); 1497 } else { 1498 left = mScroll.horizontal.getPaddingLow(); 1499 // estimation of top 1500 top = viewLoc != Integer.MIN_VALUE ? viewLoc: mScroll.vertical.getPaddingLow() 1501 + mScrapHeight * (selection / mItemsOnOffAxis); 1502 } 1503 mRightIndex = startIndex; 1504 mLeftIndex = mRightIndex - 1; 1505 fillOneAxis(left, top, true, false); 1506 mMadeInitialSelection = true; 1507 // fill all views, should include the "selection" view 1508 fillVisibleViewsInLayout(); 1509 View child = getExpandableView(selection); 1510 if (child == null) { 1511 Log.w(TAG, "unable to restore selection view"); 1512 return; 1513 } 1514 mExpandableChildStates.loadView(child, selection); 1515 if (viewLoc != Integer.MIN_VALUE && mScrollerState == SCROLL_AND_CENTER_FOCUS) { 1516 // continue scroll animation but since the views and sizes might change, we need 1517 // update the scrolling final target 1518 int finalLocation = (mOrientation == HORIZONTAL) ? mScroll.getFinalX() : 1519 mScroll.getFinalY(); 1520 mSelectedIndex = getAdapterIndex(indexOfChild(child)); 1521 int scrollCenter = getScrollCenter(child); 1522 if (mScroll.mainAxis().getScrollCenter() <= finalLocation) { 1523 while (scrollCenter < finalLocation) { 1524 int nextAdapterIndex = mSelectedIndex + mItemsOnOffAxis; 1525 View nextView = getExpandableView(nextAdapterIndex); 1526 if (nextView == null) { 1527 if (!fillOneRightChildView(false)) { 1528 break; 1529 } 1530 nextView = getExpandableView(nextAdapterIndex); 1531 } 1532 int nextScrollCenter = getScrollCenter(nextView); 1533 if (nextScrollCenter > finalLocation) { 1534 break; 1535 } 1536 mSelectedIndex = nextAdapterIndex; 1537 scrollCenter = nextScrollCenter; 1538 } 1539 } else { 1540 while (scrollCenter > finalLocation) { 1541 int nextAdapterIndex = mSelectedIndex - mItemsOnOffAxis; 1542 View nextView = getExpandableView(nextAdapterIndex); 1543 if (nextView == null) { 1544 if (!fillOneLeftChildView(false)) { 1545 break; 1546 } 1547 nextView = getExpandableView(nextAdapterIndex); 1548 } 1549 int nextScrollCenter = getScrollCenter(nextView); 1550 if (nextScrollCenter < finalLocation) { 1551 break; 1552 } 1553 mSelectedIndex = nextAdapterIndex; 1554 scrollCenter = nextScrollCenter; 1555 } 1556 } 1557 if (mOrientation == HORIZONTAL) { 1558 mScroll.setFinalX(scrollCenter); 1559 } else { 1560 mScroll.setFinalY(scrollCenter); 1561 } 1562 } else { 1563 // otherwise center focus to the view and stop animation 1564 setSelectionInternal(selection, scrollPosition, false); 1565 } 1566 } 1567 1568 private void measureChild(View child) { 1569 LayoutParams p = child.getLayoutParams(); 1570 if (p == null) { 1571 p = generateDefaultLayoutParams(); 1572 child.setLayoutParams(p); 1573 } 1574 if (mOrientation == VERTICAL) { 1575 int childWidthSpec = ViewGroup.getChildMeasureSpec(mMeasuredSpec, 0, p.width); 1576 int lpHeight = p.height; 1577 int childHeightSpec; 1578 if (lpHeight > 0) { 1579 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 1580 } else { 1581 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1582 } 1583 child.measure(childWidthSpec, childHeightSpec); 1584 } else { 1585 int childHeightSpec = ViewGroup.getChildMeasureSpec(mMeasuredSpec, 0, p.height); 1586 int lpWidth = p.width; 1587 int childWidthSpec; 1588 if (lpWidth > 0) { 1589 childWidthSpec = MeasureSpec.makeMeasureSpec(lpWidth, MeasureSpec.EXACTLY); 1590 } else { 1591 childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1592 } 1593 child.measure(childWidthSpec, childHeightSpec); 1594 } 1595 } 1596 1597 @Override 1598 public boolean dispatchKeyEvent(KeyEvent event) { 1599 // passing key event to focused child, which has chance to stop event processing by 1600 // returning true. 1601 // If child does not handle the event, we handle DPAD etc. 1602 return super.dispatchKeyEvent(event) || event.dispatch(this, null, null); 1603 } 1604 1605 protected boolean internalKeyDown(int keyCode, KeyEvent event) { 1606 switch (keyCode) { 1607 case KeyEvent.KEYCODE_DPAD_LEFT: 1608 if (handleArrowKey(View.FOCUS_LEFT, 0, false, false)) { 1609 return true; 1610 } 1611 break; 1612 case KeyEvent.KEYCODE_DPAD_RIGHT: 1613 if (handleArrowKey(View.FOCUS_RIGHT, 0, false, false)) { 1614 return true; 1615 } 1616 break; 1617 case KeyEvent.KEYCODE_DPAD_UP: 1618 if (handleArrowKey(View.FOCUS_UP, 0, false, false)) { 1619 return true; 1620 } 1621 break; 1622 case KeyEvent.KEYCODE_DPAD_DOWN: 1623 if (handleArrowKey(View.FOCUS_DOWN, 0, false, false)) { 1624 return true; 1625 } 1626 break; 1627 } 1628 return super.onKeyDown(keyCode, event); 1629 } 1630 1631 @Override 1632 public boolean onKeyDown(int keyCode, KeyEvent event) { 1633 return internalKeyDown(keyCode, event); 1634 } 1635 1636 @Override 1637 public boolean onKeyUp(int keyCode, KeyEvent event) { 1638 switch (keyCode) { 1639 case KeyEvent.KEYCODE_DPAD_CENTER: 1640 case KeyEvent.KEYCODE_ENTER: 1641 if (getOnItemClickListener() != null) { 1642 int index = findViewIndexContainingScrollCenter(); 1643 View child = getChildAt(index); 1644 if (child != null) { 1645 int adapterIndex = getAdapterIndex(index); 1646 getOnItemClickListener().onItemClick(this, child, 1647 adapterIndex, mAdapter.getItemId(adapterIndex)); 1648 return true; 1649 } 1650 } 1651 // otherwise fall back to default handling, typically handled by 1652 // the focused child view 1653 break; 1654 } 1655 return super.onKeyUp(keyCode, event); 1656 } 1657 1658 /** 1659 * Scroll to next/last expandable view. 1660 * @param direction The direction corresponding to the arrow key that was pressed 1661 * @param repeats repeated count (0 means no repeat) 1662 * @return True if we consumed the event, false otherwise 1663 */ 1664 public boolean arrowScroll(int direction, int repeats) { 1665 if (DBG) Log.d(TAG, "arrowScroll " + direction); 1666 return handleArrowKey(direction, repeats, true, false); 1667 } 1668 1669 /** equivalent to arrowScroll(direction, 0) */ 1670 public boolean arrowScroll(int direction) { 1671 return arrowScroll(direction, 0); 1672 } 1673 1674 public boolean isInScrolling() { 1675 return !mScroll.isFinished(); 1676 } 1677 1678 public boolean isInScrollingOrDragging() { 1679 return mScrollerState != NO_SCROLL; 1680 } 1681 1682 public void setPlaySoundEffects(boolean playSoundEffects) { 1683 mPlaySoundEffects = playSoundEffects; 1684 } 1685 1686 private static boolean isDirectionGrowing(int direction) { 1687 return direction == View.FOCUS_RIGHT || direction == View.FOCUS_DOWN; 1688 } 1689 1690 private static boolean isDescendant(View parent, View v) { 1691 while (v != null) { 1692 ViewParent p = v.getParent(); 1693 if (p == parent) { 1694 return true; 1695 } 1696 if (!(p instanceof View)) { 1697 return false; 1698 } 1699 v = (View) p; 1700 } 1701 return false; 1702 } 1703 1704 private boolean requestNextFocus(int direction, View focused, View newFocus) { 1705 focused.getFocusedRect(mTempRect); 1706 offsetDescendantRectToMyCoords(focused, mTempRect); 1707 offsetRectIntoDescendantCoords(newFocus, mTempRect); 1708 return newFocus.requestFocus(direction, mTempRect); 1709 } 1710 1711 protected boolean handleArrowKey(int direction, int repeats, boolean forceFindNextExpandable, 1712 boolean page) { 1713 View currentTop = getFocusedChild(); 1714 View currentExpandable = getExpandableChild(currentTop); 1715 View focused = findFocus(); 1716 if (currentTop == currentExpandable && focused != null && !forceFindNextExpandable) { 1717 // find next focused inside expandable item 1718 View v = focused.focusSearch(direction); 1719 if (v != null && v != focused && isDescendant(currentTop, v)) { 1720 requestNextFocus(direction, focused, v); 1721 return true; 1722 } 1723 } 1724 boolean isGrowing = isDirectionGrowing(direction); 1725 boolean isOnOffAxis = false; 1726 if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) { 1727 isOnOffAxis = mOrientation == VERTICAL; 1728 } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) { 1729 isOnOffAxis = mOrientation == HORIZONTAL; 1730 } 1731 1732 if (currentTop != currentExpandable && !forceFindNextExpandable) { 1733 // find next focused inside expanded item 1734 View nextFocused = currentTop instanceof ViewGroup ? FocusFinder.getInstance() 1735 .findNextFocus((ViewGroup) currentTop, findFocus(), direction) 1736 : null; 1737 View nextTop = getTopItem(nextFocused); 1738 if (nextTop == currentTop) { 1739 // within same expanded item 1740 // ignore at this level, the key handler of expanded item will take care 1741 return false; 1742 } 1743 } 1744 1745 // focus to next expandable item 1746 int currentExpandableIndex = expandableIndexFromAdapterIndex(mSelectedIndex); 1747 if (currentExpandableIndex < 0) { 1748 return false; 1749 } 1750 View nextTop = null; 1751 if (isOnOffAxis) { 1752 if (isGrowing && currentExpandableIndex + 1 < lastExpandableIndex() && 1753 getAdapterIndex(currentExpandableIndex) % mItemsOnOffAxis 1754 != mItemsOnOffAxis - 1) { 1755 nextTop = getChildAt(currentExpandableIndex + 1); 1756 } else if (!isGrowing && currentExpandableIndex - 1 >= firstExpandableIndex() 1757 && getAdapterIndex(currentExpandableIndex) % mItemsOnOffAxis != 0) { 1758 nextTop = getChildAt(currentExpandableIndex - 1); 1759 } else { 1760 return !mNavigateOutOfOffAxisAllowed; 1761 } 1762 } else { 1763 int adapterIndex = getAdapterIndex(currentExpandableIndex); 1764 int focusAdapterIndex = adapterIndex; 1765 for (int totalCount = repeats + 1; totalCount > 0;) { 1766 int nextFocusAdapterIndex = isGrowing ? focusAdapterIndex + mItemsOnOffAxis: 1767 focusAdapterIndex - mItemsOnOffAxis; 1768 if ((isGrowing && nextFocusAdapterIndex >= mAdapter.getCount()) 1769 || (!isGrowing && nextFocusAdapterIndex < 0)) { 1770 if (focusAdapterIndex == adapterIndex 1771 || !mAdapter.isEnabled(focusAdapterIndex)) { 1772 if (hasFocus() && mNavigateOutAllowed) { 1773 View view = getChildAt( 1774 expandableIndexFromAdapterIndex(focusAdapterIndex)); 1775 if (view != null && !view.hasFocus()) { 1776 view.requestFocus(); 1777 } 1778 } 1779 return !mNavigateOutAllowed; 1780 } else { 1781 break; 1782 } 1783 } 1784 focusAdapterIndex = nextFocusAdapterIndex; 1785 if (mAdapter.isEnabled(focusAdapterIndex)) { 1786 totalCount--; 1787 } 1788 } 1789 if (isGrowing) { 1790 do { 1791 if (focusAdapterIndex <= getAdapterIndex(lastExpandableIndex() - 1)) { 1792 nextTop = getChildAt(expandableIndexFromAdapterIndex(focusAdapterIndex)); 1793 break; 1794 } 1795 } while (fillOneRightChildView(false)); 1796 if (nextTop == null) { 1797 nextTop = getChildAt(lastExpandableIndex() - 1); 1798 } 1799 } else { 1800 do { 1801 if (focusAdapterIndex >= getAdapterIndex(firstExpandableIndex())) { 1802 nextTop = getChildAt(expandableIndexFromAdapterIndex(focusAdapterIndex)); 1803 break; 1804 } 1805 } while (fillOneLeftChildView(false)); 1806 if (nextTop == null) { 1807 nextTop = getChildAt(firstExpandableIndex()); 1808 } 1809 } 1810 if (nextTop == null) { 1811 return true; 1812 } 1813 } 1814 scrollAndFocusTo(nextTop, direction, false, 0, page); 1815 if (mPlaySoundEffects) { 1816 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1817 } 1818 return true; 1819 } 1820 1821 private void fireItemChange() { 1822 int childIndex = findViewIndexContainingScrollCenter(); 1823 View topItem = getChildAt(childIndex); 1824 if (isFocused() && getDescendantFocusability() == FOCUS_AFTER_DESCENDANTS 1825 && topItem != null) { 1826 // transfer focus to child for reset/restore 1827 topItem.requestFocus(); 1828 } 1829 if (mOnItemChangeListeners != null && !mOnItemChangeListeners.isEmpty()) { 1830 if (topItem == null) { 1831 if (mItemSelected != -1) { 1832 for (OnItemChangeListener listener : mOnItemChangeListeners) { 1833 listener.onItemSelected(null, -1, 0); 1834 } 1835 mItemSelected = -1; 1836 } 1837 } else { 1838 int adapterIndex = getAdapterIndex(childIndex); 1839 int scrollCenter = getScrollCenter(topItem); 1840 for (OnItemChangeListener listener : mOnItemChangeListeners) { 1841 listener.onItemSelected(topItem, adapterIndex, scrollCenter - 1842 mScroll.mainAxis().getSystemScrollPos(scrollCenter)); 1843 } 1844 mItemSelected = adapterIndex; 1845 } 1846 } 1847 1848 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1849 } 1850 1851 private void updateScrollInfo(ScrollInfo info) { 1852 int scrollCenter = mScroll.mainAxis().getScrollCenter(); 1853 int scrollCenterOff = mScroll.secondAxis().getScrollCenter(); 1854 int index = findViewIndexContainingScrollCenter( 1855 scrollCenter, scrollCenterOff, false); 1856 if (index < 0) { 1857 info.index = -1; 1858 return; 1859 } 1860 View view = getChildAt(index); 1861 int center = getScrollCenter(view); 1862 if (scrollCenter > center) { 1863 if (index + mItemsOnOffAxis < lastExpandableIndex()) { 1864 int nextCenter = getScrollCenter(getChildAt(index + mItemsOnOffAxis)); 1865 info.mainPos = (float)(scrollCenter - center) / (nextCenter - center); 1866 } else { 1867 // overscroll to right 1868 info.mainPos = (float)(scrollCenter - center) / getSize(view); 1869 } 1870 } else if (scrollCenter == center){ 1871 info.mainPos = 0; 1872 } else { 1873 if (index - mItemsOnOffAxis >= firstExpandableIndex()) { 1874 index = index - mItemsOnOffAxis; 1875 view = getChildAt(index); 1876 int previousCenter = getScrollCenter(view); 1877 info.mainPos = (float) (scrollCenter - previousCenter) / 1878 (center - previousCenter); 1879 } else { 1880 // overscroll to left, negative value 1881 info.mainPos = (float) (scrollCenter - center) / getSize(view); 1882 } 1883 } 1884 int centerOffAxis = getCenterInOffAxis(view); 1885 if (scrollCenterOff > centerOffAxis) { 1886 if (index + 1 < lastExpandableIndex()) { 1887 int nextCenter = getCenterInOffAxis(getChildAt(index + 1)); 1888 info.secondPos = (float) (scrollCenterOff - centerOffAxis) 1889 / (nextCenter - centerOffAxis); 1890 } else { 1891 // overscroll to right 1892 info.secondPos = (float) (scrollCenterOff - centerOffAxis) / 1893 getSizeInOffAxis(view); 1894 } 1895 } else if (scrollCenterOff == centerOffAxis) { 1896 info.secondPos = 0; 1897 } else { 1898 if (index - 1 >= firstExpandableIndex()) { 1899 index = index - 1; 1900 view = getChildAt(index); 1901 int previousCenter = getCenterInOffAxis(view); 1902 info.secondPos = (float) (scrollCenterOff - previousCenter) 1903 / (centerOffAxis - previousCenter); 1904 } else { 1905 // overscroll to left, negative value 1906 info.secondPos = (float) (scrollCenterOff - centerOffAxis) / 1907 getSizeInOffAxis(view); 1908 } 1909 } 1910 info.index = getAdapterIndex(index); 1911 info.viewLocation = mOrientation == HORIZONTAL ? view.getLeft() : view.getTop(); 1912 if (mAdapter.hasStableIds()) { 1913 info.id = mAdapter.getItemId(info.index); 1914 } 1915 } 1916 1917 private void fireScrollChange() { 1918 int savedIndex = mCurScroll.index; 1919 float savedMainPos = mCurScroll.mainPos; 1920 float savedSecondPos = mCurScroll.secondPos; 1921 updateScrollInfo(mCurScroll); 1922 if (mOnScrollListeners != null && !mOnScrollListeners.isEmpty() 1923 &&(savedIndex != mCurScroll.index 1924 || savedMainPos != mCurScroll.mainPos || savedSecondPos != mCurScroll.secondPos)) { 1925 if (mCurScroll.index >= 0) { 1926 for (OnScrollListener l : mOnScrollListeners) { 1927 l.onScrolled(getChildAt(expandableIndexFromAdapterIndex( 1928 mCurScroll.index)), mCurScroll.index, 1929 mCurScroll.mainPos, mCurScroll.secondPos); 1930 } 1931 } 1932 } 1933 } 1934 1935 private void fireItemSelected() { 1936 OnItemSelectedListener listener = getOnItemSelectedListener(); 1937 if (listener != null) { 1938 listener.onItemSelected(this, getSelectedView(), getSelectedItemPosition(), 1939 getSelectedItemId()); 1940 } 1941 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1942 } 1943 1944 /** manually set scroll position */ 1945 private void setSelectionInternal(int adapterIndex, float scrollPosition, boolean fireEvent) { 1946 if (adapterIndex < 0 || mAdapter == null || adapterIndex >= mAdapter.getCount() 1947 || !mAdapter.isEnabled(adapterIndex)) { 1948 Log.w(TAG, "invalid selection index = " + adapterIndex); 1949 return; 1950 } 1951 int viewIndex = expandableIndexFromAdapterIndex(adapterIndex); 1952 if (mDataSetChangedFlag || viewIndex < firstExpandableIndex() || 1953 viewIndex >= lastExpandableIndex()) { 1954 mPendingSelection = adapterIndex; 1955 mPendingScrollPosition = scrollPosition; 1956 fireDataSetChanged(); 1957 return; 1958 } 1959 View view = getChildAt(viewIndex); 1960 int scrollCenter = getScrollCenter(view); 1961 int scrollCenterOffAxis = getCenterInOffAxis(view); 1962 int deltaMain; 1963 if (scrollPosition > 0 && viewIndex + mItemsOnOffAxis < lastExpandableIndex()) { 1964 int nextCenter = getScrollCenter(getChildAt(viewIndex + mItemsOnOffAxis)); 1965 deltaMain = (int) ((nextCenter - scrollCenter) * scrollPosition); 1966 } else { 1967 deltaMain = (int) (getSize(view) * scrollPosition); 1968 } 1969 if (mOrientation == HORIZONTAL) { 1970 mScroll.setScrollCenter(scrollCenter + deltaMain, scrollCenterOffAxis); 1971 } else { 1972 mScroll.setScrollCenter(scrollCenterOffAxis, scrollCenter + deltaMain); 1973 } 1974 transferFocusTo(view, 0); 1975 adjustSystemScrollPos(); 1976 applyTransformations(); 1977 if (fireEvent) { 1978 updateViewsLocations(false); 1979 fireScrollChange(); 1980 if (scrollPosition == 0) { 1981 fireItemChange(); 1982 } 1983 } 1984 } 1985 1986 private void transferFocusTo(View topItem, int direction) { 1987 View oldSelection = getSelectedView(); 1988 if (topItem == oldSelection) { 1989 return; 1990 } 1991 mSelectedIndex = getAdapterIndex(indexOfChild(topItem)); 1992 View focused = findFocus(); 1993 if (focused != null) { 1994 if (direction != 0) { 1995 requestNextFocus(direction, focused, topItem); 1996 } else { 1997 topItem.requestFocus(); 1998 } 1999 } 2000 fireItemSelected(); 2001 } 2002 2003 /** scroll And Focus To expandable item in the main direction */ 2004 public void scrollAndFocusTo(View topItem, int direction, boolean easeFling, int duration, 2005 boolean page) { 2006 if (topItem == null) { 2007 mScrollerState = NO_SCROLL; 2008 return; 2009 } 2010 int delta = getScrollCenter(topItem) - mScroll.mainAxis().getScrollCenter(); 2011 int deltaOffAxis = mItemsOnOffAxis == 1 ? 0 : // dont scroll 2nd axis for non-grid 2012 getCenterInOffAxis(topItem) - mScroll.secondAxis().getScrollCenter(); 2013 if (delta != 0 || deltaOffAxis != 0) { 2014 mScrollerState = SCROLL_AND_CENTER_FOCUS; 2015 mScroll.startScrollByMain(delta, deltaOffAxis, easeFling, duration, page); 2016 // Instead of waiting scrolling animation finishes, we immediately change focus. 2017 // This will cause focused item to be off center and benefit is to dealing multiple 2018 // DPAD events without waiting animation finish. 2019 } else { 2020 mScrollerState = NO_SCROLL; 2021 } 2022 2023 transferFocusTo(topItem, direction); 2024 2025 scheduleScrollTask(); 2026 } 2027 2028 public int getScrollCenterStrategy() { 2029 return mScroll.mainAxis().getScrollCenterStrategy(); 2030 } 2031 2032 public void setScrollCenterStrategy(int scrollCenterStrategy) { 2033 mScroll.mainAxis().setScrollCenterStrategy(scrollCenterStrategy); 2034 } 2035 2036 public int getScrollCenterOffset() { 2037 return mScroll.mainAxis().getScrollCenterOffset(); 2038 } 2039 2040 public void setScrollCenterOffset(int scrollCenterOffset) { 2041 mScroll.mainAxis().setScrollCenterOffset(scrollCenterOffset); 2042 } 2043 2044 public void setScrollCenterOffsetPercent(int scrollCenterOffsetPercent) { 2045 mScroll.mainAxis().setScrollCenterOffsetPercent(scrollCenterOffsetPercent); 2046 } 2047 2048 public void setItemTransform(ScrollAdapterTransform transform) { 2049 mItemTransform = transform; 2050 } 2051 2052 public ScrollAdapterTransform getItemTransform() { 2053 return mItemTransform; 2054 } 2055 2056 private void ensureSimpleItemTransform() { 2057 if (! (mItemTransform instanceof SimpleScrollAdapterTransform)) { 2058 mItemTransform = new SimpleScrollAdapterTransform(getContext()); 2059 } 2060 } 2061 2062 public void setLowItemTransform(Animator anim) { 2063 ensureSimpleItemTransform(); 2064 ((SimpleScrollAdapterTransform)mItemTransform).setLowItemTransform(anim); 2065 } 2066 2067 public void setHighItemTransform(Animator anim) { 2068 ensureSimpleItemTransform(); 2069 ((SimpleScrollAdapterTransform)mItemTransform).setHighItemTransform(anim); 2070 } 2071 2072 @Override 2073 protected float getRightFadingEdgeStrength() { 2074 if (mOrientation != HORIZONTAL || mAdapter == null || getChildCount() == 0) { 2075 return 0; 2076 } 2077 if (mRightIndex == mAdapter.getCount()) { 2078 View lastChild = getChildAt(lastExpandableIndex() - 1); 2079 int maxEdge = lastChild.getRight(); 2080 if (getScrollX() + getWidth() >= maxEdge) { 2081 return 0; 2082 } 2083 } 2084 return 1; 2085 } 2086 2087 @Override 2088 protected float getBottomFadingEdgeStrength() { 2089 if (mOrientation != HORIZONTAL || mAdapter == null || getChildCount() == 0) { 2090 return 0; 2091 } 2092 if (mRightIndex == mAdapter.getCount()) { 2093 View lastChild = getChildAt(lastExpandableIndex() - 1); 2094 int maxEdge = lastChild.getBottom(); 2095 if (getScrollY() + getHeight() >= maxEdge) { 2096 return 0; 2097 } 2098 } 2099 return 1; 2100 } 2101 2102 /** 2103 * get the view which is ancestor of "v" and immediate child of root view return "v" if 2104 * rootView is not ViewGroup or "v" is not in the subtree 2105 */ 2106 private final View getTopItem(View v) { 2107 ViewGroup root = this; 2108 View ret = v; 2109 while (ret != null && ret.getParent() != root) { 2110 if (!(ret.getParent() instanceof View)) { 2111 break; 2112 } 2113 ret = (View) ret.getParent(); 2114 } 2115 if (ret == null) { 2116 return v; 2117 } else { 2118 return ret; 2119 } 2120 } 2121 2122 private final int getCenter(View v) { 2123 return mOrientation == HORIZONTAL ? (v.getLeft() + v.getRight()) / 2 : (v.getTop() 2124 + v.getBottom()) / 2; 2125 } 2126 2127 private final int getCenterInOffAxis(View v) { 2128 return mOrientation == VERTICAL ? (v.getLeft() + v.getRight()) / 2 : (v.getTop() 2129 + v.getBottom()) / 2; 2130 } 2131 2132 private final int getSize(View v) { 2133 return ((ChildViewHolder) v.getTag(R.id.ScrollAdapterViewChild)).mMaxSize; 2134 } 2135 2136 private final int getSizeInOffAxis(View v) { 2137 return mOrientation == HORIZONTAL ? v.getHeight() : v.getWidth(); 2138 } 2139 2140 public View getExpandableView(int adapterIndex) { 2141 return getChildAt(expandableIndexFromAdapterIndex(adapterIndex)); 2142 } 2143 2144 public int firstExpandableIndex() { 2145 return mExpandedViews.size(); 2146 } 2147 2148 public int lastExpandableIndex() { 2149 return getChildCount(); 2150 } 2151 2152 private int getAdapterIndex(int expandableViewIndex) { 2153 return expandableViewIndex - firstExpandableIndex() + mLeftIndex + 1; 2154 } 2155 2156 private int expandableIndexFromAdapterIndex(int index) { 2157 return firstExpandableIndex() + index - mLeftIndex - 1; 2158 } 2159 2160 View getExpandableChild(View view) { 2161 if (view != null) { 2162 for (int i = 0, size = mExpandedViews.size(); i < size; i++) { 2163 ExpandedView v = mExpandedViews.get(i); 2164 if (v.expandedView == view) { 2165 return getChildAt(expandableIndexFromAdapterIndex(v.index)); 2166 } 2167 } 2168 } 2169 return view; 2170 } 2171 2172 private static ExpandedView findExpandedView(ArrayList<ExpandedView> expandedView, int index) { 2173 int expandedCount = expandedView.size(); 2174 for (int i = 0; i < expandedCount; i++) { 2175 ExpandedView v = expandedView.get(i); 2176 if (v.index == index) { 2177 return v; 2178 } 2179 } 2180 return null; 2181 } 2182 2183 /** 2184 * This function is only called from {@link #updateViewsLocations()} Returns existing 2185 * ExpandedView or create a new one. 2186 */ 2187 private ExpandedView getOrCreateExpandedView(int index) { 2188 if (mExpandAdapter == null || index < 0) { 2189 return null; 2190 } 2191 ExpandedView ret = findExpandedView(mExpandedViews, index); 2192 if (ret != null) { 2193 return ret; 2194 } 2195 int type = mExpandAdapter.getItemViewType(index); 2196 View recycleView = mRecycleExpandedViews.getView(type); 2197 View v = mExpandAdapter.getView(index, recycleView, ScrollAdapterView.this); 2198 if (v == null) { 2199 return null; 2200 } 2201 addViewInLayout(v, 0, v.getLayoutParams(), true); 2202 mExpandedChildStates.loadView(v, index); 2203 measureChild(v); 2204 if (DBG) Log.d(TAG, "created new expanded View for " + index + " " + v); 2205 ExpandedView view = new ExpandedView(v, index, type); 2206 for (int i = 0, size = mExpandedViews.size(); i < size; i++) { 2207 if (view.index < mExpandedViews.get(i).index) { 2208 mExpandedViews.add(i, view); 2209 return view; 2210 } 2211 } 2212 mExpandedViews.add(view); 2213 return view; 2214 } 2215 2216 public void setAnimateLayoutChange(boolean animateLayoutChange) { 2217 mAnimateLayoutChange = animateLayoutChange; 2218 } 2219 2220 public boolean getAnimateLayoutChange() { 2221 return mAnimateLayoutChange; 2222 } 2223 2224 /** 2225 * Key function to update expandable views location and create/destroy expanded views 2226 */ 2227 private void updateViewsLocations(boolean onLayout) { 2228 int lastExpandable = lastExpandableIndex(); 2229 if (((mExpandAdapter == null && !selectedItemCanScale() && mAdapterCustomAlign == null) 2230 || lastExpandable == 0) && 2231 (!onLayout || getChildCount() == 0)) { 2232 return; 2233 } 2234 2235 int scrollCenter = mScroll.mainAxis().getScrollCenter(); 2236 int scrollCenterOffAxis = mScroll.secondAxis().getScrollCenter(); 2237 // 1 search center and nextCenter that contains mScrollCenter. 2238 int expandedCount = mExpandedViews.size(); 2239 int center = -1; 2240 int nextCenter = -1; 2241 int expandIdx = -1; 2242 int firstExpandable = firstExpandableIndex(); 2243 int alignExtraOffset = 0; 2244 for (int idx = firstExpandable; idx < lastExpandable; idx++) { 2245 View view = getChildAt(idx); 2246 int centerMain = getScrollCenter(view); 2247 int centerOffAxis = getCenterInOffAxis(view); 2248 int viewSizeOffAxis = mOrientation == HORIZONTAL ? view.getHeight() : view.getWidth(); 2249 if (centerMain <= scrollCenter && (mItemsOnOffAxis == 1 || hasScrollPositionSecondAxis( 2250 scrollCenterOffAxis, viewSizeOffAxis, centerOffAxis))) { 2251 // find last one match the criteria, we can optimize it.. 2252 expandIdx = idx; 2253 center = centerMain; 2254 if (mAdapterCustomAlign != null) { 2255 alignExtraOffset = mAdapterCustomAlign.getItemAlignmentExtraOffset( 2256 getAdapterIndex(idx), view); 2257 } 2258 } 2259 } 2260 if (expandIdx == -1) { 2261 // mScrollCenter scrolls too fast, a fling action might cause this 2262 return; 2263 } 2264 int nextExpandIdx = expandIdx + mItemsOnOffAxis; 2265 int nextAlignExtraOffset = 0; 2266 if (nextExpandIdx < lastExpandable) { 2267 View nextView = getChildAt(nextExpandIdx); 2268 nextCenter = getScrollCenter(nextView); 2269 if (mAdapterCustomAlign != null) { 2270 nextAlignExtraOffset = mAdapterCustomAlign.getItemAlignmentExtraOffset( 2271 getAdapterIndex(nextExpandIdx), nextView); 2272 } 2273 } else { 2274 nextExpandIdx = -1; 2275 } 2276 int previousExpandIdx = expandIdx - mItemsOnOffAxis; 2277 if (previousExpandIdx < firstExpandable) { 2278 previousExpandIdx = -1; 2279 } 2280 2281 // 2. prepare the expanded view, they could be new created or from existing. 2282 int xindex = getAdapterIndex(expandIdx); 2283 ExpandedView thisExpanded = getOrCreateExpandedView(xindex); 2284 ExpandedView nextExpanded = null; 2285 if (nextExpandIdx != -1) { 2286 nextExpanded = getOrCreateExpandedView(xindex + mItemsOnOffAxis); 2287 } 2288 // cache one more expanded view before the visible one, it's always invisible 2289 ExpandedView previousExpanded = null; 2290 if (previousExpandIdx != -1) { 2291 previousExpanded = getOrCreateExpandedView(xindex - mItemsOnOffAxis); 2292 } 2293 2294 // these count and index needs to be updated after we inserted new views 2295 int newExpandedAdded = mExpandedViews.size() - expandedCount; 2296 expandIdx += newExpandedAdded; 2297 if (nextExpandIdx != -1) { 2298 nextExpandIdx += newExpandedAdded; 2299 } 2300 expandedCount = mExpandedViews.size(); 2301 lastExpandable = lastExpandableIndex(); 2302 2303 // 3. calculate the expanded View size, and optional next expanded view size. 2304 int expandedSize = 0; 2305 int nextExpandedSize = 0; 2306 float progress = 1; 2307 if (expandIdx < lastExpandable - 1) { 2308 progress = (float) (nextCenter - mScroll.mainAxis().getScrollCenter()) / 2309 (float) (nextCenter - center); 2310 if (thisExpanded != null) { 2311 expandedSize = 2312 (mOrientation == HORIZONTAL ? thisExpanded.expandedView.getMeasuredWidth() 2313 : thisExpanded.expandedView.getMeasuredHeight()); 2314 expandedSize = (int) (progress * expandedSize); 2315 thisExpanded.setProgress(progress); 2316 } 2317 if (nextExpanded != null) { 2318 nextExpandedSize = 2319 (mOrientation == HORIZONTAL ? nextExpanded.expandedView.getMeasuredWidth() 2320 : nextExpanded.expandedView.getMeasuredHeight()); 2321 nextExpandedSize = (int) ((1f - progress) * nextExpandedSize); 2322 nextExpanded.setProgress(1f - progress); 2323 } 2324 } else { 2325 if (thisExpanded != null) { 2326 expandedSize = 2327 (mOrientation == HORIZONTAL ? thisExpanded.expandedView.getMeasuredWidth() 2328 : thisExpanded.expandedView.getMeasuredHeight()); 2329 thisExpanded.setProgress(1f); 2330 } 2331 } 2332 2333 int totalExpandedSize = expandedSize + nextExpandedSize; 2334 int extraSpaceLow = 0, extraSpaceHigh = 0; 2335 // 4. update expandable views positions 2336 int low = Integer.MAX_VALUE; 2337 int expandedStart = 0; 2338 int nextExpandedStart = 0; 2339 int numOffAxis = (lastExpandable - firstExpandableIndex() + mItemsOnOffAxis - 1) 2340 / mItemsOnOffAxis; 2341 boolean canAnimateExpandedSize = mAnimateLayoutChange && 2342 mScroll.isFinished() && mExpandAdapter != null; 2343 for (int j = 0; j < numOffAxis; j++) { 2344 int viewIndex = firstExpandableIndex() + j * mItemsOnOffAxis; 2345 int endViewIndex = viewIndex + mItemsOnOffAxis - 1; 2346 if (endViewIndex >= lastExpandable) { 2347 endViewIndex = lastExpandable - 1; 2348 } 2349 // get maxSize of the off-axis, get start position for first off-axis 2350 int maxSize = 0; 2351 for (int k = viewIndex; k <= endViewIndex; k++) { 2352 View view = getChildAt(k); 2353 ChildViewHolder h = (ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild); 2354 if (canAnimateExpandedSize) { 2355 // remember last position in temporary variable 2356 if (mOrientation == HORIZONTAL) { 2357 h.mLocation = view.getLeft(); 2358 h.mLocationInParent = h.mLocation + view.getTranslationX(); 2359 } else { 2360 h.mLocation = view.getTop(); 2361 h.mLocationInParent = h.mLocation + view.getTranslationY(); 2362 } 2363 } 2364 maxSize = Math.max(maxSize, mOrientation == HORIZONTAL ? view.getMeasuredWidth() : 2365 view.getMeasuredHeight()); 2366 if (j == 0) { 2367 int viewLow = mOrientation == HORIZONTAL ? view.getLeft() : view.getTop(); 2368 // because we start over again, we should remove the extraspace 2369 if (mScroll.mainAxis().getSelectedTakesMoreSpace()) { 2370 viewLow -= h.mExtraSpaceLow; 2371 } 2372 if (viewLow < low) { 2373 low = viewLow; 2374 } 2375 } 2376 } 2377 // layout views within the off axis and get the max right/bottom 2378 int maxSelectedSize = Integer.MIN_VALUE; 2379 int maxHigh = low + maxSize; 2380 for (int k = viewIndex; k <= endViewIndex; k++) { 2381 View view = getChildAt(k); 2382 int viewStart = low; 2383 int viewMeasuredSize = mOrientation == HORIZONTAL ? view.getMeasuredWidth() 2384 : view.getMeasuredHeight(); 2385 switch (mScroll.getScrollItemAlign()) { 2386 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 2387 viewStart += maxSize / 2 - viewMeasuredSize / 2; 2388 break; 2389 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 2390 viewStart += maxSize - viewMeasuredSize; 2391 break; 2392 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 2393 break; 2394 } 2395 if (mOrientation == HORIZONTAL) { 2396 if (view.isLayoutRequested()) { 2397 measureChild(view); 2398 view.layout(viewStart, view.getTop(), viewStart + view.getMeasuredWidth(), 2399 view.getTop() + view.getMeasuredHeight()); 2400 } else { 2401 view.offsetLeftAndRight(viewStart - view.getLeft()); 2402 } 2403 } else { 2404 if (view.isLayoutRequested()) { 2405 measureChild(view); 2406 view.layout(view.getLeft(), viewStart, view.getLeft() + 2407 view.getMeasuredWidth(), viewStart + view.getMeasuredHeight()); 2408 } else { 2409 view.offsetTopAndBottom(viewStart - view.getTop()); 2410 } 2411 } 2412 if (selectedItemCanScale()) { 2413 maxSelectedSize = Math.max(maxSelectedSize, 2414 getSelectedItemSize(getAdapterIndex(k), view)); 2415 } 2416 } 2417 // we might need update mMaxSize/mMaxSelectedSize in case a relayout happens 2418 for (int k = viewIndex; k <= endViewIndex; k++) { 2419 View view = getChildAt(k); 2420 ChildViewHolder h = (ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild); 2421 h.mMaxSize = maxSize; 2422 h.mExtraSpaceLow = 0; 2423 h.mScrollCenter = computeScrollCenter(k); 2424 } 2425 boolean isTransitionFrom = viewIndex <= expandIdx && expandIdx <= endViewIndex; 2426 boolean isTransitionTo = viewIndex <= nextExpandIdx && nextExpandIdx <= endViewIndex; 2427 // adding extra space 2428 if (maxSelectedSize != Integer.MIN_VALUE) { 2429 int extraSpace = 0; 2430 if (isTransitionFrom) { 2431 extraSpace = (int) ((maxSelectedSize - maxSize) * progress); 2432 } else if (isTransitionTo) { 2433 extraSpace = (int) ((maxSelectedSize - maxSize) * (1 - progress)); 2434 } 2435 if (extraSpace > 0) { 2436 int lowExtraSpace; 2437 if (mScroll.mainAxis().getSelectedTakesMoreSpace()) { 2438 maxHigh = maxHigh + extraSpace; 2439 totalExpandedSize = totalExpandedSize + extraSpace; 2440 switch (mScroll.getScrollItemAlign()) { 2441 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 2442 lowExtraSpace = extraSpace / 2; // extraSpace added low and high 2443 break; 2444 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 2445 lowExtraSpace = extraSpace; // extraSpace added on the low 2446 break; 2447 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 2448 default: 2449 lowExtraSpace = 0; // extraSpace is added on the high 2450 break; 2451 } 2452 } else { 2453 // if we don't add extra space surrounding it, the view should 2454 // grow evenly on low and high 2455 lowExtraSpace = extraSpace / 2; 2456 } 2457 extraSpaceLow += lowExtraSpace; 2458 extraSpaceHigh += (extraSpace - lowExtraSpace); 2459 for (int k = viewIndex; k <= endViewIndex; k++) { 2460 View view = getChildAt(k); 2461 if (mScroll.mainAxis().getSelectedTakesMoreSpace()) { 2462 if (mOrientation == HORIZONTAL) { 2463 view.offsetLeftAndRight(lowExtraSpace); 2464 } else { 2465 view.offsetTopAndBottom(lowExtraSpace); 2466 } 2467 ChildViewHolder h = (ChildViewHolder) 2468 view.getTag(R.id.ScrollAdapterViewChild); 2469 h.mExtraSpaceLow = lowExtraSpace; 2470 } 2471 } 2472 } 2473 } 2474 // animate between different expanded view size 2475 if (canAnimateExpandedSize) { 2476 for (int k = viewIndex; k <= endViewIndex; k++) { 2477 View view = getChildAt(k); 2478 ChildViewHolder h = (ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild); 2479 float target = (mOrientation == HORIZONTAL) ? view.getLeft() : view.getTop(); 2480 if (h.mLocation != target) { 2481 if (mOrientation == HORIZONTAL) { 2482 view.setTranslationX(h.mLocationInParent - target); 2483 view.animate().translationX(0).start(); 2484 } else { 2485 view.setTranslationY(h.mLocationInParent - target); 2486 view.animate().translationY(0).start(); 2487 } 2488 } 2489 } 2490 } 2491 // adding expanded size 2492 if (isTransitionFrom) { 2493 expandedStart = maxHigh; 2494 // "low" (next expandable start) is next to current one until fully expanded 2495 maxHigh += progress == 1f ? expandedSize : 0; 2496 } else if (isTransitionTo) { 2497 nextExpandedStart = maxHigh; 2498 maxHigh += progress == 1f ? nextExpandedSize : expandedSize + nextExpandedSize; 2499 } 2500 // assign beginning position for next "off axis" 2501 low = maxHigh + mSpace; 2502 } 2503 mScroll.mainAxis().setAlignExtraOffset( 2504 (int) (alignExtraOffset * progress + nextAlignExtraOffset * (1 - progress))); 2505 mScroll.mainAxis().setExpandedSize(totalExpandedSize); 2506 mScroll.mainAxis().setExtraSpaceLow(extraSpaceLow); 2507 mScroll.mainAxis().setExtraSpaceHigh(extraSpaceHigh); 2508 2509 // 5. update expanded views 2510 for (int j = 0; j < expandedCount;) { 2511 // remove views in mExpandedViews and are not newly created 2512 ExpandedView v = mExpandedViews.get(j); 2513 if (v!= thisExpanded && v!= nextExpanded && v != previousExpanded) { 2514 if (v.expandedView.hasFocus()) { 2515 View expandableView = getChildAt(expandableIndexFromAdapterIndex(v.index)); 2516 expandableView.requestFocus(); 2517 } 2518 v.close(); 2519 mExpandedChildStates.saveInvisibleView(v.expandedView, v.index); 2520 removeViewInLayout(v.expandedView); 2521 mRecycleExpandedViews.recycleView(v.expandedView, v.viewType); 2522 mExpandedViews.remove(j); 2523 expandedCount--; 2524 } else { 2525 j++; 2526 } 2527 } 2528 for (int j = 0, size = mExpandedViews.size(); j < size; j++) { 2529 ExpandedView v = mExpandedViews.get(j); 2530 int start = v == thisExpanded ? expandedStart : nextExpandedStart; 2531 if (!(v == previousExpanded || v == nextExpanded && progress == 1f)) { 2532 v.expandedView.setVisibility(VISIBLE); 2533 } 2534 if (mOrientation == HORIZONTAL) { 2535 if (v.expandedView.isLayoutRequested()) { 2536 measureChild(v.expandedView); 2537 } 2538 v.expandedView.layout(start, 0, start + v.expandedView.getMeasuredWidth(), 2539 v.expandedView.getMeasuredHeight()); 2540 } else { 2541 if (v.expandedView.isLayoutRequested()) { 2542 measureChild(v.expandedView); 2543 } 2544 v.expandedView.layout(0, start, v.expandedView.getMeasuredWidth(), 2545 start + v.expandedView.getMeasuredHeight()); 2546 } 2547 } 2548 for (int j = 0, size = mExpandedViews.size(); j < size; j++) { 2549 ExpandedView v = mExpandedViews.get(j); 2550 int start = v == thisExpanded ? expandedStart : nextExpandedStart; 2551 if (v == previousExpanded || v == nextExpanded && progress == 1f) { 2552 v.expandedView.setVisibility(View.INVISIBLE); 2553 } 2554 } 2555 2556 // 6. move focus from expandable view to expanded view, disable expandable view after it's 2557 // expanded 2558 if (mExpandAdapter != null && hasFocus()) { 2559 View focusedChild = getFocusedChild(); 2560 int focusedIndex = indexOfChild(focusedChild); 2561 if (focusedIndex >= firstExpandableIndex()) { 2562 for (int j = 0, size = mExpandedViews.size(); j < size; j++) { 2563 ExpandedView v = mExpandedViews.get(j); 2564 if (expandableIndexFromAdapterIndex(v.index) == focusedIndex 2565 && v.expandedView.getVisibility() == View.VISIBLE) { 2566 v.expandedView.requestFocus(); 2567 } 2568 } 2569 } 2570 } 2571 } 2572 2573 @Override 2574 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 2575 View view = getSelectedExpandedView(); 2576 if (view != null) { 2577 return view.requestFocus(direction, previouslyFocusedRect); 2578 } 2579 view = getSelectedView(); 2580 if (view != null) { 2581 return view.requestFocus(direction, previouslyFocusedRect); 2582 } 2583 return false; 2584 } 2585 2586 private int getScrollCenter(View view) { 2587 return ((ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild)).mScrollCenter; 2588 } 2589 2590 public int getScrollItemAlign() { 2591 return mScroll.getScrollItemAlign(); 2592 } 2593 2594 private boolean hasScrollPosition(int scrollCenter, int maxSize, int scrollPosInMain) { 2595 switch (mScroll.getScrollItemAlign()) { 2596 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 2597 return scrollCenter - maxSize / 2 - mSpaceLow < scrollPosInMain && 2598 scrollPosInMain < scrollCenter + maxSize / 2 + mSpaceHigh; 2599 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 2600 return scrollCenter - mSpaceLow <= scrollPosInMain && 2601 scrollPosInMain < scrollCenter + maxSize + mSpaceHigh; 2602 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 2603 return scrollCenter - maxSize - mSpaceLow < scrollPosInMain && 2604 scrollPosInMain <= scrollCenter + mSpaceHigh; 2605 } 2606 return false; 2607 } 2608 2609 private boolean hasScrollPositionSecondAxis(int scrollCenterOffAxis, int viewSizeOffAxis, 2610 int centerOffAxis) { 2611 return centerOffAxis - viewSizeOffAxis / 2 - mSpaceLow <= scrollCenterOffAxis 2612 && scrollCenterOffAxis <= centerOffAxis + viewSizeOffAxis / 2 + mSpaceHigh; 2613 } 2614 2615 /** 2616 * Get the center of expandable view in the state that all expandable views are collapsed, i.e. 2617 * expanded views are excluded from calculating. The space is included in calculation 2618 */ 2619 private int computeScrollCenter(int expandViewIndex) { 2620 int lastIndex = lastExpandableIndex(); 2621 int firstIndex = firstExpandableIndex(); 2622 View firstView = getChildAt(firstIndex); 2623 int center = 0; 2624 switch (mScroll.getScrollItemAlign()) { 2625 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 2626 center = getCenter(firstView); 2627 break; 2628 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 2629 center = mOrientation == HORIZONTAL ? firstView.getLeft() : firstView.getTop(); 2630 break; 2631 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 2632 center = mOrientation == HORIZONTAL ? firstView.getRight() : firstView.getBottom(); 2633 break; 2634 } 2635 if (mScroll.mainAxis().getSelectedTakesMoreSpace()) { 2636 center -= ((ChildViewHolder) firstView.getTag( 2637 R.id.ScrollAdapterViewChild)).mExtraSpaceLow; 2638 } 2639 int nextCenter = -1; 2640 for (int idx = firstIndex; idx < lastIndex; idx += mItemsOnOffAxis) { 2641 View view = getChildAt(idx); 2642 if (idx <= expandViewIndex && expandViewIndex < idx + mItemsOnOffAxis) { 2643 return center; 2644 } 2645 if (idx < lastIndex - mItemsOnOffAxis) { 2646 // nextView is never null if scrollCenter is larger than center of current view 2647 View nextView = getChildAt(idx + mItemsOnOffAxis); 2648 switch (mScroll.getScrollItemAlign()) { // fixme 2649 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 2650 nextCenter = center + (getSize(view) + getSize(nextView)) / 2; 2651 break; 2652 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 2653 nextCenter = center + getSize(view); 2654 break; 2655 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 2656 nextCenter = center + getSize(nextView); 2657 break; 2658 } 2659 nextCenter += mSpace; 2660 } else { 2661 nextCenter = Integer.MAX_VALUE; 2662 } 2663 center = nextCenter; 2664 } 2665 assertFailure("Scroll out of range?"); 2666 return 0; 2667 } 2668 2669 private int getScrollLow(int scrollCenter, View view) { 2670 ChildViewHolder holder = (ChildViewHolder)view.getTag(R.id.ScrollAdapterViewChild); 2671 switch (mScroll.getScrollItemAlign()) { 2672 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 2673 return scrollCenter - holder.mMaxSize / 2; 2674 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 2675 return scrollCenter; 2676 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 2677 return scrollCenter - holder.mMaxSize; 2678 } 2679 return 0; 2680 } 2681 2682 private int getScrollHigh(int scrollCenter, View view) { 2683 ChildViewHolder holder = (ChildViewHolder)view.getTag(R.id.ScrollAdapterViewChild); 2684 switch (mScroll.getScrollItemAlign()) { 2685 case ScrollController.SCROLL_ITEM_ALIGN_CENTER: 2686 return scrollCenter + holder.mMaxSize / 2; 2687 case ScrollController.SCROLL_ITEM_ALIGN_LOW: 2688 return scrollCenter + holder.mMaxSize; 2689 case ScrollController.SCROLL_ITEM_ALIGN_HIGH: 2690 return scrollCenter; 2691 } 2692 return 0; 2693 } 2694 2695 /** 2696 * saves the current item index and scroll information for fully restore from 2697 */ 2698 final static class AdapterViewState { 2699 int itemsOnOffAxis; 2700 int index; // index inside adapter of the current view 2701 Bundle expandedChildStates = Bundle.EMPTY; 2702 Bundle expandableChildStates = Bundle.EMPTY; 2703 } 2704 2705 final static class SavedState extends BaseSavedState { 2706 2707 final AdapterViewState theState = new AdapterViewState(); 2708 2709 public SavedState(Parcelable superState) { 2710 super(superState); 2711 } 2712 2713 @Override 2714 public void writeToParcel(Parcel out, int flags) { 2715 super.writeToParcel(out, flags); 2716 out.writeInt(theState.itemsOnOffAxis); 2717 out.writeInt(theState.index); 2718 out.writeBundle(theState.expandedChildStates); 2719 out.writeBundle(theState.expandableChildStates); 2720 } 2721 2722 @SuppressWarnings("hiding") 2723 public static final Parcelable.Creator<SavedState> CREATOR = 2724 new Parcelable.Creator<SavedState>() { 2725 @Override 2726 public SavedState createFromParcel(Parcel in) { 2727 return new SavedState(in); 2728 } 2729 2730 @Override 2731 public SavedState[] newArray(int size) { 2732 return new SavedState[size]; 2733 } 2734 }; 2735 2736 SavedState(Parcel in) { 2737 super(in); 2738 theState.itemsOnOffAxis = in.readInt(); 2739 theState.index = in.readInt(); 2740 ClassLoader loader = ScrollAdapterView.class.getClassLoader(); 2741 theState.expandedChildStates = in.readBundle(loader); 2742 theState.expandableChildStates = in.readBundle(loader); 2743 } 2744 } 2745 2746 @Override 2747 protected Parcelable onSaveInstanceState() { 2748 Parcelable superState = super.onSaveInstanceState(); 2749 SavedState ss = new SavedState(superState); 2750 int lastIndex = lastExpandableIndex(); 2751 int index = findViewIndexContainingScrollCenter(); 2752 if (index < 0) { 2753 return superState; 2754 } 2755 mExpandedChildStates.saveVisibleViews(); 2756 mExpandableChildStates.saveVisibleViews(); 2757 ss.theState.itemsOnOffAxis = mItemsOnOffAxis; 2758 ss.theState.index = getAdapterIndex(index); 2759 View view = getChildAt(index); 2760 ss.theState.expandedChildStates = mExpandedChildStates.getChildStates(); 2761 ss.theState.expandableChildStates = mExpandableChildStates.getChildStates(); 2762 return ss; 2763 } 2764 2765 @Override 2766 protected void onRestoreInstanceState(Parcelable state) { 2767 if (!(state instanceof SavedState)) { 2768 super.onRestoreInstanceState(state); 2769 return; 2770 } 2771 SavedState ss = (SavedState)state; 2772 super.onRestoreInstanceState(ss.getSuperState()); 2773 mLoadingState = ss.theState; 2774 fireDataSetChanged(); 2775 } 2776 2777 /** 2778 * Returns expandable children states policy, returns one of 2779 * {@link ViewsStateBundle#SAVE_NO_CHILD} {@link ViewsStateBundle#SAVE_VISIBLE_CHILD} 2780 * {@link ViewsStateBundle#SAVE_LIMITED_CHILD} {@link ViewsStateBundle#SAVE_ALL_CHILD} 2781 */ 2782 public int getSaveExpandableViewsPolicy() { 2783 return mExpandableChildStates.getSavePolicy(); 2784 } 2785 2786 /** See explanation in {@link #getSaveExpandableViewsPolicy()} */ 2787 public void setSaveExpandableViewsPolicy(int saveExpandablePolicy) { 2788 mExpandableChildStates.setSavePolicy(saveExpandablePolicy); 2789 } 2790 2791 /** 2792 * Returns the limited number of expandable children that will be saved when 2793 * {@link #getSaveExpandableViewsPolicy()} is {@link ViewsStateBundle#SAVE_LIMITED_CHILD} 2794 */ 2795 public int getSaveExpandableViewsLimit() { 2796 return mExpandableChildStates.getLimitNumber(); 2797 } 2798 2799 /** See explanation in {@link #getSaveExpandableViewsLimit()} */ 2800 public void setSaveExpandableViewsLimit(int saveExpandableChildNumber) { 2801 mExpandableChildStates.setLimitNumber(saveExpandableChildNumber); 2802 } 2803 2804 /** 2805 * Returns expanded children states policy, returns one of 2806 * {@link ViewsStateBundle#SAVE_NO_CHILD} {@link ViewsStateBundle#SAVE_VISIBLE_CHILD} 2807 * {@link ViewsStateBundle#SAVE_LIMITED_CHILD} {@link ViewsStateBundle#SAVE_ALL_CHILD} 2808 */ 2809 public int getSaveExpandedViewsPolicy() { 2810 return mExpandedChildStates.getSavePolicy(); 2811 } 2812 2813 /** See explanation in {@link #getSaveExpandedViewsPolicy} */ 2814 public void setSaveExpandedViewsPolicy(int saveExpandedChildPolicy) { 2815 mExpandedChildStates.setSavePolicy(saveExpandedChildPolicy); 2816 } 2817 2818 /** 2819 * Returns the limited number of expanded children that will be saved when 2820 * {@link #getSaveExpandedViewsPolicy()} is {@link ViewsStateBundle#SAVE_LIMITED_CHILD} 2821 */ 2822 public int getSaveExpandedViewsLimit() { 2823 return mExpandedChildStates.getLimitNumber(); 2824 } 2825 2826 /** See explanation in {@link #getSaveExpandedViewsLimit()} */ 2827 public void setSaveExpandedViewsLimit(int mSaveExpandedNumber) { 2828 mExpandedChildStates.setLimitNumber(mSaveExpandedNumber); 2829 } 2830 2831 public ArrayList<OnItemChangeListener> getOnItemChangeListeners() { 2832 return mOnItemChangeListeners; 2833 } 2834 2835 public void setOnItemChangeListener(OnItemChangeListener onItemChangeListener) { 2836 mOnItemChangeListeners.clear(); 2837 addOnItemChangeListener(onItemChangeListener); 2838 } 2839 2840 public void addOnItemChangeListener(OnItemChangeListener onItemChangeListener) { 2841 if (!mOnItemChangeListeners.contains(onItemChangeListener)) { 2842 mOnItemChangeListeners.add(onItemChangeListener); 2843 } 2844 } 2845 2846 public ArrayList<OnScrollListener> getOnScrollListeners() { 2847 return mOnScrollListeners; 2848 } 2849 2850 public void setOnScrollListener(OnScrollListener onScrollListener) { 2851 mOnScrollListeners.clear(); 2852 addOnScrollListener(onScrollListener); 2853 } 2854 2855 public void addOnScrollListener(OnScrollListener onScrollListener) { 2856 if (!mOnScrollListeners.contains(onScrollListener)) { 2857 mOnScrollListeners.add(onScrollListener); 2858 } 2859 } 2860 2861 public void setExpandedItemInAnim(Animator animator) { 2862 mExpandedItemInAnim = animator; 2863 } 2864 2865 public Animator getExpandedItemInAnim() { 2866 return mExpandedItemInAnim; 2867 } 2868 2869 public void setExpandedItemOutAnim(Animator animator) { 2870 mExpandedItemOutAnim = animator; 2871 } 2872 2873 public Animator getExpandedItemOutAnim() { 2874 return mExpandedItemOutAnim; 2875 } 2876 2877 public boolean isNavigateOutOfOffAxisAllowed() { 2878 return mNavigateOutOfOffAxisAllowed; 2879 } 2880 2881 public boolean isNavigateOutAllowed() { 2882 return mNavigateOutAllowed; 2883 } 2884 2885 /** 2886 * if allow DPAD key in secondary axis to navigate out of ScrollAdapterView 2887 */ 2888 public void setNavigateOutOfOffAxisAllowed(boolean navigateOut) { 2889 mNavigateOutOfOffAxisAllowed = navigateOut; 2890 } 2891 2892 /** 2893 * if allow DPAD key in main axis to navigate out of ScrollAdapterView 2894 */ 2895 public void setNavigateOutAllowed(boolean navigateOut) { 2896 mNavigateOutAllowed = navigateOut; 2897 } 2898 2899 public boolean isNavigateInAnimationAllowed() { 2900 return mNavigateInAnimationAllowed; 2901 } 2902 2903 /** 2904 * if {@code true} allow DPAD event from trackpadNavigation when ScrollAdapterView is in 2905 * animation, this does not affect physical keyboard or manually calling arrowScroll() 2906 */ 2907 public void setNavigateInAnimationAllowed(boolean navigateInAnimation) { 2908 mNavigateInAnimationAllowed = navigateInAnimation; 2909 } 2910 2911 /** set space in pixels between two items */ 2912 public void setSpace(int space) { 2913 mSpace = space; 2914 // mSpace may not be evenly divided by 2 2915 mSpaceLow = mSpace / 2; 2916 mSpaceHigh = mSpace - mSpaceLow; 2917 } 2918 2919 /** get space in pixels between two items */ 2920 public int getSpace() { 2921 return mSpace; 2922 } 2923 2924 /** set pixels of selected item, use {@link ScrollAdapterCustomSize} for more complicated case */ 2925 public void setSelectedSize(int selectedScale) { 2926 mSelectedSize = selectedScale; 2927 } 2928 2929 /** get pixels of selected item */ 2930 public int getSelectedSize() { 2931 return mSelectedSize; 2932 } 2933 2934 public void setSelectedTakesMoreSpace(boolean selectedTakesMoreSpace) { 2935 mScroll.mainAxis().setSelectedTakesMoreSpace(selectedTakesMoreSpace); 2936 } 2937 2938 public boolean getSelectedTakesMoreSpace() { 2939 return mScroll.mainAxis().getSelectedTakesMoreSpace(); 2940 } 2941 2942 private boolean selectedItemCanScale() { 2943 return mSelectedSize != 0 || mAdapterCustomSize != null; 2944 } 2945 2946 private int getSelectedItemSize(int adapterIndex, View view) { 2947 if (mSelectedSize != 0) { 2948 return mSelectedSize; 2949 } else if (mAdapterCustomSize != null) { 2950 return mAdapterCustomSize.getSelectItemSize(adapterIndex, view); 2951 } 2952 return 0; 2953 } 2954 2955 private static void assertFailure(String msg) { 2956 throw new RuntimeException(msg); 2957 } 2958 2959 } 2960