1 /* 2 * Copyright (C) 2013 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.photos.views; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.database.DataSetObserver; 22 import android.graphics.Canvas; 23 import android.support.v4.view.MotionEventCompat; 24 import android.support.v4.view.VelocityTrackerCompat; 25 import android.support.v4.view.ViewCompat; 26 import android.support.v4.widget.EdgeEffectCompat; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.util.SparseArray; 30 import android.view.MotionEvent; 31 import android.view.VelocityTracker; 32 import android.view.View; 33 import android.view.ViewConfiguration; 34 import android.view.ViewGroup; 35 import android.widget.ListAdapter; 36 import android.widget.OverScroller; 37 38 import java.util.ArrayList; 39 40 public class GalleryThumbnailView extends ViewGroup { 41 42 public interface GalleryThumbnailAdapter extends ListAdapter { 43 /** 44 * @param position Position to get the intrinsic aspect ratio for 45 * @return width / height 46 */ getIntrinsicAspectRatio(int position)47 float getIntrinsicAspectRatio(int position); 48 } 49 50 private static final String TAG = "GalleryThumbnailView"; 51 private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f); 52 private static final int LAND_UNITS = 2; 53 private static final int PORT_UNITS = 3; 54 55 private GalleryThumbnailAdapter mAdapter; 56 57 private final RecycleBin mRecycler = new RecycleBin(); 58 59 private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver(); 60 61 private boolean mDataChanged; 62 private int mOldItemCount; 63 private int mItemCount; 64 private boolean mHasStableIds; 65 66 private int mFirstPosition; 67 68 private boolean mPopulating; 69 private boolean mInLayout; 70 71 private int mTouchSlop; 72 private int mMaximumVelocity; 73 private int mFlingVelocity; 74 private float mLastTouchX; 75 private float mTouchRemainderX; 76 private int mActivePointerId; 77 78 private static final int TOUCH_MODE_IDLE = 0; 79 private static final int TOUCH_MODE_DRAGGING = 1; 80 private static final int TOUCH_MODE_FLINGING = 2; 81 82 private int mTouchMode; 83 private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 84 private final OverScroller mScroller; 85 86 private final EdgeEffectCompat mLeftEdge; 87 private final EdgeEffectCompat mRightEdge; 88 89 private int mLargeColumnWidth; 90 private int mSmallColumnWidth; 91 private int mLargeColumnUnitCount = 8; 92 private int mSmallColumnUnitCount = 10; 93 GalleryThumbnailView(Context context)94 public GalleryThumbnailView(Context context) { 95 this(context, null); 96 } 97 GalleryThumbnailView(Context context, AttributeSet attrs)98 public GalleryThumbnailView(Context context, AttributeSet attrs) { 99 this(context, attrs, 0); 100 } 101 GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle)102 public GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle) { 103 super(context, attrs, defStyle); 104 105 final ViewConfiguration vc = ViewConfiguration.get(context); 106 mTouchSlop = vc.getScaledTouchSlop(); 107 mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); 108 mFlingVelocity = vc.getScaledMinimumFlingVelocity(); 109 mScroller = new OverScroller(context); 110 111 mLeftEdge = new EdgeEffectCompat(context); 112 mRightEdge = new EdgeEffectCompat(context); 113 setWillNotDraw(false); 114 setClipToPadding(false); 115 } 116 117 @Override requestLayout()118 public void requestLayout() { 119 if (!mPopulating) { 120 super.requestLayout(); 121 } 122 } 123 124 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)125 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 126 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 127 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 128 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 129 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 130 131 if (widthMode != MeasureSpec.EXACTLY) { 132 Log.e(TAG, "onMeasure: must have an exact width or match_parent! " + 133 "Using fallback spec of EXACTLY " + widthSize); 134 } 135 if (heightMode != MeasureSpec.EXACTLY) { 136 Log.e(TAG, "onMeasure: must have an exact height or match_parent! " + 137 "Using fallback spec of EXACTLY " + heightSize); 138 } 139 140 setMeasuredDimension(widthSize, heightSize); 141 142 float portSpaces = mLargeColumnUnitCount / PORT_UNITS; 143 float height = getMeasuredHeight() / portSpaces; 144 mLargeColumnWidth = (int) (height / ASPECT_RATIO); 145 portSpaces++; 146 height = getMeasuredHeight() / portSpaces; 147 mSmallColumnWidth = (int) (height / ASPECT_RATIO); 148 } 149 150 @Override onLayout(boolean changed, int l, int t, int r, int b)151 protected void onLayout(boolean changed, int l, int t, int r, int b) { 152 mInLayout = true; 153 populate(); 154 mInLayout = false; 155 156 final int width = r - l; 157 final int height = b - t; 158 mLeftEdge.setSize(width, height); 159 mRightEdge.setSize(width, height); 160 } 161 populate()162 private void populate() { 163 if (getWidth() == 0 || getHeight() == 0) { 164 return; 165 } 166 167 // TODO: Handle size changing 168 // final int colCount = mColCount; 169 // if (mItemTops == null || mItemTops.length != colCount) { 170 // mItemTops = new int[colCount]; 171 // mItemBottoms = new int[colCount]; 172 // final int top = getPaddingTop(); 173 // final int offset = top + Math.min(mRestoreOffset, 0); 174 // Arrays.fill(mItemTops, offset); 175 // Arrays.fill(mItemBottoms, offset); 176 // mLayoutRecords.clear(); 177 // if (mInLayout) { 178 // removeAllViewsInLayout(); 179 // } else { 180 // removeAllViews(); 181 // } 182 // mRestoreOffset = 0; 183 // } 184 185 mPopulating = true; 186 layoutChildren(mDataChanged); 187 fillRight(mFirstPosition + getChildCount(), 0); 188 fillLeft(mFirstPosition - 1, 0); 189 mPopulating = false; 190 mDataChanged = false; 191 } 192 layoutChildren(boolean queryAdapter)193 final void layoutChildren(boolean queryAdapter) { 194 // TODO 195 // final int childCount = getChildCount(); 196 // for (int i = 0; i < childCount; i++) { 197 // View child = getChildAt(i); 198 // 199 // if (child.isLayoutRequested()) { 200 // final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY); 201 // final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY); 202 // child.measure(widthSpec, heightSpec); 203 // child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); 204 // } 205 // 206 // int childTop = mItemBottoms[col] > Integer.MIN_VALUE ? 207 // mItemBottoms[col] + mItemMargin : child.getTop(); 208 // if (span > 1) { 209 // int lowest = childTop; 210 // for (int j = col + 1; j < col + span; j++) { 211 // final int bottom = mItemBottoms[j] + mItemMargin; 212 // if (bottom > lowest) { 213 // lowest = bottom; 214 // } 215 // } 216 // childTop = lowest; 217 // } 218 // final int childHeight = child.getMeasuredHeight(); 219 // final int childBottom = childTop + childHeight; 220 // final int childLeft = paddingLeft + col * (colWidth + itemMargin); 221 // final int childRight = childLeft + child.getMeasuredWidth(); 222 // child.layout(childLeft, childTop, childRight, childBottom); 223 // } 224 } 225 226 /** 227 * Obtain the view and add it to our list of children. The view can be made 228 * fresh, converted from an unused view, or used as is if it was in the 229 * recycle bin. 230 * 231 * @param startPosition Logical position in the list to start from 232 * @param x Left or right edge of the view to add 233 * @param forward If true, align left edge to x and increase position. 234 * If false, align right edge to x and decrease position. 235 * @return Number of views added 236 */ makeAndAddColumn(int startPosition, int x, boolean forward)237 private int makeAndAddColumn(int startPosition, int x, boolean forward) { 238 int columnWidth = mLargeColumnWidth; 239 int addViews = 0; 240 for (int remaining = mLargeColumnUnitCount, i = 0; 241 remaining > 0 && startPosition + i >= 0 && startPosition + i < mItemCount; 242 i += forward ? 1 : -1, addViews++) { 243 if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) { 244 // landscape 245 remaining -= LAND_UNITS; 246 } else { 247 // portrait 248 remaining -= PORT_UNITS; 249 if (remaining < 0) { 250 remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount); 251 columnWidth = mSmallColumnWidth; 252 } 253 } 254 } 255 int nextTop = 0; 256 for (int i = 0; i < addViews; i++) { 257 int position = startPosition + (forward ? i : -i); 258 View child = obtainView(position, null); 259 if (child.getParent() != this) { 260 if (mInLayout) { 261 addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams()); 262 } else { 263 addView(child, forward ? -1 : 0); 264 } 265 } 266 int heightSize = (int) (.5f + (mAdapter.getIntrinsicAspectRatio(position) >= 1f 267 ? columnWidth / ASPECT_RATIO 268 : columnWidth * ASPECT_RATIO)); 269 int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 270 int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY); 271 child.measure(widthSpec, heightSpec); 272 int childLeft = forward ? x : x - columnWidth; 273 child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize); 274 nextTop += heightSize; 275 } 276 return addViews; 277 } 278 279 @Override onInterceptTouchEvent(MotionEvent ev)280 public boolean onInterceptTouchEvent(MotionEvent ev) { 281 mVelocityTracker.addMovement(ev); 282 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 283 switch (action) { 284 case MotionEvent.ACTION_DOWN: 285 mVelocityTracker.clear(); 286 mScroller.abortAnimation(); 287 mLastTouchX = ev.getX(); 288 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 289 mTouchRemainderX = 0; 290 if (mTouchMode == TOUCH_MODE_FLINGING) { 291 // Catch! 292 mTouchMode = TOUCH_MODE_DRAGGING; 293 return true; 294 } 295 break; 296 297 case MotionEvent.ACTION_MOVE: { 298 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 299 if (index < 0) { 300 Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + 301 mActivePointerId + " - did StaggeredGridView receive an inconsistent " + 302 "event stream?"); 303 return false; 304 } 305 final float x = MotionEventCompat.getX(ev, index); 306 final float dx = x - mLastTouchX + mTouchRemainderX; 307 final int deltaY = (int) dx; 308 mTouchRemainderX = dx - deltaY; 309 310 if (Math.abs(dx) > mTouchSlop) { 311 mTouchMode = TOUCH_MODE_DRAGGING; 312 return true; 313 } 314 } 315 } 316 317 return false; 318 } 319 320 @Override onTouchEvent(MotionEvent ev)321 public boolean onTouchEvent(MotionEvent ev) { 322 mVelocityTracker.addMovement(ev); 323 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 324 switch (action) { 325 case MotionEvent.ACTION_DOWN: 326 mVelocityTracker.clear(); 327 mScroller.abortAnimation(); 328 mLastTouchX = ev.getX(); 329 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 330 mTouchRemainderX = 0; 331 break; 332 333 case MotionEvent.ACTION_MOVE: { 334 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 335 if (index < 0) { 336 Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + 337 mActivePointerId + " - did StaggeredGridView receive an inconsistent " + 338 "event stream?"); 339 return false; 340 } 341 final float x = MotionEventCompat.getX(ev, index); 342 final float dx = x - mLastTouchX + mTouchRemainderX; 343 final int deltaX = (int) dx; 344 mTouchRemainderX = dx - deltaX; 345 346 if (Math.abs(dx) > mTouchSlop) { 347 mTouchMode = TOUCH_MODE_DRAGGING; 348 } 349 350 if (mTouchMode == TOUCH_MODE_DRAGGING) { 351 mLastTouchX = x; 352 353 if (!trackMotionScroll(deltaX, true)) { 354 // Break fling velocity if we impacted an edge. 355 mVelocityTracker.clear(); 356 } 357 } 358 } break; 359 360 case MotionEvent.ACTION_CANCEL: 361 mTouchMode = TOUCH_MODE_IDLE; 362 break; 363 364 case MotionEvent.ACTION_UP: { 365 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 366 final float velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker, 367 mActivePointerId); 368 if (Math.abs(velocity) > mFlingVelocity) { // TODO 369 mTouchMode = TOUCH_MODE_FLINGING; 370 mScroller.fling(0, 0, (int) velocity, 0, 371 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); 372 mLastTouchX = 0; 373 ViewCompat.postInvalidateOnAnimation(this); 374 } else { 375 mTouchMode = TOUCH_MODE_IDLE; 376 } 377 378 } break; 379 } 380 return true; 381 } 382 383 /** 384 * 385 * @param deltaX Pixels that content should move by 386 * @return true if the movement completed, false if it was stopped prematurely. 387 */ trackMotionScroll(int deltaX, boolean allowOverScroll)388 private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) { 389 final boolean contentFits = contentFits(); 390 final int allowOverhang = Math.abs(deltaX); 391 392 final int overScrolledBy; 393 final int movedBy; 394 if (!contentFits) { 395 final int overhang; 396 final boolean up; 397 mPopulating = true; 398 if (deltaX > 0) { 399 overhang = fillLeft(mFirstPosition - 1, allowOverhang); 400 up = true; 401 } else { 402 overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang); 403 up = false; 404 } 405 movedBy = Math.min(overhang, allowOverhang); 406 offsetChildren(up ? movedBy : -movedBy); 407 recycleOffscreenViews(); 408 mPopulating = false; 409 overScrolledBy = allowOverhang - overhang; 410 } else { 411 overScrolledBy = allowOverhang; 412 movedBy = 0; 413 } 414 415 if (allowOverScroll) { 416 final int overScrollMode = ViewCompat.getOverScrollMode(this); 417 418 if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 419 (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) { 420 421 if (overScrolledBy > 0) { 422 EdgeEffectCompat edge = deltaX > 0 ? mLeftEdge : mRightEdge; 423 edge.onPull((float) Math.abs(deltaX) / getWidth()); 424 ViewCompat.postInvalidateOnAnimation(this); 425 } 426 } 427 } 428 429 return deltaX == 0 || movedBy != 0; 430 } 431 432 /** 433 * Important: this method will leave offscreen views attached if they 434 * are required to maintain the invariant that child view with index i 435 * is always the view corresponding to position mFirstPosition + i. 436 */ recycleOffscreenViews()437 private void recycleOffscreenViews() { 438 final int height = getHeight(); 439 final int clearAbove = 0; 440 final int clearBelow = height; 441 for (int i = getChildCount() - 1; i >= 0; i--) { 442 final View child = getChildAt(i); 443 if (child.getTop() <= clearBelow) { 444 // There may be other offscreen views, but we need to maintain 445 // the invariant documented above. 446 break; 447 } 448 449 if (mInLayout) { 450 removeViewsInLayout(i, 1); 451 } else { 452 removeViewAt(i); 453 } 454 455 mRecycler.addScrap(child); 456 } 457 458 while (getChildCount() > 0) { 459 final View child = getChildAt(0); 460 if (child.getBottom() >= clearAbove) { 461 // There may be other offscreen views, but we need to maintain 462 // the invariant documented above. 463 break; 464 } 465 466 if (mInLayout) { 467 removeViewsInLayout(0, 1); 468 } else { 469 removeViewAt(0); 470 } 471 472 mRecycler.addScrap(child); 473 mFirstPosition++; 474 } 475 } 476 offsetChildren(int offset)477 final void offsetChildren(int offset) { 478 final int childCount = getChildCount(); 479 for (int i = 0; i < childCount; i++) { 480 final View child = getChildAt(i); 481 child.layout(child.getLeft() + offset, child.getTop(), 482 child.getRight() + offset, child.getBottom()); 483 } 484 } 485 contentFits()486 private boolean contentFits() { 487 final int childCount = getChildCount(); 488 if (childCount == 0) return true; 489 if (childCount != mItemCount) return false; 490 491 return getChildAt(0).getLeft() >= getPaddingLeft() && 492 getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight(); 493 } 494 recycleAllViews()495 private void recycleAllViews() { 496 for (int i = 0; i < getChildCount(); i++) { 497 mRecycler.addScrap(getChildAt(i)); 498 } 499 500 if (mInLayout) { 501 removeAllViewsInLayout(); 502 } else { 503 removeAllViews(); 504 } 505 } 506 fillRight(int pos, int overhang)507 private int fillRight(int pos, int overhang) { 508 int end = (getRight() - getLeft()) + overhang; 509 510 int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight(); 511 while (nextLeft < end && pos < mItemCount) { 512 pos += makeAndAddColumn(pos, nextLeft, true); 513 nextLeft = getChildAt(getChildCount() - 1).getRight(); 514 } 515 final int gridRight = getWidth() - getPaddingRight(); 516 return getChildAt(getChildCount() - 1).getRight() - gridRight; 517 } 518 fillLeft(int pos, int overhang)519 private int fillLeft(int pos, int overhang) { 520 int end = getPaddingLeft() - overhang; 521 522 int nextRight = getChildAt(0).getLeft(); 523 while (nextRight > end && pos >= 0) { 524 pos -= makeAndAddColumn(pos, nextRight, false); 525 nextRight = getChildAt(0).getLeft(); 526 } 527 528 mFirstPosition = pos + 1; 529 return getPaddingLeft() - getChildAt(0).getLeft(); 530 } 531 532 @Override computeScroll()533 public void computeScroll() { 534 if (mScroller.computeScrollOffset()) { 535 final int x = mScroller.getCurrX(); 536 final int dx = (int) (x - mLastTouchX); 537 mLastTouchX = x; 538 final boolean stopped = !trackMotionScroll(dx, false); 539 540 if (!stopped && !mScroller.isFinished()) { 541 ViewCompat.postInvalidateOnAnimation(this); 542 } else { 543 if (stopped) { 544 final int overScrollMode = ViewCompat.getOverScrollMode(this); 545 if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { 546 final EdgeEffectCompat edge; 547 if (dx > 0) { 548 edge = mLeftEdge; 549 } else { 550 edge = mRightEdge; 551 } 552 edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity())); 553 ViewCompat.postInvalidateOnAnimation(this); 554 } 555 mScroller.abortAnimation(); 556 } 557 mTouchMode = TOUCH_MODE_IDLE; 558 } 559 } 560 } 561 562 @Override draw(Canvas canvas)563 public void draw(Canvas canvas) { 564 super.draw(canvas); 565 566 if (!mLeftEdge.isFinished()) { 567 final int restoreCount = canvas.save(); 568 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 569 570 canvas.rotate(270); 571 canvas.translate(-height + getPaddingTop(), 0); 572 mLeftEdge.setSize(height, getWidth()); 573 if (mLeftEdge.draw(canvas)) { 574 postInvalidateOnAnimation(); 575 } 576 canvas.restoreToCount(restoreCount); 577 } 578 if (!mRightEdge.isFinished()) { 579 final int restoreCount = canvas.save(); 580 final int width = getWidth(); 581 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 582 583 canvas.rotate(90); 584 canvas.translate(-getPaddingTop(), width); 585 mRightEdge.setSize(height, width); 586 if (mRightEdge.draw(canvas)) { 587 postInvalidateOnAnimation(); 588 } 589 canvas.restoreToCount(restoreCount); 590 } 591 } 592 593 /** 594 * Obtain a populated view from the adapter. If optScrap is non-null and is not 595 * reused it will be placed in the recycle bin. 596 * 597 * @param position position to get view for 598 * @param optScrap Optional scrap view; will be reused if possible 599 * @return A new view, a recycled view from mRecycler, or optScrap 600 */ obtainView(int position, View optScrap)601 private final View obtainView(int position, View optScrap) { 602 View view = mRecycler.getTransientStateView(position); 603 if (view != null) { 604 return view; 605 } 606 607 // Reuse optScrap if it's of the right type (and not null) 608 final int optType = optScrap != null ? 609 ((LayoutParams) optScrap.getLayoutParams()).viewType : -1; 610 final int positionViewType = mAdapter.getItemViewType(position); 611 final View scrap = optType == positionViewType ? 612 optScrap : mRecycler.getScrapView(positionViewType); 613 614 view = mAdapter.getView(position, scrap, this); 615 616 if (view != scrap && scrap != null) { 617 // The adapter didn't use it; put it back. 618 mRecycler.addScrap(scrap); 619 } 620 621 ViewGroup.LayoutParams lp = view.getLayoutParams(); 622 623 if (view.getParent() != this) { 624 if (lp == null) { 625 lp = generateDefaultLayoutParams(); 626 } else if (!checkLayoutParams(lp)) { 627 lp = generateLayoutParams(lp); 628 } 629 view.setLayoutParams(lp); 630 } 631 632 final LayoutParams sglp = (LayoutParams) lp; 633 sglp.position = position; 634 sglp.viewType = positionViewType; 635 636 return view; 637 } 638 getAdapter()639 public GalleryThumbnailAdapter getAdapter() { 640 return mAdapter; 641 } 642 setAdapter(GalleryThumbnailAdapter adapter)643 public void setAdapter(GalleryThumbnailAdapter adapter) { 644 if (mAdapter != null) { 645 mAdapter.unregisterDataSetObserver(mObserver); 646 } 647 // TODO: If the new adapter says that there are stable IDs, remove certain layout records 648 // and onscreen views if they have changed instead of removing all of the state here. 649 clearAllState(); 650 mAdapter = adapter; 651 mDataChanged = true; 652 mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0; 653 if (adapter != null) { 654 adapter.registerDataSetObserver(mObserver); 655 mRecycler.setViewTypeCount(adapter.getViewTypeCount()); 656 mHasStableIds = adapter.hasStableIds(); 657 } else { 658 mHasStableIds = false; 659 } 660 populate(); 661 } 662 663 /** 664 * Clear all state because the grid will be used for a completely different set of data. 665 */ clearAllState()666 private void clearAllState() { 667 // Clear all layout records and views 668 removeAllViews(); 669 670 // Reset to the top of the grid 671 mFirstPosition = 0; 672 673 // Clear recycler because there could be different view types now 674 mRecycler.clear(); 675 } 676 677 @Override generateDefaultLayoutParams()678 protected LayoutParams generateDefaultLayoutParams() { 679 return new LayoutParams(LayoutParams.WRAP_CONTENT); 680 } 681 682 @Override generateLayoutParams(ViewGroup.LayoutParams lp)683 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 684 return new LayoutParams(lp); 685 } 686 687 @Override checkLayoutParams(ViewGroup.LayoutParams lp)688 protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { 689 return lp instanceof LayoutParams; 690 } 691 692 @Override generateLayoutParams(AttributeSet attrs)693 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 694 return new LayoutParams(getContext(), attrs); 695 } 696 697 public static class LayoutParams extends ViewGroup.LayoutParams { 698 private static final int[] LAYOUT_ATTRS = new int[] { 699 android.R.attr.layout_span 700 }; 701 702 private static final int SPAN_INDEX = 0; 703 704 /** 705 * The number of columns this item should span 706 */ 707 public int span = 1; 708 709 /** 710 * Item position this view represents 711 */ 712 int position; 713 714 /** 715 * Type of this view as reported by the adapter 716 */ 717 int viewType; 718 719 /** 720 * The column this view is occupying 721 */ 722 int column; 723 724 /** 725 * The stable ID of the item this view displays 726 */ 727 long id = -1; 728 LayoutParams(int height)729 public LayoutParams(int height) { 730 super(MATCH_PARENT, height); 731 732 if (this.height == MATCH_PARENT) { 733 Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + 734 "impossible! Falling back to WRAP_CONTENT"); 735 this.height = WRAP_CONTENT; 736 } 737 } 738 LayoutParams(Context c, AttributeSet attrs)739 public LayoutParams(Context c, AttributeSet attrs) { 740 super(c, attrs); 741 742 if (this.width != MATCH_PARENT) { 743 Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + 744 " - must be MATCH_PARENT"); 745 this.width = MATCH_PARENT; 746 } 747 if (this.height == MATCH_PARENT) { 748 Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + 749 "impossible! Falling back to WRAP_CONTENT"); 750 this.height = WRAP_CONTENT; 751 } 752 753 TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 754 span = a.getInteger(SPAN_INDEX, 1); 755 a.recycle(); 756 } 757 LayoutParams(ViewGroup.LayoutParams other)758 public LayoutParams(ViewGroup.LayoutParams other) { 759 super(other); 760 761 if (this.width != MATCH_PARENT) { 762 Log.w(TAG, "Constructing LayoutParams with width " + this.width + 763 " - must be MATCH_PARENT"); 764 this.width = MATCH_PARENT; 765 } 766 if (this.height == MATCH_PARENT) { 767 Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + 768 "impossible! Falling back to WRAP_CONTENT"); 769 this.height = WRAP_CONTENT; 770 } 771 } 772 } 773 774 private class RecycleBin { 775 private ArrayList<View>[] mScrapViews; 776 private int mViewTypeCount; 777 private int mMaxScrap; 778 779 private SparseArray<View> mTransientStateViews; 780 setViewTypeCount(int viewTypeCount)781 public void setViewTypeCount(int viewTypeCount) { 782 if (viewTypeCount < 1) { 783 throw new IllegalArgumentException("Must have at least one view type (" + 784 viewTypeCount + " types reported)"); 785 } 786 if (viewTypeCount == mViewTypeCount) { 787 return; 788 } 789 790 ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; 791 for (int i = 0; i < viewTypeCount; i++) { 792 scrapViews[i] = new ArrayList<View>(); 793 } 794 mViewTypeCount = viewTypeCount; 795 mScrapViews = scrapViews; 796 } 797 clear()798 public void clear() { 799 final int typeCount = mViewTypeCount; 800 for (int i = 0; i < typeCount; i++) { 801 mScrapViews[i].clear(); 802 } 803 if (mTransientStateViews != null) { 804 mTransientStateViews.clear(); 805 } 806 } 807 clearTransientViews()808 public void clearTransientViews() { 809 if (mTransientStateViews != null) { 810 mTransientStateViews.clear(); 811 } 812 } 813 addScrap(View v)814 public void addScrap(View v) { 815 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 816 if (ViewCompat.hasTransientState(v)) { 817 if (mTransientStateViews == null) { 818 mTransientStateViews = new SparseArray<View>(); 819 } 820 mTransientStateViews.put(lp.position, v); 821 return; 822 } 823 824 final int childCount = getChildCount(); 825 if (childCount > mMaxScrap) { 826 mMaxScrap = childCount; 827 } 828 829 ArrayList<View> scrap = mScrapViews[lp.viewType]; 830 if (scrap.size() < mMaxScrap) { 831 scrap.add(v); 832 } 833 } 834 getTransientStateView(int position)835 public View getTransientStateView(int position) { 836 if (mTransientStateViews == null) { 837 return null; 838 } 839 840 final View result = mTransientStateViews.get(position); 841 if (result != null) { 842 mTransientStateViews.remove(position); 843 } 844 return result; 845 } 846 getScrapView(int type)847 public View getScrapView(int type) { 848 ArrayList<View> scrap = mScrapViews[type]; 849 if (scrap.isEmpty()) { 850 return null; 851 } 852 853 final int index = scrap.size() - 1; 854 final View result = scrap.get(index); 855 scrap.remove(index); 856 return result; 857 } 858 } 859 860 private class AdapterDataSetObserver extends DataSetObserver { 861 @Override onChanged()862 public void onChanged() { 863 mDataChanged = true; 864 mOldItemCount = mItemCount; 865 mItemCount = mAdapter.getCount(); 866 867 // TODO: Consider matching these back up if we have stable IDs. 868 mRecycler.clearTransientViews(); 869 870 if (!mHasStableIds) { 871 recycleAllViews(); 872 } 873 874 // TODO: consider repopulating in a deferred runnable instead 875 // (so that successive changes may still be batched) 876 requestLayout(); 877 } 878 879 @Override onInvalidated()880 public void onInvalidated() { 881 } 882 } 883 } 884