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.insertingcells;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.PropertyValuesHolder;
24 import android.animation.TypeEvaluator;
25 import android.animation.ValueAnimator;
26 import android.app.Activity;
27 import android.content.Context;
28 import android.graphics.Bitmap;
29 import android.graphics.BitmapFactory;
30 import android.graphics.Canvas;
31 import android.graphics.Point;
32 import android.graphics.Rect;
33 import android.graphics.drawable.BitmapDrawable;
34 import android.util.AttributeSet;
35 import android.util.DisplayMetrics;
36 import android.view.View;
37 import android.view.ViewTreeObserver;
38 import android.view.animation.OvershootInterpolator;
39 import android.widget.ImageView;
40 import android.widget.ListView;
41 import android.widget.RelativeLayout;
42 import android.widget.TextView;
43 
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 
48 /**
49  * This ListView displays a set of ListItemObjects. By calling addRow with a new
50  * ListItemObject, it is added to the top of the ListView and the new row is animated
51  * in. If the ListView content is at the top (the scroll offset is 0), the animation of
52  * the new row is accompanied by an extra image animation that pops into place in its
53  * corresponding item in the ListView.
54  */
55 public class InsertionListView extends ListView {
56 
57     private static final int NEW_ROW_DURATION = 500;
58     private static final int OVERSHOOT_INTERPOLATOR_TENSION = 5;
59 
60     private OvershootInterpolator sOvershootInterpolator;
61 
62     private RelativeLayout mLayout;
63 
64     private Context mContext;
65 
66     private OnRowAdditionAnimationListener mRowAdditionAnimationListener;
67 
68     private List<ListItemObject> mData;
69     private List<BitmapDrawable> mCellBitmapDrawables;
70 
InsertionListView(Context context)71     public InsertionListView(Context context) {
72         super(context);
73         init(context);
74     }
75 
InsertionListView(Context context, AttributeSet attrs)76     public InsertionListView(Context context, AttributeSet attrs) {
77         super(context, attrs);
78         init(context);
79     }
80 
InsertionListView(Context context, AttributeSet attrs, int defStyle)81     public InsertionListView(Context context, AttributeSet attrs, int defStyle) {
82         super(context, attrs, defStyle);
83         init(context);
84     }
85 
init(Context context)86     public void init(Context context) {
87         setDivider(null);
88         mContext = context;
89         mCellBitmapDrawables = new ArrayList<BitmapDrawable>();
90         sOvershootInterpolator = new OvershootInterpolator(OVERSHOOT_INTERPOLATOR_TENSION);
91     }
92 
93     /**
94      * Modifies the underlying data set and adapter through the addition of the new object
95      * to the first item of the ListView. The new cell is then animated into place from
96      * above the bounds of the ListView.
97      */
addRow(ListItemObject newObj)98     public void addRow(ListItemObject newObj) {
99 
100         final CustomArrayAdapter adapter = (CustomArrayAdapter)getAdapter();
101 
102         /**
103          * Stores the starting bounds and the corresponding bitmap drawables of every
104          * cell present in the ListView before the data set change takes place.
105          */
106         final HashMap<Long, Rect> listViewItemBounds = new HashMap<Long, Rect>();
107         final HashMap<Long, BitmapDrawable> listViewItemDrawables = new HashMap<Long,
108                 BitmapDrawable>();
109 
110         int firstVisiblePosition = getFirstVisiblePosition();
111         for (int i = 0; i < getChildCount(); ++i) {
112             View child = getChildAt(i);
113             int position = firstVisiblePosition + i;
114             long itemID = adapter.getItemId(position);
115             Rect startRect = new Rect(child.getLeft(), child.getTop(), child.getRight(),
116                     child.getBottom());
117             listViewItemBounds.put(itemID, startRect);
118             listViewItemDrawables.put(itemID, getBitmapDrawableFromView(child));
119         }
120 
121         /** Adds the new object to the data set, thereby modifying the adapter,
122          *  as well as adding a stable Id for that specified object.*/
123         mData.add(0, newObj);
124         adapter.addStableIdForDataAtPosition(0);
125         adapter.notifyDataSetChanged();
126 
127         final ViewTreeObserver observer = getViewTreeObserver();
128         observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
129             public boolean onPreDraw() {
130                 observer.removeOnPreDrawListener(this);
131 
132                 ArrayList<Animator> animations = new ArrayList<Animator>();
133 
134                 final View newCell = getChildAt(0);
135                 final ImageView imgView = (ImageView)newCell.findViewById(R.id.image_view);
136                 final ImageView copyImgView = new ImageView(mContext);
137 
138                 int firstVisiblePosition = getFirstVisiblePosition();
139                 final boolean shouldAnimateInNewRow = shouldAnimateInNewRow();
140                 final boolean shouldAnimateInImage = shouldAnimateInNewImage();
141 
142                 if (shouldAnimateInNewRow) {
143                     /** Fades in the text of the first cell. */
144                     TextView textView = (TextView)newCell.findViewById(R.id.text_view);
145                     ObjectAnimator textAlphaAnimator = ObjectAnimator.ofFloat(textView,
146                             View.ALPHA, 0.0f, 1.0f);
147                     animations.add(textAlphaAnimator);
148 
149                     /** Animates in the extra hover view corresponding to the image
150                      * in the top row of the ListView. */
151                     if (shouldAnimateInImage) {
152 
153                         int width = imgView.getWidth();
154                         int height = imgView.getHeight();
155 
156                         Point childLoc = getLocationOnScreen(newCell);
157                         Point layoutLoc = getLocationOnScreen(mLayout);
158 
159                         ListItemObject obj = mData.get(0);
160                         Bitmap bitmap = CustomArrayAdapter.getCroppedBitmap(BitmapFactory
161                                 .decodeResource(mContext.getResources(), obj.getImgResource(),
162                                         null));
163                         copyImgView.setImageBitmap(bitmap);
164 
165                         imgView.setVisibility(View.INVISIBLE);
166 
167                         copyImgView.setScaleType(ImageView.ScaleType.CENTER);
168 
169                         ObjectAnimator imgViewTranslation = ObjectAnimator.ofFloat(copyImgView,
170                                 View.Y, childLoc.y - layoutLoc.y);
171 
172                         PropertyValuesHolder imgViewScaleY = PropertyValuesHolder.ofFloat
173                                 (View.SCALE_Y, 0, 1.0f);
174                         PropertyValuesHolder imgViewScaleX = PropertyValuesHolder.ofFloat
175                                 (View.SCALE_X, 0, 1.0f);
176                         ObjectAnimator imgViewScaleAnimator = ObjectAnimator
177                                 .ofPropertyValuesHolder(copyImgView, imgViewScaleX, imgViewScaleY);
178                         imgViewScaleAnimator.setInterpolator(sOvershootInterpolator);
179                         animations.add(imgViewTranslation);
180                         animations.add(imgViewScaleAnimator);
181 
182                         RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams
183                                 (width, height);
184 
185                         mLayout.addView(copyImgView, params);
186                     }
187                 }
188 
189                 /** Loops through all the current visible cells in the ListView and animates
190                  * all of them into their post layout positions from their original positions.*/
191                 for (int i = 0; i < getChildCount(); i++) {
192                     View child = getChildAt(i);
193                     int position = firstVisiblePosition + i;
194                     long itemId = adapter.getItemId(position);
195                     Rect startRect = listViewItemBounds.get(itemId);
196                     int top = child.getTop();
197                     if (startRect != null) {
198                         /** If the cell was visible before the data set change and
199                          * after the data set change, then animate the cell between
200                          * the two positions.*/
201                         int startTop = startRect.top;
202                         int delta = startTop - top;
203                         ObjectAnimator animation = ObjectAnimator.ofFloat(child,
204                                 View.TRANSLATION_Y, delta, 0);
205                         animations.add(animation);
206                     } else {
207                         /** If the cell was not visible (or present) before the data set
208                          * change but is visible after the data set change, then use its
209                          * height to determine the delta by which it should be animated.*/
210                         int childHeight = child.getHeight() + getDividerHeight();
211                         int startTop = top + (i > 0 ? childHeight : -childHeight);
212                         int delta = startTop - top;
213                         ObjectAnimator animation = ObjectAnimator.ofFloat(child,
214                                 View.TRANSLATION_Y, delta, 0);
215                         animations.add(animation);
216                     }
217                     listViewItemBounds.remove(itemId);
218                     listViewItemDrawables.remove(itemId);
219                 }
220 
221                 /**
222                  * Loops through all the cells that were visible before the data set
223                  * changed but not after, and keeps track of their corresponding
224                  * drawables. The bounds of each drawable are then animated from the
225                  * original state to the new one (off the screen). By storing all
226                  * the drawables that meet this criteria, they can be redrawn on top
227                  * of the ListView via dispatchDraw as they are animating.
228                  */
229                 for (Long itemId: listViewItemBounds.keySet()) {
230                     BitmapDrawable bitmapDrawable = listViewItemDrawables.get(itemId);
231                     Rect startBounds = listViewItemBounds.get(itemId);
232                     bitmapDrawable.setBounds(startBounds);
233 
234                     int childHeight = startBounds.bottom - startBounds.top + getDividerHeight();
235                     Rect endBounds = new Rect(startBounds);
236                     endBounds.offset(0, childHeight);
237 
238                     ObjectAnimator animation = ObjectAnimator.ofObject(bitmapDrawable,
239                             "bounds", sBoundsEvaluator, startBounds, endBounds);
240                     animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
241                         private Rect mLastBound = null;
242                         private Rect mCurrentBound = new Rect();
243                         @Override
244                         public void onAnimationUpdate(ValueAnimator valueAnimator) {
245                             Rect bounds = (Rect)valueAnimator.getAnimatedValue();
246                             mCurrentBound.set(bounds);
247                             if (mLastBound != null) {
248                                 mCurrentBound.union(mLastBound);
249                             }
250                             mLastBound = bounds;
251                             invalidate(mCurrentBound);
252                         }
253                     });
254 
255                     listViewItemBounds.remove(itemId);
256                     listViewItemDrawables.remove(itemId);
257 
258                     mCellBitmapDrawables.add(bitmapDrawable);
259 
260                     animations.add(animation);
261                 }
262 
263                 /** Animates all the cells from their old position to their new position
264                  *  at the same time.*/
265                 setEnabled(false);
266                 mRowAdditionAnimationListener.onRowAdditionAnimationStart();
267                 AnimatorSet set = new AnimatorSet();
268                 set.setDuration(NEW_ROW_DURATION);
269                 set.playTogether(animations);
270                 set.addListener(new AnimatorListenerAdapter() {
271                     @Override
272                     public void onAnimationEnd(Animator animation) {
273                         mCellBitmapDrawables.clear();
274                         imgView.setVisibility(View.VISIBLE);
275                         mLayout.removeView(copyImgView);
276                         mRowAdditionAnimationListener.onRowAdditionAnimationEnd();
277                         setEnabled(true);
278                         invalidate();
279                     }
280                 });
281                 set.start();
282 
283                 listViewItemBounds.clear();
284                 listViewItemDrawables.clear();
285                 return true;
286             }
287         });
288     }
289 
290     /**
291      * By overriding dispatchDraw, the BitmapDrawables of all the cells that were on the
292      * screen before (but not after) the layout are drawn and animated off the screen.
293      */
294     @Override
dispatchDraw(Canvas canvas)295     protected void dispatchDraw (Canvas canvas) {
296         super.dispatchDraw(canvas);
297         if (mCellBitmapDrawables.size() > 0) {
298             for (BitmapDrawable bitmapDrawable: mCellBitmapDrawables) {
299                 bitmapDrawable.draw(canvas);
300             }
301         }
302     }
303 
shouldAnimateInNewRow()304     public boolean shouldAnimateInNewRow() {
305         int firstVisiblePosition = getFirstVisiblePosition();
306         return (firstVisiblePosition == 0);
307     }
308 
shouldAnimateInNewImage()309     public boolean shouldAnimateInNewImage() {
310         if (getChildCount() == 0) {
311             return true;
312         }
313         boolean shouldAnimateInNewRow = shouldAnimateInNewRow();
314         View topCell = getChildAt(0);
315         return (shouldAnimateInNewRow && topCell.getTop() == 0);
316     }
317 
318     /** Returns a bitmap drawable showing a screenshot of the view passed in. */
getBitmapDrawableFromView(View v)319     private BitmapDrawable getBitmapDrawableFromView(View v) {
320         Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
321         Canvas canvas = new Canvas (bitmap);
322         v.draw(canvas);
323         return new BitmapDrawable(getResources(), bitmap);
324     }
325 
326     /**
327      * Returns the absolute x,y coordinates of the view relative to the top left
328      * corner of the phone screen.
329      */
getLocationOnScreen(View v)330     public Point getLocationOnScreen(View v) {
331         DisplayMetrics dm = new DisplayMetrics();
332         ((Activity)getContext()).getWindowManager().getDefaultDisplay().getMetrics(dm);
333 
334         int[] location = new int[2];
335         v.getLocationOnScreen(location);
336 
337         return new Point(location[0], location[1]);
338     }
339 
340     /** Setter for the underlying data set controlling the adapter. */
setData(List<ListItemObject> data)341     public void setData(List<ListItemObject> data) {
342         mData = data;
343     }
344 
345     /**
346      * Setter for the parent RelativeLayout of this ListView. A reference to this
347      * ViewGroup is required in order to add the custom animated overlaying bitmap
348      * when adding a new row.
349      */
setLayout(RelativeLayout layout)350     public void setLayout(RelativeLayout layout) {
351         mLayout = layout;
352     }
353 
setRowAdditionAnimationListener(OnRowAdditionAnimationListener rowAdditionAnimationListener)354     public void setRowAdditionAnimationListener(OnRowAdditionAnimationListener
355                                                         rowAdditionAnimationListener) {
356         mRowAdditionAnimationListener = rowAdditionAnimationListener;
357     }
358 
359     /**
360      * This TypeEvaluator is used to animate the position of a BitmapDrawable
361      * by updating its bounds.
362      */
363     static final TypeEvaluator<Rect> sBoundsEvaluator = new TypeEvaluator<Rect>() {
364         public Rect evaluate(float fraction, Rect startValue, Rect endValue) {
365             return new Rect(interpolate(startValue.left, endValue.left, fraction),
366                     interpolate(startValue.top, endValue.top, fraction),
367                     interpolate(startValue.right, endValue.right, fraction),
368                     interpolate(startValue.bottom, endValue.bottom, fraction));
369         }
370 
371         public int interpolate(int start, int end, float fraction) {
372             return (int)(start + fraction * (end - start));
373         }
374     };
375 
376 }
377