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.listviewitemanimations; 18 19 import java.util.ArrayList; 20 import java.util.HashMap; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.ObjectAnimator; 25 import android.annotation.SuppressLint; 26 import android.app.Activity; 27 import android.os.Bundle; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.ViewConfiguration; 31 import android.view.ViewTreeObserver; 32 import android.view.animation.AlphaAnimation; 33 import android.view.animation.Animation; 34 import android.view.animation.Animation.AnimationListener; 35 import android.view.animation.AnimationSet; 36 import android.view.animation.TranslateAnimation; 37 import android.widget.ListView; 38 39 /** 40 * This example shows how to use a swipe effect to remove items from a ListView, 41 * and how to use animations to complete the swipe as well as to animate the other 42 * items in the list into their final places. This code works on runtimes back to Gingerbread 43 * (Android 2.3), by using the android.view.animation classes on earlier releases. 44 * 45 * Watch the associated video for this demo on the DevBytes channel of developer.android.com 46 * or on the DevBytes playlist in the androiddevelopers channel on YouTube at 47 * https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_XOgcRukSoKKjewFJZrKV0. 48 */ 49 public class ListViewItemAnimations extends Activity { 50 51 final ArrayList<View> mCheckedViews = new ArrayList<View>(); 52 StableArrayAdapter mAdapter; 53 ListView mListView; 54 BackgroundContainer mBackgroundContainer; 55 boolean mSwiping = false; 56 boolean mItemPressed = false; 57 HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>(); 58 boolean mAnimating = false; 59 float mCurrentX = 0; 60 float mCurrentAlpha = 1; 61 62 private static final int SWIPE_DURATION = 250; 63 private static final int MOVE_DURATION = 150; 64 65 @Override 66 protected void onCreate(Bundle savedInstanceState) { 67 super.onCreate(savedInstanceState); 68 setContentView(R.layout.activity_list_view_item_animations); 69 70 mBackgroundContainer = (BackgroundContainer) findViewById(R.id.listViewBackground); 71 mListView = (ListView) findViewById(R.id.listview); 72 final ArrayList<String> cheeseList = new ArrayList<String>(); 73 for (int i = 0; i < Cheeses.sCheeseStrings.length; ++i) { 74 cheeseList.add(Cheeses.sCheeseStrings[i]); 75 } 76 mAdapter = new StableArrayAdapter(this,R.layout.opaque_text_view, cheeseList, 77 mTouchListener); 78 mListView.setAdapter(mAdapter); 79 } 80 81 /** 82 * Returns true if the current runtime is Honeycomb or later 83 */ 84 private boolean isRuntimePostGingerbread() { 85 return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB; 86 } 87 88 private View.OnTouchListener mTouchListener = new View.OnTouchListener() { 89 90 float mDownX; 91 private int mSwipeSlop = -1; 92 93 @SuppressLint("NewApi") 94 @Override 95 public boolean onTouch(final View v, MotionEvent event) { 96 if (mSwipeSlop < 0) { 97 mSwipeSlop = ViewConfiguration.get(ListViewItemAnimations.this). 98 getScaledTouchSlop(); 99 } 100 switch (event.getAction()) { 101 case MotionEvent.ACTION_DOWN: 102 if (mAnimating) { 103 // Multi-item swipes not handled 104 return true; 105 } 106 mItemPressed = true; 107 mDownX = event.getX(); 108 break; 109 case MotionEvent.ACTION_CANCEL: 110 setSwipePosition(v, 0); 111 mItemPressed = false; 112 break; 113 case MotionEvent.ACTION_MOVE: 114 { 115 if (mAnimating) { 116 return true; 117 } 118 float x = event.getX(); 119 if (isRuntimePostGingerbread()) { 120 x += v.getTranslationX(); 121 } 122 float deltaX = x - mDownX; 123 float deltaXAbs = Math.abs(deltaX); 124 if (!mSwiping) { 125 if (deltaXAbs > mSwipeSlop) { 126 mSwiping = true; 127 mListView.requestDisallowInterceptTouchEvent(true); 128 mBackgroundContainer.showBackground(v.getTop(), v.getHeight()); 129 } 130 } 131 if (mSwiping) { 132 setSwipePosition(v, deltaX); 133 } 134 } 135 break; 136 case MotionEvent.ACTION_UP: 137 { 138 if (mAnimating) { 139 return true; 140 } 141 // User let go - figure out whether to animate the view out, or back into place 142 if (mSwiping) { 143 float x = event.getX(); 144 if (isRuntimePostGingerbread()) { 145 x += v.getTranslationX(); 146 } 147 float deltaX = x - mDownX; 148 float deltaXAbs = Math.abs(deltaX); 149 float fractionCovered; 150 float endX; 151 final boolean remove; 152 if (deltaXAbs > v.getWidth() / 4) { 153 // Greater than a quarter of the width - animate it out 154 fractionCovered = deltaXAbs / v.getWidth(); 155 endX = deltaX < 0 ? -v.getWidth() : v.getWidth(); 156 remove = true; 157 } else { 158 // Not far enough - animate it back 159 fractionCovered = 1 - (deltaXAbs / v.getWidth()); 160 endX = 0; 161 remove = false; 162 } 163 // Animate position and alpha 164 long duration = (int) ((1 - fractionCovered) * SWIPE_DURATION); 165 animateSwipe(v, endX, duration, remove); 166 } else { 167 mItemPressed = false; 168 } 169 } 170 break; 171 default: 172 return false; 173 } 174 return true; 175 } 176 }; 177 178 /** 179 * Animates a swipe of the item either back into place or out of the listview container. 180 * NOTE: This is a simplified version of swipe behavior, for the purposes of this demo 181 * about animation. A real version should use velocity (via the VelocityTracker class) 182 * to send the item off or back at an appropriate speed. 183 */ 184 @SuppressLint("NewApi") 185 private void animateSwipe(final View view, float endX, long duration, final boolean remove) { 186 mAnimating = true; 187 mListView.setEnabled(false); 188 if (isRuntimePostGingerbread()) { 189 view.animate().setDuration(duration). 190 alpha(remove ? 0 : 1).translationX(endX). 191 setListener(new AnimatorListenerAdapter() { 192 @Override 193 public void onAnimationEnd(Animator animation) { 194 // Restore animated values 195 view.setAlpha(1); 196 view.setTranslationX(0); 197 if (remove) { 198 animateOtherViews(mListView, view); 199 } else { 200 mBackgroundContainer.hideBackground(); 201 mSwiping = false; 202 mAnimating = false; 203 mListView.setEnabled(true); 204 } 205 mItemPressed = false; 206 } 207 }); 208 } else { 209 TranslateAnimation swipeAnim = new TranslateAnimation(mCurrentX, endX, 0, 0); 210 AlphaAnimation alphaAnim = new AlphaAnimation(mCurrentAlpha, remove ? 0 : 1); 211 AnimationSet set = new AnimationSet(true); 212 set.addAnimation(swipeAnim); 213 set.addAnimation(alphaAnim); 214 set.setDuration(duration); 215 view.startAnimation(set); 216 setAnimationEndAction(set, new Runnable() { 217 @Override 218 public void run() { 219 if (remove) { 220 animateOtherViews(mListView, view); 221 } else { 222 mBackgroundContainer.hideBackground(); 223 mSwiping = false; 224 mAnimating = false; 225 mListView.setEnabled(true); 226 } 227 mItemPressed = false; 228 } 229 }); 230 } 231 232 } 233 234 /** 235 * Sets the horizontal position and translucency of the view being swiped. 236 */ 237 @SuppressLint("NewApi") 238 private void setSwipePosition(View view, float deltaX) { 239 float fraction = Math.abs(deltaX) / view.getWidth(); 240 if (isRuntimePostGingerbread()) { 241 view.setTranslationX(deltaX); 242 view.setAlpha(1 - fraction); 243 } else { 244 // Hello, Gingerbread! 245 TranslateAnimation swipeAnim = new TranslateAnimation(deltaX, deltaX, 0, 0); 246 mCurrentX = deltaX; 247 mCurrentAlpha = (1 - fraction); 248 AlphaAnimation alphaAnim = new AlphaAnimation(mCurrentAlpha, mCurrentAlpha); 249 AnimationSet set = new AnimationSet(true); 250 set.addAnimation(swipeAnim); 251 set.addAnimation(alphaAnim); 252 set.setFillAfter(true); 253 set.setFillEnabled(true); 254 view.startAnimation(set); 255 } 256 } 257 258 /** 259 * This method animates all other views in the ListView container (not including ignoreView) 260 * into their final positions. It is called after ignoreView has been removed from the 261 * adapter, but before layout has been run. The approach here is to figure out where 262 * everything is now, then allow layout to run, then figure out where everything is after 263 * layout, and then to run animations between all of those start/end positions. 264 */ 265 private void animateOtherViews(final ListView listview, View viewToRemove) { 266 int firstVisiblePosition = listview.getFirstVisiblePosition(); 267 for (int i = 0; i < listview.getChildCount(); ++i) { 268 View child = listview.getChildAt(i); 269 int position = firstVisiblePosition + i; 270 long itemId = mAdapter.getItemId(position); 271 if (child != viewToRemove) { 272 mItemIdTopMap.put(itemId, child.getTop()); 273 } 274 } 275 // Delete the item from the adapter 276 int position = mListView.getPositionForView(viewToRemove); 277 mAdapter.remove(mAdapter.getItem(position)); 278 279 // After layout runs, capture position of all itemIDs, compare to pre-layout 280 // positions, and animate changes 281 final ViewTreeObserver observer = listview.getViewTreeObserver(); 282 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 283 public boolean onPreDraw() { 284 observer.removeOnPreDrawListener(this); 285 boolean firstAnimation = true; 286 int firstVisiblePosition = listview.getFirstVisiblePosition(); 287 for (int i = 0; i < listview.getChildCount(); ++i) { 288 final View child = listview.getChildAt(i); 289 int position = firstVisiblePosition + i; 290 long itemId = mAdapter.getItemId(position); 291 Integer startTop = mItemIdTopMap.get(itemId); 292 int top = child.getTop(); 293 if (startTop == null) { 294 // Animate new views along with the others. The catch is that they did not 295 // exist in the start state, so we must calculate their starting position 296 // based on whether they're coming in from the bottom (i > 0) or top. 297 int childHeight = child.getHeight() + listview.getDividerHeight(); 298 startTop = top + (i > 0 ? childHeight : -childHeight); 299 } 300 int delta = startTop - top; 301 if (delta != 0) { 302 Runnable endAction = firstAnimation ? 303 new Runnable() { 304 public void run() { 305 mBackgroundContainer.hideBackground(); 306 mSwiping = false; 307 mAnimating = false; 308 mListView.setEnabled(true); 309 } 310 } : 311 null; 312 firstAnimation = false; 313 moveView(child, 0, 0, delta, 0, endAction); 314 } 315 } 316 mItemIdTopMap.clear(); 317 return true; 318 } 319 }); 320 } 321 322 /** 323 * Animate a view between start and end X/Y locations, using either old (pre-3.0) or 324 * new animation APIs. 325 */ 326 @SuppressLint("NewApi") 327 private void moveView(View view, float startX, float endX, float startY, float endY, 328 Runnable endAction) { 329 final Runnable finalEndAction = endAction; 330 if (isRuntimePostGingerbread()) { 331 view.animate().setDuration(MOVE_DURATION); 332 if (startX != endX) { 333 ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, startX, endX); 334 anim.setDuration(MOVE_DURATION); 335 anim.start(); 336 setAnimatorEndAction(anim, endAction); 337 endAction = null; 338 } 339 if (startY != endY) { 340 ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY); 341 anim.setDuration(MOVE_DURATION); 342 anim.start(); 343 setAnimatorEndAction(anim, endAction); 344 } 345 } else { 346 TranslateAnimation translator = new TranslateAnimation(startX, endX, startY, endY); 347 translator.setDuration(MOVE_DURATION); 348 view.startAnimation(translator); 349 if (endAction != null) { 350 view.getAnimation().setAnimationListener(new AnimationListenerAdapter() { 351 @Override 352 public void onAnimationEnd(Animation animation) { 353 finalEndAction.run(); 354 } 355 }); 356 } 357 } 358 } 359 360 @SuppressLint("NewApi") 361 private void setAnimatorEndAction(Animator animator, final Runnable endAction) { 362 if (endAction != null) { 363 animator.addListener(new AnimatorListenerAdapter() { 364 @Override 365 public void onAnimationEnd(Animator animation) { 366 endAction.run(); 367 } 368 }); 369 } 370 } 371 372 private void setAnimationEndAction(Animation animation, final Runnable endAction) { 373 if (endAction != null) { 374 animation.setAnimationListener(new AnimationListenerAdapter() { 375 @Override 376 public void onAnimationEnd(Animation animation) { 377 endAction.run(); 378 } 379 }); 380 } 381 } 382 383 /** 384 * Utility, to avoid having to implement every method in AnimationListener in 385 * every implementation class 386 */ 387 static class AnimationListenerAdapter implements AnimationListener { 388 389 @Override 390 public void onAnimationEnd(Animation animation) { 391 } 392 393 @Override 394 public void onAnimationRepeat(Animation animation) { 395 } 396 397 @Override 398 public void onAnimationStart(Animation animation) { 399 } 400 } 401 402 } 403