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