• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DynamicListView(Context context)98     public DynamicListView(Context context) {
99         super(context);
100         init(context);
101     }
102 
DynamicListView(Context context, AttributeSet attrs, int defStyle)103     public DynamicListView(Context context, AttributeSet attrs, int defStyle) {
104         super(context, attrs, defStyle);
105         init(context);
106     }
107 
DynamicListView(Context context, AttributeSet attrs)108     public DynamicListView(Context context, AttributeSet attrs) {
109         super(context, attrs);
110         init(context);
111     }
112 
init(Context context)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      */
getAndAddHoverView(View v)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. */
getBitmapWithBorder(View v)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. */
getBitmapFromView(View v)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      */
updateNeighborViewsForID(long itemID)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 */
getViewForID(long 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 */
getPositionForID(long 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
dispatchDraw(Canvas canvas)239     protected void dispatchDraw(Canvas canvas) {
240         super.dispatchDraw(canvas);
241         if (mHoverCell != null) {
242             mHoverCell.draw(canvas);
243         }
244     }
245 
246     @Override
onTouchEvent(MotionEvent event)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      */
handleCellSwitch()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 
swapElements(ArrayList arrayList, int indexOne, int indexTwo)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      */
touchEventsEnded()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      */
touchEventsCancelled()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 their 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      */
handleMobileCellScroll()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      */
handleMobileCellScroll(Rect r)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 
setCheeseList(ArrayList<String> cheeseList)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 }