• Android 使用NineOldAndroids实现绚丽的ListView左右滑动删除Item效果


    转载请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/18311877),请尊重他人的辛勤劳动成果,谢谢!

    今天还是给大家带来自定义控件的编写,自定义一个ListView的左右滑动删除Item的效果,这个效果之前已经实现过了,有兴趣的可以看下Android 使用Scroller实现绚丽的ListView左右滑动删除Item效果, 之前使用的是滑动类Scroller来实现的,但是看了下通知栏的左右滑动删除效果,确实很棒,当我们滑动Item超过一半的时候,item的透明度就变 成了0,我们就知道抬起手指的时候item就被删除了,当item的透明度不为0的时候,我们抬起手指Item会回到起始位置,这样我们就知道拖动到什么 位置item会删除,什么位置Item不删除,用户体验更好了,还有一个效果,就是我们滑动删除了item的时候,ListView的其他item会出现 向上或者向下滚动的效果,感觉效果很棒,所以在GitHub上面搜索了下,发现很多开源库都有这个效果,比如ListViewAnimationsandroid-swipelistview等等,我看了下实现原理,使用的是Jake Wharton的动画开源库NineOldAndroids, 这个库究竟是干嘛的呢?在API3.0(Honeycomb), SDK新增了一个android.animation包,里面的类是实现动画效果相关的类,通过Honeycomb API,能够实现非常复杂的动画效果,但是如果开发者想在3.0以下使用这一套API, 则需要使用开源框架Nine Old Androids,在这个库中会根据我们运行的机器判断其SDK版本,如果是API3.0以上则使用Android自带的动画类,否则就使用Nine Old Androids库中,这是一个兼容库,接下来我们就来看看这个效果的具体实现吧

    实现该效果的主要思路

     

    1. 先根据手指触摸的点来获取点击的是ListView的哪一个Item
    2. 当手指在屏幕上面滑动的时候,我们要使得Item跟随手指的滑动而滑动
    3. 当我们抬起手指的时候,我们根据滑动的距离或者手指在屏幕上面的速度来判断Item是滑出屏幕还是滑动至其实位置
    4. Item滑出屏幕时,使ListView的其他item产生向上挤压或者向下挤压的效果

     

    大致的思路这是这四步,其中的一些细节接下来我会一一为大家解答的,接下来我们就用代码来实现这种效果吧

    首先我们新建一个工程,叫Swipedismisslistview,我们需要将Nine Old Androids这个库引入到工程,大家可以去https://github.com/JakeWharton/NineOldAndroids下载,可以使用Jar包,也可以使用工程库的形式引入到我们自己的工程,我们还需要自定义一个ListView,我们先看代码然后给大家讲解下具体的功能实现

      1 package com.example.swipedismisslistview;
      2 
      3 import static com.nineoldandroids.view.ViewHelper.setAlpha;
      4 import static com.nineoldandroids.view.ViewHelper.setTranslationX;
      5 import android.content.Context;
      6 import android.util.AttributeSet;
      7 import android.view.MotionEvent;
      8 import android.view.VelocityTracker;
      9 import android.view.View;
     10 import android.view.ViewConfiguration;
     11 import android.view.ViewGroup;
     12 import android.widget.AdapterView;
     13 import android.widget.ListView;
     14 
     15 import com.nineoldandroids.animation.Animator;
     16 import com.nineoldandroids.animation.AnimatorListenerAdapter;
     17 import com.nineoldandroids.animation.ValueAnimator;
     18 import com.nineoldandroids.view.ViewHelper;
     19 import com.nineoldandroids.view.ViewPropertyAnimator;
     20 /**
     21  * @blog http://blog.csdn.net/xiaanming
     22  * 
     23  * @author xiaanming
     24  *
     25  */
     26 public class SwipeDismissListView extends ListView {
     27     /**
     28      * 认为是用户滑动的最小距离
     29      */
     30     private int mSlop;
     31     /**
     32      * 滑动的最小速度
     33      */
     34     private int mMinFlingVelocity;
     35     /**
     36      * 滑动的最大速度
     37      */
     38     private int mMaxFlingVelocity;
     39     /**
     40      * 执行动画的时间
     41      */
     42     protected long mAnimationTime = 150;
     43     /**
     44      * 用来标记用户是否正在滑动中
     45      */
     46     private boolean mSwiping;
     47     /**
     48      * 滑动速度检测类
     49      */
     50     private VelocityTracker mVelocityTracker;
     51     /**
     52      * 手指按下的position
     53      */
     54     private int mDownPosition;
     55     /**
     56      * 按下的item对应的View
     57      */
     58     private View mDownView;
     59     private float mDownX;
     60     private float mDownY;
     61     /**
     62      * item的宽度
     63      */
     64     private int mViewWidth;
     65     /**
     66      * 当ListView的Item滑出界面回调的接口
     67      */
     68     private OnDismissCallback onDismissCallback;
     69 
     70     /**
     71      * 设置动画时间
     72      * 
     73      * @param mAnimationTime
     74      */
     75     public void setmAnimationTime(long mAnimationTime) {
     76         this.mAnimationTime = mAnimationTime;
     77     }
     78 
     79     /**
     80      * 设置删除回调接口
     81      * 
     82      * @param onDismissCallback
     83      */
     84     public void setOnDismissCallback(OnDismissCallback onDismissCallback) {
     85         this.onDismissCallback = onDismissCallback;
     86     }
     87 
     88     public SwipeDismissListView(Context context) {
     89         this(context, null);
     90     }
     91 
     92     public SwipeDismissListView(Context context, AttributeSet attrs) {
     93         this(context, attrs, 0);
     94     }
     95 
     96     public SwipeDismissListView(Context context, AttributeSet attrs,
     97             int defStyle) {
     98         super(context, attrs, defStyle);
     99 
    100         ViewConfiguration vc = ViewConfiguration.get(context);
    101         mSlop = vc.getScaledTouchSlop();
    102         mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 8; //获取滑动的最小速度
    103         mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();  //获取滑动的最大速度
    104     }
    105 
    106     
    107     @Override
    108     public boolean onTouchEvent(MotionEvent ev) {
    109         switch (ev.getAction()) {
    110         case MotionEvent.ACTION_DOWN:
    111             handleActionDown(ev);
    112             break;
    113         case MotionEvent.ACTION_MOVE:
    114             return handleActionMove(ev);
    115         case MotionEvent.ACTION_UP:
    116             handleActionUp(ev);
    117             break;
    118         }
    119         return super.onTouchEvent(ev);
    120     }
    121 
    122     /**
    123      * 按下事件处理
    124      * 
    125      * @param ev
    126      * @return
    127      */
    128     private void handleActionDown(MotionEvent ev) {
    129         mDownX = ev.getX();
    130         mDownY = ev.getY();
    131         
    132         mDownPosition = pointToPosition((int) mDownX, (int) mDownY);
    133 
    134         if (mDownPosition == AdapterView.INVALID_POSITION) {
    135             return;
    136         }
    137 
    138         mDownView = getChildAt(mDownPosition - getFirstVisiblePosition());
    139 
    140         if (mDownView != null) {
    141             mViewWidth = mDownView.getWidth();
    142         }
    143 
    144         //加入速度检测
    145         mVelocityTracker = VelocityTracker.obtain();
    146         mVelocityTracker.addMovement(ev);
    147     }
    148     
    149 
    150     /**
    151      * 处理手指滑动的方法
    152      * 
    153      * @param ev
    154      * @return
    155      */
    156     private boolean handleActionMove(MotionEvent ev) {
    157         if (mVelocityTracker == null || mDownView == null) {
    158             return super.onTouchEvent(ev);
    159         }
    160 
    161         // 获取X方向滑动的距离
    162         float deltaX = ev.getX() - mDownX;
    163         float deltaY = ev.getY() - mDownY;
    164 
    165         // X方向滑动的距离大于mSlop并且Y方向滑动的距离小于mSlop,表示可以滑动
    166         if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < mSlop) {
    167             mSwiping = true;
    168             
    169             //当手指滑动item,取消item的点击事件,不然我们滑动Item也伴随着item点击事件的发生
    170             MotionEvent cancelEvent = MotionEvent.obtain(ev);
    171             cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
    172                        (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
    173             onTouchEvent(cancelEvent);
    174         }
    175 
    176         if (mSwiping) {
    177             // 跟谁手指移动item
    178             ViewHelper.setTranslationX(mDownView, deltaX);
    179             // 透明度渐变
    180             ViewHelper.setAlpha(mDownView, Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX)/ mViewWidth)));
    181 
    182             // 手指滑动的时候,返回true,表示SwipeDismissListView自己处理onTouchEvent,其他的就交给父类来处理
    183             return true;
    184         }
    185 
    186         return super.onTouchEvent(ev);
    187 
    188     }
    189 
    190     /**
    191      * 手指抬起的事件处理
    192      * @param ev
    193      */
    194     private void handleActionUp(MotionEvent ev) {
    195         if (mVelocityTracker == null || mDownView == null|| !mSwiping) {
    196             return;
    197         }
    198 
    199         float deltaX = ev.getX() - mDownX;
    200         
    201         //通过滑动的距离计算出X,Y方向的速度
    202         mVelocityTracker.computeCurrentVelocity(1000);
    203         float velocityX = Math.abs(mVelocityTracker.getXVelocity());
    204         float velocityY = Math.abs(mVelocityTracker.getYVelocity());
    205         
    206         boolean dismiss = false; //item是否要滑出屏幕
    207         boolean dismissRight = false;//是否往右边删除
    208         
    209         //当拖动item的距离大于item的一半,item滑出屏幕
    210         if (Math.abs(deltaX) > mViewWidth / 2) {
    211             dismiss = true;
    212             dismissRight = deltaX > 0;
    213             
    214             //手指在屏幕滑动的速度在某个范围内,也使得item滑出屏幕
    215         } else if (mMinFlingVelocity <= velocityX
    216                 && velocityX <= mMaxFlingVelocity && velocityY < velocityX) {
    217             dismiss = true;
    218             dismissRight = mVelocityTracker.getXVelocity() > 0;
    219         }
    220 
    221         if (dismiss) {
    222             ViewPropertyAnimator.animate(mDownView)
    223                     .translationX(dismissRight ? mViewWidth : -mViewWidth)//X轴方向的移动距离
    224                     .alpha(0)
    225                     .setDuration(mAnimationTime)
    226                     .setListener(new AnimatorListenerAdapter() {
    227                         @Override
    228                         public void onAnimationEnd(Animator animation) {
    229                             //Item滑出界面之后执行删除
    230                             performDismiss(mDownView, mDownPosition);
    231                         }
    232                     });
    233         } else {
    234             //将item滑动至开始位置
    235             ViewPropertyAnimator.animate(mDownView)
    236             .translationX(0)
    237             .alpha(1)    
    238             .setDuration(mAnimationTime).setListener(null);
    239         }
    240         
    241         //移除速度检测
    242         if(mVelocityTracker != null){
    243             mVelocityTracker.recycle();
    244             mVelocityTracker = null;
    245         }
    246         
    247         mSwiping = false;
    248     }
    249     
    250 
    251     
    252     /**
    253      * 在此方法中执行item删除之后,其他的item向上或者向下滚动的动画,并且将position回调到方法onDismiss()中
    254      * @param dismissView
    255      * @param dismissPosition
    256      */
    257     private void performDismiss(final View dismissView, final int dismissPosition) {
    258         final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();//获取item的布局参数
    259         final int originalHeight = dismissView.getHeight();//item的高度
    260 
    261         ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 0).setDuration(mAnimationTime);
    262         animator.start();
    263 
    264         animator.addListener(new AnimatorListenerAdapter() {
    265             @Override
    266             public void onAnimationEnd(Animator animation) {
    267                 if (onDismissCallback != null) {
    268                     onDismissCallback.onDismiss(dismissPosition);
    269                 }
    270 
    271                 //这段代码很重要,因为我们并没有将item从ListView中移除,而是将item的高度设置为0
    272                 //所以我们在动画执行完毕之后将item设置回来
    273                 ViewHelper.setAlpha(dismissView, 1f);
    274                 ViewHelper.setTranslationX(dismissView, 0);
    275                 ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
    276                 lp.height = originalHeight;
    277                 dismissView.setLayoutParams(lp);
    278 
    279             }
    280         });
    281 
    282         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    283             @Override
    284             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    285                 //这段代码的效果是ListView删除某item之后,其他的item向上滑动的效果
    286                 lp.height = (Integer) valueAnimator.getAnimatedValue();
    287                 dismissView.setLayoutParams(lp);
    288             }
    289         });
    290 
    291     }
    292 
    293     /**
    294      * 删除的回调接口
    295      * 
    296      * @author xiaanming
    297      * 
    298      */
    299     public interface OnDismissCallback {
    300         public void onDismiss(int dismissPosition);
    301     }
    302 
    303 }

    看过Android 使用Scroller实现绚丽的ListView左右滑动删除Item效果你会发现,这个自定义的SwipeDismissListView只重写了onTouchEvent()方法,其实我们重写这一个方法就能实现我们需要的效果

    1. 我们先看手指按下屏幕的处理方法handleActionDown();该方法里面根据我们手指按下的点根据pointToPosition()方法来获 取我们点击的position,然后利用getChildAt()来获取我们按下的item的View对象,并且加入手指在屏幕滑动的速度检查,这一步相 对来说还是比较简单


    2. 接下来就是手指在屏幕上面滑动的处理方法handleActionMove(),这个方法就稍微的复杂些,我们需要根据手指在X轴的滑动距离和Y轴的滑动 距离来判断是ListView item的水平滑动还是ListView的上下滑动,当满足Math.abs(deltaX) > mSlop && Math.abs(deltaY) < mSlop这个条件时候,我们用一个布尔值mSwiping来标记Item现在处于水平滑动的状态,这时候我们需要处理Item跟随手指的滑动而滑动的逻 辑,我们使用ViewHelper来处理Item的滑动逻辑,这个类会根据机器的SDK版本来判断使用Android系统的API还是 NineOldandroids中自己实现的API使得View滑动的效果,NineOldandroids中主要使用Camera(可以实现各种复杂动 画效果的类),我们直接使用ViewHelper的setTranslationX()和setAlpha()就实现了item滑动和透明度渐变的效果, 为了使得我们在滑动item的时候,ListView不上下滚动,我们必须返回true来屏蔽ListView的上下滚动,这里需要我们要非常熟悉 Android的事件分发机制,这里我就不说明了,大家不了解的去网上找找相关的文章看看
    还有一个问题,就是当我们滑动ListView的item的时候,会伴随着item的点击事件,这不是我们想要的效果,所以当Item滑动的时候我们需要取消ListView Item的点击事件

    3. 在看手指抬起的时候的处理方法handleActionUp(),这里面需要根据手指的滑动速度或者Item移动的距离来判断Item是滑出屏幕还是滑动至起始位置,并且要判断item向左还是向右滑出屏幕等等逻辑,具体的逻辑可以看代码,相信大家都看得懂.

    我这里要说说ViewPropertyAnimator类,这个类能更好的实现一个View同时进行多个动画的功能,当然我们也可以使用ObjectAnimator利用AnimatorSet来实现一个View上的多个同时进行的动画效果,例如我们可以将

     1             ViewPropertyAnimator.animate(mDownView)
     2                     .translationX(dismissRight ? mViewWidth : -mViewWidth)//X轴方向的移动距离
     3                     .alpha(0)
     4                     .setDuration(mAnimationTime)
     5                     .setListener(new AnimatorListenerAdapter() {
     6                         @Override
     7                         public void onAnimationEnd(Animator animation) {
     8                             //Item滑出界面之后执行删除
     9                             performDismiss(mDownView, mDownPosition);
    10                         }
    11                     });

    替换成

     1 AnimatorSet set = new AnimatorSet();
     2             set.playTogether(ObjectAnimator.ofFloat(mDownView, "translationX", dismissRight ? mViewWidth : -mViewWidth), 
     3                             ObjectAnimator.ofFloat(mDownView, "alpha", 0));
     4             set.setDuration(mAnimationTime).start();
     5             set.addListener(new AnimatorListenerAdapter() {
     6                         @Override
     7                         public void onAnimationEnd(Animator animation) {
     8                             //Item滑出界面之后执行删除
     9                             performDismiss(mDownView, mDownPosition);
    10                         }
    11                     });

    在效果上面是一样的,但是ViewPropertyAnimator在性能上要比使用ObjectAnimator来实现多个同时进行的动画要高的多,举 个例子,假如要对View使用移动和透明度的动画,使用ViewPropertyAnimator的话,某个时间点上我们只需要调用一次 invalidate()方法刷新界面就行了,而使用ObjectAnimator的话,移动的动画需要调用invalidate(),透明度的动画也需 要调用invalidate()方法,在性能上使用AnimationSet比ViewPropertyAnimator要低,但是有的时候我们还是需要 使用ObjectAnimator,比如,在某个时间内,我们需要将View先变大在变小在变大等复杂情况,这时候ObjectAnimator就派上用 场了,例如

    1 ObjectAnimator.ofInt(mDownView, "scaleX", 0 ,100 ,0, 100).setDuration(100).start()

    通过上面的几步我们就实现了ListView的左右滑动删除item的效果啦,但是还有一个效果,item删除之后,ListView的其他 item向上或者向下缓缓滑动的效果,实现这个也很容易,就是动态设置item的高度,item高度逐渐变小,这样其他的item就会出现向上或者向下挤 压的效果啦!

    4. 这里我们使用的是ValueAnimator这个类,这个类并不是针对View作用的动画,而是对某个值作用的动画,他默认使用的 Interpolator(插补器)是AccelerateDecelerateInterpolator(开始和结束的时候慢,中间快) , 举个很简单的例子,我们在10秒内使用ValueAnimator将某个值从0变化到100,如果使用LinearInterpolator(线性插 补器,匀速变化)在第2秒的时候,这个值变成了20,而是用AccelerateDecelerateInterpolator,可能在第二秒的时候这个 值为15或者13,所以我们在ValueAnimator变化的时候设置值动画变化的监听器AnimatorUpdateListener就知道某个时间 这个值变成了多少,从而对View的某个属性进行设置(例如大小),所以ValueAnimator是间接的对View设置动画的

    了解了 ValueAnimator的使用原理,我们就可以现实上面的动画效果了,我们使用ValueAnimator将item的高度变成0,设置 ValueAnimator变化的监听,我们在回调函数onAnimationUpdate()中动态的设置item的高度, 然后添加AnimatorListener监听动画的状态(例如动画开始,结束,重复等)监听,在动画结束的回调函数onAnimationEnd()中 删除该item的数据,调用notifyDataSetChanged刷新ListView,看看下面这段代码

    1 ViewHelper.setAlpha(dismissView, 1f);
    2                 ViewHelper.setTranslationX(dismissView, 0);
    3                 ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
    4                 lp.height = originalHeight;
    5                 dismissView.setLayoutParams(lp);

    我们使用动画只是将item移动出了屏幕,并且将item的高度设置为了0,并没有将item的View从ListView中Remove掉,况且 ListView也不能直接Remove掉Item的,只能将数据源删除,在调用notifyDataSetChanged()刷新,所以我们需要将刚刚 滑出屏幕高度设置为0的Item恢复回来

    自定义控件的代码我们已经编写完了,接下来我们就要使用它了,先看界面的布局代码

     1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
     2     xmlns:tools="http://schemas.android.com/tools"  
     3     android:layout_width="match_parent"  
     4     android:layout_height="match_parent">  
     5   
     6     <com.example.swipedismisslistview.SwipeDismissListView
     7         android:id="@+id/swipeDismissListView"  
     8         android:layout_width="match_parent"  
     9         android:layout_height="match_parent" 
    10         android:listSelector="@android:color/transparent"
    11         android:cacheColorHint="@android:color/transparent">  
    12     </com.example.swipedismisslistview.SwipeDismissListView>
    13   
    14 </RelativeLayout>  

    很简单,一个RelativeLayout包裹我们自定义的ListView控件,接下来就是主界面的代码编写,跟平常的ListView使用一样,但是我们需要设置OnDismissCallback()监听,在

    onDismiss()中删除该位置对于的数据,刷新ListView

     1 package com.example.swipedismisslistview;
     2 
     3 import java.util.ArrayList;
     4 import java.util.List;
     5 
     6 import android.app.Activity;
     7 import android.os.Bundle;
     8 import android.view.View;
     9 import android.widget.AdapterView;
    10 import android.widget.AdapterView.OnItemClickListener;
    11 import android.widget.ArrayAdapter;
    12 import android.widget.Toast;
    13 
    14 import com.example.swipedismisslistview.SwipeDismissListView.OnDismissCallback;
    15 
    16 public class SwipeActivity extends Activity {
    17     private SwipeDismissListView swipeDismissListView;
    18     private ArrayAdapter<String> adapter;
    19     private List<String> dataSourceList = new ArrayList<String>();
    20 
    21     @Override
    22     protected void onCreate(Bundle savedInstanceState) {
    23         super.onCreate(savedInstanceState);
    24         setContentView(R.layout.activity_swipe);
    25         init();
    26     }
    27 
    28     private void init() {
    29         swipeDismissListView = (SwipeDismissListView) findViewById(R.id.swipeDismissListView);
    30         for (int i = 0; i < 20; i++) {
    31             dataSourceList.add("滑动删除" + i);
    32         }
    33 
    34         adapter = new ArrayAdapter<String>(this,
    35                 android.R.layout.simple_list_item_1,
    36                 android.R.id.text1, dataSourceList);
    37         
    38         swipeDismissListView.setAdapter(adapter);
    39         
    40         swipeDismissListView.setOnDismissCallback(new OnDismissCallback() {
    41             
    42             @Override
    43             public void onDismiss(int dismissPosition) {
    44                  adapter.remove(adapter.getItem(dismissPosition)); 
    45             }
    46         });
    47         
    48         
    49         swipeDismissListView.setOnItemClickListener(new OnItemClickListener() {
    50 
    51             @Override
    52             public void onItemClick(AdapterView<?> parent, View view,
    53                     int position, long id) {
    54                 Toast.makeText(SwipeActivity.this, adapter.getItem(position), Toast.LENGTH_SHORT).show();
    55             }
    56         });
    57 
    58     }
    59 
    60 }

    所有的代码都已经编写完毕了,接下来就是运行工程,看看具体的效果是不是我们想要的

    好 了,今天的讲解到这里结束了,有了NineOldAndroids我们可以在2.x的手机上面实现许多复杂的动画效果,文章也介绍了关于开源库 NineOldAndroids使用的一些知识,文章有点长,希望读者还是先将文章看下,然后自己看看能不能自己实现出来,有什么不明白的地方请在下面留 言,我会为大家解答的!

    项目源码,点击下载

     

     

  • 相关阅读:
    java设计模式----工厂模式
    使用.Net Core Mvc +SqlSugar +Autofac+AutoMapper+....
    使用 Date 和 SimpleDateFormat 类表示时间
    .Net Core 3.0 IdentityServer4 快速入门02
    .Net Core 3.0 IdentityServer4 快速入门
    微信小程序支付
    微信小程序集成腾讯云 IM SDK
    架构杂谈《十》
    架构杂谈《九》
    架构杂谈《八》
  • 原文地址:https://www.cnblogs.com/liangstudyhome/p/3920737.html
Copyright © 2020-2023  润新知