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.example.android.listviewdragginganimation; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.TypeEvaluator; 23 import android.animation.ValueAnimator; 24 import android.content.Context; 25 import android.graphics.Bitmap; 26 import android.graphics.Canvas; 27 import android.graphics.Color; 28 import android.graphics.Paint; 29 import android.graphics.Rect; 30 import android.graphics.drawable.BitmapDrawable; 31 import android.util.AttributeSet; 32 import android.util.DisplayMetrics; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewTreeObserver; 36 import android.widget.AbsListView; 37 import android.widget.AdapterView; 38 import android.widget.BaseAdapter; 39 import android.widget.ListView; 40 41 import java.util.ArrayList; 42 43 /** 44 * The dynamic listview is an extension of listview that supports cell dragging 45 * and swapping. 46 * 47 * This layout is in charge of positioning the hover cell in the correct location 48 * on the screen in response to user touch events. It uses the position of the 49 * hover cell to determine when two cells should be swapped. If two cells should 50 * be swapped, all the corresponding data set and layout changes are handled here. 51 * 52 * If no cell is selected, all the touch events are passed down to the listview 53 * and behave normally. If one of the items in the listview experiences a 54 * long press event, the contents of its current visible state are captured as 55 * a bitmap and its visibility is set to INVISIBLE. A hover cell is then created and 56 * added to this layout as an overlaying BitmapDrawable above the listview. Once the 57 * hover cell is translated some distance to signify an item swap, a data set change 58 * accompanied by animation takes place. When the user releases the hover cell, 59 * it animates into its corresponding position in the listview. 60 * 61 * When the hover cell is either above or below the bounds of the listview, this 62 * listview also scrolls on its own so as to reveal additional content. 63 */ 64 public class DynamicListView extends ListView { 65 66 private final int SMOOTH_SCROLL_AMOUNT_AT_EDGE = 15; 67 private final int MOVE_DURATION = 150; 68 private final int LINE_THICKNESS = 15; 69 70 public ArrayList<String> mCheeseList; 71 72 private int mLastEventY = -1; 73 74 private int mDownY = -1; 75 private int mDownX = -1; 76 77 private int mTotalOffset = 0; 78 79 private boolean mCellIsMobile = false; 80 private boolean mIsMobileScrolling = false; 81 private int mSmoothScrollAmountAtEdge = 0; 82 83 private final int INVALID_ID = -1; 84 private long mAboveItemId = INVALID_ID; 85 private long mMobileItemId = INVALID_ID; 86 private long mBelowItemId = INVALID_ID; 87 88 private BitmapDrawable mHoverCell; 89 private Rect mHoverCellCurrentBounds; 90 private Rect mHoverCellOriginalBounds; 91 92 private final int INVALID_POINTER_ID = -1; 93 private int mActivePointerId = INVALID_POINTER_ID; 94 95 private boolean mIsWaitingForScrollFinish = false; 96 private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; 97 98 public DynamicListView(Context context) { 99 super(context); 100 init(context); 101 } 102 103 public DynamicListView(Context context, AttributeSet attrs, int defStyle) { 104 super(context, attrs, defStyle); 105 init(context); 106 } 107 108 public DynamicListView(Context context, AttributeSet attrs) { 109 super(context, attrs); 110 init(context); 111 } 112 113 public void init(Context context) { 114 setOnItemLongClickListener(mOnItemLongClickListener); 115 setOnScrollListener(mScrollListener); 116 DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 117 mSmoothScrollAmountAtEdge = (int)(SMOOTH_SCROLL_AMOUNT_AT_EDGE / metrics.density); 118 } 119 120 /** 121 * Listens for long clicks on any items in the listview. When a cell has 122 * been selected, the hover cell is created and set up. 123 */ 124 private AdapterView.OnItemLongClickListener mOnItemLongClickListener = 125 new AdapterView.OnItemLongClickListener() { 126 public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int pos, long id) { 127 mTotalOffset = 0; 128 129 int position = pointToPosition(mDownX, mDownY); 130 int itemNum = position - getFirstVisiblePosition(); 131 132 View selectedView = getChildAt(itemNum); 133 mMobileItemId = getAdapter().getItemId(position); 134 mHoverCell = getAndAddHoverView(selectedView); 135 selectedView.setVisibility(INVISIBLE); 136 137 mCellIsMobile = true; 138 139 updateNeighborViewsForID(mMobileItemId); 140 141 return true; 142 } 143 }; 144 145 /** 146 * Creates the hover cell with the appropriate bitmap and of appropriate 147 * size. The hover cell's BitmapDrawable is drawn on top of the bitmap every 148 * single time an invalidate call is made. 149 */ 150 private BitmapDrawable getAndAddHoverView(View v) { 151 152 int w = v.getWidth(); 153 int h = v.getHeight(); 154 int top = v.getTop(); 155 int left = v.getLeft(); 156 157 Bitmap b = getBitmapWithBorder(v); 158 159 BitmapDrawable drawable = new BitmapDrawable(getResources(), b); 160 161 mHoverCellOriginalBounds = new Rect(left, top, left + w, top + h); 162 mHoverCellCurrentBounds = new Rect(mHoverCellOriginalBounds); 163 164 drawable.setBounds(mHoverCellCurrentBounds); 165 166 return drawable; 167 } 168 169 /** Draws a black border over the screenshot of the view passed in. */ 170 private Bitmap getBitmapWithBorder(View v) { 171 Bitmap bitmap = getBitmapFromView(v); 172 Canvas can = new Canvas(bitmap); 173 174 Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); 175 176 Paint paint = new Paint(); 177 paint.setStyle(Paint.Style.STROKE); 178 paint.setStrokeWidth(LINE_THICKNESS); 179 paint.setColor(Color.BLACK); 180 181 can.drawBitmap(bitmap, 0, 0, null); 182 can.drawRect(rect, paint); 183 184 return bitmap; 185 } 186 187 /** Returns a bitmap showing a screenshot of the view passed in. */ 188 private Bitmap getBitmapFromView(View v) { 189 Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); 190 Canvas canvas = new Canvas (bitmap); 191 v.draw(canvas); 192 return bitmap; 193 } 194 195 /** 196 * Stores a reference to the views above and below the item currently 197 * corresponding to the hover cell. It is important to note that if this 198 * item is either at the top or bottom of the list, mAboveItemId or mBelowItemId 199 * may be invalid. 200 */ 201 private void updateNeighborViewsForID(long itemID) { 202 int position = getPositionForID(itemID); 203 StableArrayAdapter adapter = ((StableArrayAdapter)getAdapter()); 204 mAboveItemId = adapter.getItemId(position - 1); 205 mBelowItemId = adapter.getItemId(position + 1); 206 } 207 208 /** Retrieves the view in the list corresponding to itemID */ 209 public View getViewForID (long itemID) { 210 int firstVisiblePosition = getFirstVisiblePosition(); 211 StableArrayAdapter adapter = ((StableArrayAdapter)getAdapter()); 212 for(int i = 0; i < getChildCount(); i++) { 213 View v = getChildAt(i); 214 int position = firstVisiblePosition + i; 215 long id = adapter.getItemId(position); 216 if (id == itemID) { 217 return v; 218 } 219 } 220 return null; 221 } 222 223 /** Retrieves the position in the list corresponding to itemID */ 224 public int getPositionForID (long itemID) { 225 View v = getViewForID(itemID); 226 if (v == null) { 227 return -1; 228 } else { 229 return getPositionForView(v); 230 } 231 } 232 233 /** 234 * dispatchDraw gets invoked when all the child views are about to be drawn. 235 * By overriding this method, the hover cell (BitmapDrawable) can be drawn 236 * over the listview's items whenever the listview is redrawn. 237 */ 238 @Override 239 protected void dispatchDraw(Canvas canvas) { 240 super.dispatchDraw(canvas); 241 if (mHoverCell != null) { 242 mHoverCell.draw(canvas); 243 } 244 } 245 246 @Override 247 public boolean onTouchEvent (MotionEvent event) { 248 249 switch (event.getAction() & MotionEvent.ACTION_MASK) { 250 case MotionEvent.ACTION_DOWN: 251 mDownX = (int)event.getX(); 252 mDownY = (int)event.getY(); 253 mActivePointerId = event.getPointerId(0); 254 break; 255 case MotionEvent.ACTION_MOVE: 256 if (mActivePointerId == INVALID_POINTER_ID) { 257 break; 258 } 259 260 int pointerIndex = event.findPointerIndex(mActivePointerId); 261 262 mLastEventY = (int) event.getY(pointerIndex); 263 int deltaY = mLastEventY - mDownY; 264 265 if (mCellIsMobile) { 266 mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, 267 mHoverCellOriginalBounds.top + deltaY + mTotalOffset); 268 mHoverCell.setBounds(mHoverCellCurrentBounds); 269 invalidate(); 270 271 handleCellSwitch(); 272 273 mIsMobileScrolling = false; 274 handleMobileCellScroll(); 275 276 return false; 277 } 278 break; 279 case MotionEvent.ACTION_UP: 280 touchEventsEnded(); 281 break; 282 case MotionEvent.ACTION_CANCEL: 283 touchEventsCancelled(); 284 break; 285 case MotionEvent.ACTION_POINTER_UP: 286 /* If a multitouch event took place and the original touch dictating 287 * the movement of the hover cell has ended, then the dragging event 288 * ends and the hover cell is animated to its corresponding position 289 * in the listview. */ 290 pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 291 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 292 final int pointerId = event.getPointerId(pointerIndex); 293 if (pointerId == mActivePointerId) { 294 touchEventsEnded(); 295 } 296 break; 297 default: 298 break; 299 } 300 301 return super.onTouchEvent(event); 302 } 303 304 /** 305 * This method determines whether the hover cell has been shifted far enough 306 * to invoke a cell swap. If so, then the respective cell swap candidate is 307 * determined and the data set is changed. Upon posting a notification of the 308 * data set change, a layout is invoked to place the cells in the right place. 309 * Using a ViewTreeObserver and a corresponding OnPreDrawListener, we can 310 * offset the cell being swapped to where it previously was and then animate it to 311 * its new position. 312 */ 313 private void handleCellSwitch() { 314 final int deltaY = mLastEventY - mDownY; 315 int deltaYTotal = mHoverCellOriginalBounds.top + mTotalOffset + deltaY; 316 317 View belowView = getViewForID(mBelowItemId); 318 View mobileView = getViewForID(mMobileItemId); 319 View aboveView = getViewForID(mAboveItemId); 320 321 boolean isBelow = (belowView != null) && (deltaYTotal > belowView.getTop()); 322 boolean isAbove = (aboveView != null) && (deltaYTotal < aboveView.getTop()); 323 324 if (isBelow || isAbove) { 325 326 final long switchItemID = isBelow ? mBelowItemId : mAboveItemId; 327 View switchView = isBelow ? belowView : aboveView; 328 final int originalItem = getPositionForView(mobileView); 329 330 if (switchView == null) { 331 updateNeighborViewsForID(mMobileItemId); 332 return; 333 } 334 335 swapElements(mCheeseList, originalItem, getPositionForView(switchView)); 336 337 ((BaseAdapter) getAdapter()).notifyDataSetChanged(); 338 339 mDownY = mLastEventY; 340 341 final int switchViewStartTop = switchView.getTop(); 342 343 mobileView.setVisibility(View.VISIBLE); 344 switchView.setVisibility(View.INVISIBLE); 345 346 updateNeighborViewsForID(mMobileItemId); 347 348 final ViewTreeObserver observer = getViewTreeObserver(); 349 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 350 public boolean onPreDraw() { 351 observer.removeOnPreDrawListener(this); 352 353 View switchView = getViewForID(switchItemID); 354 355 mTotalOffset += deltaY; 356 357 int switchViewNewTop = switchView.getTop(); 358 int delta = switchViewStartTop - switchViewNewTop; 359 360 switchView.setTranslationY(delta); 361 362 ObjectAnimator animator = ObjectAnimator.ofFloat(switchView, 363 View.TRANSLATION_Y, 0); 364 animator.setDuration(MOVE_DURATION); 365 animator.start(); 366 367 return true; 368 } 369 }); 370 } 371 } 372 373 private void swapElements(ArrayList arrayList, int indexOne, int indexTwo) { 374 Object temp = arrayList.get(indexOne); 375 arrayList.set(indexOne, arrayList.get(indexTwo)); 376 arrayList.set(indexTwo, temp); 377 } 378 379 380 /** 381 * Resets all the appropriate fields to a default state while also animating 382 * the hover cell back to its correct location. 383 */ 384 private void touchEventsEnded () { 385 final View mobileView = getViewForID(mMobileItemId); 386 if (mCellIsMobile|| mIsWaitingForScrollFinish) { 387 mCellIsMobile = false; 388 mIsWaitingForScrollFinish = false; 389 mIsMobileScrolling = false; 390 mActivePointerId = INVALID_POINTER_ID; 391 392 // If the autoscroller has not completed scrolling, we need to wait for it to 393 // finish in order to determine the final location of where the hover cell 394 // should be animated to. 395 if (mScrollState != OnScrollListener.SCROLL_STATE_IDLE) { 396 mIsWaitingForScrollFinish = true; 397 return; 398 } 399 400 mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, mobileView.getTop()); 401 402 ObjectAnimator hoverViewAnimator = ObjectAnimator.ofObject(mHoverCell, "bounds", 403 sBoundEvaluator, mHoverCellCurrentBounds); 404 hoverViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 405 @Override 406 public void onAnimationUpdate(ValueAnimator valueAnimator) { 407 invalidate(); 408 } 409 }); 410 hoverViewAnimator.addListener(new AnimatorListenerAdapter() { 411 @Override 412 public void onAnimationStart(Animator animation) { 413 setEnabled(false); 414 } 415 416 @Override 417 public void onAnimationEnd(Animator animation) { 418 mAboveItemId = INVALID_ID; 419 mMobileItemId = INVALID_ID; 420 mBelowItemId = INVALID_ID; 421 mobileView.setVisibility(VISIBLE); 422 mHoverCell = null; 423 setEnabled(true); 424 invalidate(); 425 } 426 }); 427 hoverViewAnimator.start(); 428 } else { 429 touchEventsCancelled(); 430 } 431 } 432 433 /** 434 * Resets all the appropriate fields to a default state. 435 */ 436 private void touchEventsCancelled () { 437 View mobileView = getViewForID(mMobileItemId); 438 if (mCellIsMobile) { 439 mAboveItemId = INVALID_ID; 440 mMobileItemId = INVALID_ID; 441 mBelowItemId = INVALID_ID; 442 mobileView.setVisibility(VISIBLE); 443 mHoverCell = null; 444 invalidate(); 445 } 446 mCellIsMobile = false; 447 mIsMobileScrolling = false; 448 mActivePointerId = INVALID_POINTER_ID; 449 } 450 451 /** 452 * This TypeEvaluator is used to animate the BitmapDrawable back to its 453 * final location when the user lifts his finger by modifying the 454 * BitmapDrawable's bounds. 455 */ 456 private final static TypeEvaluator<Rect> sBoundEvaluator = new TypeEvaluator<Rect>() { 457 public Rect evaluate(float fraction, Rect startValue, Rect endValue) { 458 return new Rect(interpolate(startValue.left, endValue.left, fraction), 459 interpolate(startValue.top, endValue.top, fraction), 460 interpolate(startValue.right, endValue.right, fraction), 461 interpolate(startValue.bottom, endValue.bottom, fraction)); 462 } 463 464 public int interpolate(int start, int end, float fraction) { 465 return (int)(start + fraction * (end - start)); 466 } 467 }; 468 469 /** 470 * Determines whether this listview is in a scrolling state invoked 471 * by the fact that the hover cell is out of the bounds of the listview; 472 */ 473 private void handleMobileCellScroll() { 474 mIsMobileScrolling = handleMobileCellScroll(mHoverCellCurrentBounds); 475 } 476 477 /** 478 * This method is in charge of determining if the hover cell is above 479 * or below the bounds of the listview. If so, the listview does an appropriate 480 * upward or downward smooth scroll so as to reveal new items. 481 */ 482 public boolean handleMobileCellScroll(Rect r) { 483 int offset = computeVerticalScrollOffset(); 484 int height = getHeight(); 485 int extent = computeVerticalScrollExtent(); 486 int range = computeVerticalScrollRange(); 487 int hoverViewTop = r.top; 488 int hoverHeight = r.height(); 489 490 if (hoverViewTop <= 0 && offset > 0) { 491 smoothScrollBy(-mSmoothScrollAmountAtEdge, 0); 492 return true; 493 } 494 495 if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) { 496 smoothScrollBy(mSmoothScrollAmountAtEdge, 0); 497 return true; 498 } 499 500 return false; 501 } 502 503 public void setCheeseList(ArrayList<String> cheeseList) { 504 mCheeseList = cheeseList; 505 } 506 507 /** 508 * This scroll listener is added to the listview in order to handle cell swapping 509 * when the cell is either at the top or bottom edge of the listview. If the hover 510 * cell is at either edge of the listview, the listview will begin scrolling. As 511 * scrolling takes place, the listview continuously checks if new cells became visible 512 * and determines whether they are potential candidates for a cell swap. 513 */ 514 private AbsListView.OnScrollListener mScrollListener = new AbsListView.OnScrollListener () { 515 516 private int mPreviousFirstVisibleItem = -1; 517 private int mPreviousVisibleItemCount = -1; 518 private int mCurrentFirstVisibleItem; 519 private int mCurrentVisibleItemCount; 520 private int mCurrentScrollState; 521 522 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 523 int totalItemCount) { 524 mCurrentFirstVisibleItem = firstVisibleItem; 525 mCurrentVisibleItemCount = visibleItemCount; 526 527 mPreviousFirstVisibleItem = (mPreviousFirstVisibleItem == -1) ? mCurrentFirstVisibleItem 528 : mPreviousFirstVisibleItem; 529 mPreviousVisibleItemCount = (mPreviousVisibleItemCount == -1) ? mCurrentVisibleItemCount 530 : mPreviousVisibleItemCount; 531 532 checkAndHandleFirstVisibleCellChange(); 533 checkAndHandleLastVisibleCellChange(); 534 535 mPreviousFirstVisibleItem = mCurrentFirstVisibleItem; 536 mPreviousVisibleItemCount = mCurrentVisibleItemCount; 537 } 538 539 @Override 540 public void onScrollStateChanged(AbsListView view, int scrollState) { 541 mCurrentScrollState = scrollState; 542 mScrollState = scrollState; 543 isScrollCompleted(); 544 } 545 546 /** 547 * This method is in charge of invoking 1 of 2 actions. Firstly, if the listview 548 * is in a state of scrolling invoked by the hover cell being outside the bounds 549 * of the listview, then this scrolling event is continued. Secondly, if the hover 550 * cell has already been released, this invokes the animation for the hover cell 551 * to return to its correct position after the listview has entered an idle scroll 552 * state. 553 */ 554 private void isScrollCompleted() { 555 if (mCurrentVisibleItemCount > 0 && mCurrentScrollState == SCROLL_STATE_IDLE) { 556 if (mCellIsMobile && mIsMobileScrolling) { 557 handleMobileCellScroll(); 558 } else if (mIsWaitingForScrollFinish) { 559 touchEventsEnded(); 560 } 561 } 562 } 563 564 /** 565 * Determines if the listview scrolled up enough to reveal a new cell at the 566 * top of the list. If so, then the appropriate parameters are updated. 567 */ 568 public void checkAndHandleFirstVisibleCellChange() { 569 if (mCurrentFirstVisibleItem != mPreviousFirstVisibleItem) { 570 if (mCellIsMobile && mMobileItemId != INVALID_ID) { 571 updateNeighborViewsForID(mMobileItemId); 572 handleCellSwitch(); 573 } 574 } 575 } 576 577 /** 578 * Determines if the listview scrolled down enough to reveal a new cell at the 579 * bottom of the list. If so, then the appropriate parameters are updated. 580 */ 581 public void checkAndHandleLastVisibleCellChange() { 582 int currentLastVisibleItem = mCurrentFirstVisibleItem + mCurrentVisibleItemCount; 583 int previousLastVisibleItem = mPreviousFirstVisibleItem + mPreviousVisibleItemCount; 584 if (currentLastVisibleItem != previousLastVisibleItem) { 585 if (mCellIsMobile && mMobileItemId != INVALID_ID) { 586 updateNeighborViewsForID(mMobileItemId); 587 handleCellSwitch(); 588 } 589 } 590 } 591 }; 592 }