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