zoukankan      html  css  js  c++  java
  • 详解Android中实现ListView左右滑动删除条目的方法

    使用Scroller实现绚丽的ListView左右滑动删除Item效果
    这里来给大家带来使用Scroller的小例子,同时也能用来帮助初步解除的读者更加熟悉的掌握Scroller的使用,掌握好了Scroller的使用我们就能实现很多滑动的效果。例如侧滑菜单,launcher,ListView的下拉刷新等等效果,我今天实现的是ListView的item的左右滑动删除item的效果,现在很多朋友看到这个效果应该是在Android的通知栏下拉中看到这个滑动删除的效果吧,我看到这个效果是在我之前的三星手机上左右滑动打电话发短信的效果,感觉很棒,不过现在很多手机联系人滑动都不是我之前那台手机的效果啦,网上很多朋友也写了关于滑动删除ListView的item的例子,有些是滑动手指离开之后然后给item加向左或者向右的移动动画,我觉得这样子的用户体验不是很好,所以今天自己也写了一个关于ListView左右滑动删除Item的小例子,ListView的item会随着手指在屏幕上的滑动而滑动,手指离开屏幕的时候item会根据判断向左或者向右划出屏幕,就是跟通知栏的效果差不多,接下来就带大家来实现这个效果。
    先说下实现该效果的主要思路
    先根据手指触摸的点来获取点击的是ListView的哪一个item
    手指在屏幕中滑动我们利用scrollBy()来使该item跟随手指一起滑动
    手指放开的时候,我们判断手指拖动的距离来判断item到底是滑出屏幕还是回到开始位置
    主要思路就是上面这三步,接下来我们就用代码来实现吧,首先我们新建一个项目,叫SlideCutListView
    根据需求我们需要自己自定义一个ListView来实现该功能,接下来先贴出代码再讲解具体的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    package com.example.slidecutlistview;
      
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.VelocityTracker;
    import android.view.View;
    import android.view.ViewConfiguration;
    import android.view.WindowManager;
    import android.widget.AdapterView;
    import android.widget.ListView;
    import android.widget.Scroller;
     
    public class SlideCutListView extends ListView {
      /**
       * 当前滑动的ListView position
       */
      private int slidePosition;
      /**
       * 手指按下X的坐标
       */
      private int downY;
      /**
       * 手指按下Y的坐标
       */
      private int downX;
      /**
       * 屏幕宽度
       */
      private int screenWidth;
      /**
       * ListView的item
       */
      private View itemView;
      /**
       * 滑动类
       */
      private Scroller scroller;
      private static final int SNAP_VELOCITY = 600;
      /**
       * 速度追踪对象
       */
      private VelocityTracker velocityTracker;
      /**
       * 是否响应滑动,默认为不响应
       */
      private boolean isSlide = false;
      /**
       * 认为是用户滑动的最小距离
       */
      private int mTouchSlop;
      /**
       * 移除item后的回调接口
       */
      private RemoveListener mRemoveListener;
      /**
       * 用来指示item滑出屏幕的方向,向左或者向右,用一个枚举值来标记
       */
      private RemoveDirection removeDirection;
      
      // 滑动删除方向的枚举值
      public enum RemoveDirection {
        RIGHT, LEFT;
      }
      
      
      public SlideCutListView(Context context) {
        this(context, null);
      }
      
      public SlideCutListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
      }
      
      public SlideCutListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        screenWidth = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getWidth();
        scroller = new Scroller(context);
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
      }
        
      /**
       * 设置滑动删除的回调接口
       * @param removeListener
       */
      public void setRemoveListener(RemoveListener removeListener) {
        this.mRemoveListener = removeListener;
      }
      
      /**
       * 分发事件,主要做的是判断点击的是那个item, 以及通过postDelayed来设置响应左右滑动事件
       */
      @Override
      public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
          addVelocityTracker(event);
      
          // 假如scroller滚动还没有结束,我们直接返回
          if (!scroller.isFinished()) {
            return super.dispatchTouchEvent(event);
          }
          downX = (int) event.getX();
          downY = (int) event.getY();
      
          slidePosition = pointToPosition(downX, downY);
      
          // 无效的position, 不做任何处理
          if (slidePosition == AdapterView.INVALID_POSITION) {
            return super.dispatchTouchEvent(event);
          }
      
          // 获取我们点击的item view
          itemView = getChildAt(slidePosition - getFirstVisiblePosition());
          break;
        }
        case MotionEvent.ACTION_MOVE: {
          if (Math.abs(getScrollVelocity()) > SNAP_VELOCITY
              || (Math.abs(event.getX() - downX) > mTouchSlop && Math
                  .abs(event.getY() - downY) < mTouchSlop)) {
            isSlide = true;
              
          }
          break;
        }
        case MotionEvent.ACTION_UP:
          recycleVelocityTracker();
          break;
        }
      
        return super.dispatchTouchEvent(event);
      }
      
      /**
       * 往右滑动,getScrollX()返回的是左边缘的距离,就是以View左边缘为原点到开始滑动的距离,所以向右边滑动为负值
       */
      private void scrollRight() {
        removeDirection = RemoveDirection.RIGHT;
        final int delta = (screenWidth + itemView.getScrollX());
        // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item
        scroller.startScroll(itemView.getScrollX(), 0, -delta, 0,
            Math.abs(delta));
        postInvalidate(); // 刷新itemView
      }
      
      /**
       * 向左滑动,根据上面我们知道向左滑动为正值
       */
      private void scrollLeft() {
        removeDirection = RemoveDirection.LEFT;
        final int delta = (screenWidth - itemView.getScrollX());
        // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item
        scroller.startScroll(itemView.getScrollX(), 0, delta, 0,
            Math.abs(delta));
        postInvalidate(); // 刷新itemView
      }
      
      /**
       * 根据手指滚动itemView的距离来判断是滚动到开始位置还是向左或者向右滚动
       */
      private void scrollByDistanceX() {
        // 如果向左滚动的距离大于屏幕的二分之一,就让其删除
        if (itemView.getScrollX() >= screenWidth / 2) {
          scrollLeft();
        } else if (itemView.getScrollX() <= -screenWidth / 2) {
          scrollRight();
        } else {
          // 滚回到原始位置,为了偷下懒这里是直接调用scrollTo滚动
          itemView.scrollTo(0, 0);
        }
      
      }
      
      /**
       * 处理我们拖动ListView item的逻辑
       */
      @Override
      public boolean onTouchEvent(MotionEvent ev) {
        if (isSlide && slidePosition != AdapterView.INVALID_POSITION) {
          requestDisallowInterceptTouchEvent(true);
          addVelocityTracker(ev);
          final int action = ev.getAction();
          int x = (int) ev.getX();
          switch (action) {
          case MotionEvent.ACTION_DOWN:
            break;
          case MotionEvent.ACTION_MOVE:
              
            MotionEvent cancelEvent = MotionEvent.obtain(ev);
            cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
                  (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
            onTouchEvent(cancelEvent);
              
            int deltaX = downX - x;
            downX = x;
      
            // 手指拖动itemView滚动, deltaX大于0向左滚动,小于0向右滚
            itemView.scrollBy(deltaX, 0);
              
            return true; //拖动的时候ListView不滚动
          case MotionEvent.ACTION_UP:
            int velocityX = getScrollVelocity();
            if (velocityX > SNAP_VELOCITY) {
              scrollRight();
            } else if (velocityX < -SNAP_VELOCITY) {
              scrollLeft();
            } else {
              scrollByDistanceX();
            }
              
            recycleVelocityTracker();
            // 手指离开的时候就不响应左右滚动
            isSlide = false;
            break;
          }
        }
      
        //否则直接交给ListView来处理onTouchEvent事件
        return super.onTouchEvent(ev);
      }
      
      @Override
      public void computeScroll() {
        // 调用startScroll的时候scroller.computeScrollOffset()返回true,
        if (scroller.computeScrollOffset()) {
          // 让ListView item根据当前的滚动偏移量进行滚动
          itemView.scrollTo(scroller.getCurrX(), scroller.getCurrY());
            
          postInvalidate();
      
          // 滚动动画结束的时候调用回调接口
          if (scroller.isFinished()) {
            if (mRemoveListener == null) {
              throw new NullPointerException("RemoveListener is null, we should called setRemoveListener()");
            }
              
            itemView.scrollTo(0, 0);
            mRemoveListener.removeItem(removeDirection, slidePosition);
          }
        }
      }
      
      /**
       * 添加用户的速度跟踪器
       *
       * @param event
       */
      private void addVelocityTracker(MotionEvent event) {
        if (velocityTracker == null) {
          velocityTracker = VelocityTracker.obtain();
        }
      
        velocityTracker.addMovement(event);
      }
      
      /**
       * 移除用户速度跟踪器
       */
      private void recycleVelocityTracker() {
        if (velocityTracker != null) {
          velocityTracker.recycle();
          velocityTracker = null;
        }
      }
      
      /**
       * 获取X方向的滑动速度,大于0向右滑动,反之向左
       *
       * @return
       */
      private int getScrollVelocity() {
        velocityTracker.computeCurrentVelocity(1000);
        int velocity = (int) velocityTracker.getXVelocity();
        return velocity;
      }
      
      /**
       *
       * 当ListView item滑出屏幕,回调这个接口
       * 我们需要在回调方法removeItem()中移除该Item,然后刷新ListView
       *
       * @author xiaanming
       *
       */
      public interface RemoveListener {
        public void removeItem(RemoveDirection direction, int position);
      }
      
    }

    首先我们重写dispatchTouchEvent()方法,该方法是事件的分发方法,我们在里面只做了一些简单的步骤,我们按下屏幕的时候,如果某个item正在进行滚动,我们直接交给SlideCutListView的父类处理分发事件,否则根据点击的X,Y坐标利用pointToPosition(int x, int y)来获取点击的是ListView的哪一个position,从而获取到我们需要滑动的item的View,我们还在该方法加入了滑动速度的检测,并且在ACTION_MOVE的时候来判断是否响应item的左右移动,用isSlide来记录是否响应左右滑动
    然后就是重写onTouchEvent()方法,我们先判断isSlide为true,并且我们点击的是ListView上面的有效的position,否则直接交给SlideCutListView的父类也就是ListView来处理,在ACTION_MOVE中调用scrollBy()来移动item,scrollBy()是相对item的上一个位置进行移动的,所以我们每次都要用现在移动的距离减去上一个位置的距离然后赋给scrollBy()方法,这样子我们就实现了item的平滑移动,当我们将手指抬起的时候,我们先根据手指滑动的速度来确定是item是滑出屏幕还是滑动至原始位置,如果向右的速度大于我们设置的SNAP_VELOCITY,item就调用scrollRight()方法滚出屏幕,如果向左的速度小于-SNAP_VELOCITY,则调用scrollLeft()向左滚出屏幕,如果我们是缓慢的移动item,则调用scrollByDistanceX()方法来判断是滚到那个位置
    在scrollRight()和scrollLeft()方法中我们使用Scroller类的startScroll()方法先设置滚动的参数,然后调用postInvalidate()来刷新界面,界面刷新就会调用computeScroll()方法,我们在里面处理滚动逻辑就行了,值得一提的是computeScroll()里面的这段代码

    1
    itemView.scrollTo(0, 0);

    我们需要将该item滚动在(0, 0 )这个点,因为我们只是将ListView的Item滚动出屏幕而已,并没有将该item移除,而且我们不能手动调用removeView()来从ListView中移除该item,我们只能通过改变ListView的数据,然后通过notifyDataSetChanged()来刷新ListView,所以我们需要将其滚动至(0, 0),这里比较关键。
    定义好了我们左右滑动的ListView,接下来就来使用它,布局很简单,一个RelativeLayout包裹我们自定义的ListView

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="@android:color/darker_gray">
      
      <com.example.slidecutlistview.SlideCutListView
        android:id="@+id/slideCutListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" 
        android:listSelector="@android:color/transparent"
        android:divider="@drawable/reader_item_divider"
        android:cacheColorHint="@android:color/transparent">
      </com.example.slidecutlistview.SlideCutListView>
      
    </RelativeLayout>

    接下来我们来看看ListView的item的布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="UTF-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content" >
      
      <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/friendactivity_comment_detail_list2" >
      
        <TextView
          android:id="@+id/list_item"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:layout_margin="15dip" />
      </LinearLayout>
      
    </LinearLayout>

    还记得我在上一篇文章中提到过调用scrollTo()方法是对里面的子View进行滚动的,而不是对整个布局进行滚动的,所以我们用LinearLayout来套住我们的item的布局,这点需要注意一下,不然滚动的只是TextView。
    主页面MainActivity里面的代码比较简单,里面使用的也是ArrayAdapter,相信大家都能看懂

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package com.example.slidecutlistview;
      
    import java.util.ArrayList;
    import java.util.List;
      
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.AdapterView;
    import android.widget.AdapterView.OnItemClickListener;
    import android.widget.ArrayAdapter;
    import android.widget.Toast;
      
    import com.example.slidecutlistview.SlideCutListView.RemoveDirection;
    import com.example.slidecutlistview.SlideCutListView.RemoveListener;
      
    public class MainActivity extends Activity implements RemoveListener{
      private SlideCutListView slideCutListView ;
      private ArrayAdapter<String> adapter;
      private List<String> dataSourceList = new ArrayList<String>();
      
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
      }
      
      private void init() {
        slideCutListView = (SlideCutListView) findViewById(R.id.slideCutListView);
        slideCutListView.setRemoveListener(this);
          
        for(int i=0; i<20; i++){
          dataSourceList.add("滑动删除" + i); 
        }
          
        adapter = new ArrayAdapter<String>(this, R.layout.listview_item, R.id.list_item, dataSourceList);
        slideCutListView.setAdapter(adapter);
          
        slideCutListView.setOnItemClickListener(new OnItemClickListener() {
      
          @Override
          public void onItemClick(AdapterView<?> parent, View view,
              int position, long id) {
            Toast.makeText(MainActivity.this, dataSourceList.get(position), Toast.LENGTH_SHORT).show();
          }
        });
      }
      
        
      //滑动删除之后的回调方法
      @Override
      public void removeItem(RemoveDirection direction, int position) {
        adapter.remove(adapter.getItem(position));
        switch (direction) {
        case RIGHT:
          Toast.makeText(this, "向右删除 "+ position, Toast.LENGTH_SHORT).show();
          break;
        case LEFT:
          Toast.makeText(this, "向左删除 "+ position, Toast.LENGTH_SHORT).show();
          break;
      
        default:
          break;
        }
          
      }  
      
      
    }

    这里面需要对SlideCutListView设置RemoveListener,然后我们在回调方法removeItem(RemoveDirection direction, int position)中删除该position的数据,在调用notifyDataSetChanged()刷新ListView,我这里用的是ArrayAdatper,直接调用remove()就可以了。
    所有的代码就编写完了,我们来运行下程序看看效果吧

     

    使用NineOldAndroids实现绚丽的ListView左右滑动删除Item效果
    再给大家来一个ListView左右滑动删除Item效果的例子,上面使用的是滑动类Scroller来实现的,但是看了下通知栏的左右滑动删除效果,确实很棒,当我们滑动Item超过一半的时候,item的透明度就变成了0,我们就知道抬起手指的时候item就被删除了,当item的透明度不为0的时候,我们抬起手指Item会回到起始位置,这样我们就知道拖动到什么位置item会删除,什么位置Item不删除,用户体验更好了,还有一个效果,就是我们滑动删除了item的时候,ListView的其他item会出现向上或者向下滚动的效果,感觉效果很棒,所以在GitHub上面搜索了下,发现很多开源库都有这个效果,比如ListViewAnimations, android-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库中,这是一个兼容库,接下来我们就来看看这个效果的具体实现吧
    实现该效果的主要思路
    先根据手指触摸的点来获取点击的是ListView的哪一个Item
    当手指在屏幕上面滑动的时候,我们要使得Item跟随手指的滑动而滑动
    当我们抬起手指的时候,我们根据滑动的距离或者手指在屏幕上面的速度来判断Item是滑出屏幕还是滑动至其实位置
    Item滑出屏幕时,使ListView的其他item产生向上挤压或者向下挤压的效果
    大致的思路这是这四步,其中的一些细节接下来我会一一为大家解答的,接下来我们就用代码来实现这种效果吧
    首先我们新建一个工程,叫Swipedismisslistview,我们需要将Nine Old Androids这个库引入到工程,大家可以去https://github.com/JakeWharton/NineOldAndroids下载,可以使用Jar包,也可以使用工程库的形式引入到我们自己的工程,我们还需要自定义一个ListView,我们先看代码然后给大家讲解下具体的功能实现

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

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

    替换成

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

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

    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
    2
    3
    4
    5
    ViewHelper.setAlpha(dismissView, 1f);
            ViewHelper.setTranslationX(dismissView, 0);
            ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
            lp.height = originalHeight;
            dismissView.setLayoutParams(lp);

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

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

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

    很简单,一个RelativeLayout包裹我们自定义的ListView控件,接下来就是主界面的代码编写,跟平常的ListView使用一样,但是我们需要设置OnDismissCallback()监听,在
    onDismiss()中删除该位置对于的数据,刷新ListView

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

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

    201645144001733.gif (411×698)

    专注,认真,坚持,分分秒秒,每天进步一点点
  • 相关阅读:
    vue cli3 打包到tomcat上报错问题
    前端html转pdf
    原生js上传图片遇到的坑(axios封装)
    vue slot的使用(transform动画)
    vue购物车动画效果
    关于el-select 单选与多选切换的时候报错的解决办法
    vue html属性绑定
    关于element ui滚动条使用
    css3flex布局实现商品列表 水平垂直居中 上下布局
    vue 项目 路由 router.push、 router.replace 和 router.go
  • 原文地址:https://www.cnblogs.com/jingzhi/p/5602874.html
Copyright © 2011-2022 走看看