Home | History | Annotate | Download | only in listviewitemanimations
      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