45fan.com - 路饭网

搜索: 您的位置主页 > 手机频道 > 阅读资讯:ListView滑动删除实现之四——Scroller类与listview缓慢滑动

ListView滑动删除实现之四——Scroller类与listview缓慢滑动

2019-03-30 18:06:29 来源:www.45fan.com 【

前言:这个月真是过得太快了,本来说要看四章的内容,这下才看了两章,擦……严重没完成预算碍…撞豆腐死了算了

 

相关文章:

1、《 ListView滑动删除实现之一——merge标签与LayoutInflater.inflate()》

2、《ListView滑动删除实现之二——scrollTo、scrollBy详解》

3、《 ListView滑动删除实现之三——创建可滑动删除的ListView》

4、《ListView滑动删除实现之四——Scroller类与listview缓慢滑动》

 

上篇我们已经完成了利用scrollTo实现listview的滑动删除,但我们最后也提到了一个问题:当手指抬起时,scrollTo直接将ITEM滑动到了指定位置,中间没有缓冲过程,用户体验不好。我们想实现一个能缓慢滑动的listview:如下图所示:

ListView滑动删除实现之四——Scroller类与listview缓慢滑动

从示图中明显可以看到,这里并不像上一章一样,一下子还原到初始化状态或者一下子到完全展开的状态了。而是有一个过程,它会慢慢点,一点点地还原或展开。这就是Scroller类所起的作用。下面我们先看看Scroller类的使用方法,然后再在前一篇的基础上加上Scroller。

一、Scroller类

首先,上篇我们也提到了,scrollTo()是没有办法添加运动的时间长度的,所以为了弥补这个问题,google就新增了一个类Scroller!一定要注意的是,Scroller类,并不能像scrollTo一样移动View的视角,而只能用来计算当前滑动的位置。看不懂没关系,继续往下看。

1、相关函数

(1)构造函数:
public Scroller (Context context)
public Scroller (Context context, Interpolator interpolator)
如果我们用第一个构造函数,那么Scroller就会给我们传入一个默认的插值器;一般我们会选择第二个构造函数,传入一个我们想要的插值器。插值器的概念是从Animation来的,以前我曾把所有插值器的动画特效都列举了一遍,大家有兴趣可以参考:《Animation动画详解(二)——Interpolator插值器》
Scroller构造完了以后,就可以调用startScroll()了,对于这个函数的作用先不表,但绝对不是像scrollTo一样移动视角的。
(2)startScroll
public void startScroll(int startX, int startY, int dx, int dy, int duration)
public void startScroll(int startX, int startY, int dx, int dy)
我们先看第一个构造函数:
  • startX:开始移动的X坐标
  • startY开始移动的Y坐标
  • dx:沿X轴移动距离,可正可负,为正时,子控件向左移动;为负时,子控件向右移动
  • dy:沿Y轴移动距离,同样,为正时,子控件向上移动;为负时,子控件向下移动
  • duration:整个移动过程,所耗费时长
第二个构造函数中,没有duration这个参数,系统会使用默认的时长:250毫秒

2、使用方法:

步骤一:初始化
private Scroller mScroller;
mScroller = new Scroller(context,new LinearInterpolator(context,null));
步骤二:startScroll
在需要滚动的地方调用startScroll
 mScroller.startScroll(0,0,200,0,500);
 invalidate();
步骤三: public void computeScroll()中处理
@Override
public void computeScroll() {

  if (mScroller.computeScrollOffset()){
    itemRoot.scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
  }
  invalidate();
}
在这里注意一个地方,我们在computeScroll()中最终调用了itemRoot的scrollTo(),这里又涉及两个函数:
mScroller.getCurrX()
mScroller.getCurrY()
这两个是什么意思?看表面意思是获取当前scroller所在的X轴坐标和Y轴坐标。难道它的X轴坐标和Y轴坐标还都不一样吗?
原因是这样的:
我们前面说过,Scoller类是scrollTo的补充,他没有scrollTo的功能,它的功能就是根据传进去的参数计算位置的。我们再看一下的我们例子中的构造函数:
mScroller.startScroll(0,0,200,0,500);
这个代码就是说用500毫秒的时间从(0,0)的位置,沿X轴反方向移动200,Y轴不动;
但这个函数并不会移动,而是模拟计算,调用了这个函数之后,它就会在scroller内部用一个线程来计算从(0,0)的位置,沿X轴反方向移动200,Y轴不动;每一毫秒的位置,用户可以通过scroller.getCurrX()、scroller.getCurrY()来获取,当前应该在的位置。注意,我用的“应该”。因为scroller只是根据插值器,指定的时间,距离;算出当前所在的X轴坐标,Y轴坐标。但对图像并没有做任何操作!!!!!!要想移动图像,就必须使用scrollTo()!!!所以我们要每计算出一个新的位置就让View重绘一次。这就是为什么步骤二和步骤三都会调用invalidate()的原因。
//步骤二:
 mScroller.startScroll(0,0,200,0,500);
 invalidate();
```
``` java
@Override
public void computeScroll() {
//步骤三:
  if (mScroller.computeScrollOffset()){
    itemRoot.scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    invalidate();
  }
}
这里还要再说一个点:我们如何判断什么时候停止重绘呢。scroller给我们提供了一个函数:
Scroller.computeScrollOffset()
当scroller还在移动时,就返回TRUE,如果scroller已经移动结束就返回FALSE;所以,我们可以在直接itemRoot.scrollTo(mScroller.getCurrX(),mScroller.getCurrY());前利用mScroller.computeScrollOffset()来判断当前scroller是不是已经结束了。
这里要插一句,computeScroll()函数,不是Scroller的函数,而是VIEW的函数,当调用invaidate或者postInvalidate重绘时就会调用computeScroll() 来重绘与scroller有关的View部分,而在View中的实现方式如下:
  /**
   * Called by a parent to request that a child update its values for mScrollX
   * and mScrollY if necessary. This will typically be done if the child is
   * animating a scroll using a {@link android.widget.Scroller Scroller}
   * object.
   */
  public void computeScroll() {
  }
明显这里是一个空函数,所以,这就交由我们自己来实现,Scroller中的移动部分了,这也就是为什么我们会在computeScroll() 中调用 scrollTo来实现视图的移动了。

最后我们总结一下:Scroller在调用startScroll()之后,会自己根据移动距离和时间来计算每毫秒的移动目的坐标,用户可以通过scroller.getCurrX()和scroller.getCurrY()来获龋当VIEW在重绘时,会调用View的computeScroll()函数来处理与scroller有关的重绘操作。而由于View类并没有对computeScroll()做任何的实现(只是一个空函数),所以有关scroller的移动操作,就只能靠我们自己完成了。(重写computeScroll函数,调用)

二、实现缓慢滑动的ListView

这里我们就利用scroller类来实现文章开头时的效果,这篇文章依附上一篇的源码:《 ListView滑动删除实现之三——创建可滑动删除的ListView》
由于我们要实现的缓慢滑动效果是在用户手指抬起后,缓慢移动到目标位置的,所以我们下面所有的代码都是在MyListView中来做的。

1、声明Scroller变量及初始化

代码如下:
public class MyListView extends ListView {
  
  private Context mContext;
  private Scroller mScroller;

  public MyListView(Context context, AttributeSet attrs) {
    super(context, attrs);
    mContext = context;
    mScroller = new Scroller(context, new LinearInterpolator(context, null));
  }
  ……
}
我们这里初始化mScroller时,使用线性插值器;大家也可以用其它插值器,每一个插值器的运动效果都是不一样的。

2、用户抬起时,调用startScroll()

在初始化完scroller以后,下面就是在用户抬起手指时,调用startScroll()来开始计算每毫秒所在的目的坐标。
public boolean onTouchEvent(MotionEvent event) {
  int maxLength = dipToPx(mContext, MAX_WIDTH);
  int x = (int) event.getX();
  int y = (int) event.getY();
  switch (event.getAction()) {
    …………
    case MotionEvent.ACTION_UP: {
      int scrollX = itemRoot.getScrollX();
      int newScrollX = scrollX + mlastX - x;
      if (scrollX > maxLength / 2) {
        newScrollX = maxLength;
      } else {
        newScrollX = 0;
      }
      mScroller.startScroll(scrollX,0,newScrollX - scrollX,0);
      invalidate();
    }
    break;
  }

  mlastX = x;
  return super.onTouchEvent(event);
}
我们把核心代码摘出来:
  int scrollX = itemRoot.getScrollX();
  int newScrollX = scrollX + mlastX - x;
  if (scrollX > maxLength / 2) {
    newScrollX = maxLength;
  } else {
    newScrollX = 0;
  }
  mScroller.startScroll(scrollX,0,newScrollX - scrollX,0);
  invalidate();
首先跟上篇一样,根据当前手指的位置(如果超过黄色块1/2就将newScrollX设置到最大值;如果小于黄色块1/2,那就还原到初始化状态),然后是调用
mScroller.startScroll(scrollX,0,newScrollX - scrollX,0);
首先是移动距离,大家一定要注意的是移动距离不一定是正数,它只是表示在当前方向上的移动数,X轴为正:视角向右移动,即其中的子控件向左移动。为负:视角向左移动,即其中的子控件所右移动;Y轴同理。所以这里的第三个参数dx:就等于目的坐标减去原坐标。这就是第三个参数为什么是newScrollX - scrollX的原因。
最后调用invalidate()来重绘,由于我们这里有Scroller,所以在重绘时就会走到VIEW的computeScroll()函数中,上面我们也说了VIEW中的computeScroll()只是一个空函数,VIEW并没有对它进行任何实现。所以我们下面这样来实现它:

3、实现computeScroll()函数

先上代码:
public void computeScroll() {

  if (mScroller.computeScrollOffset()){
    itemRoot.scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    invalidate();
  }
}
可以看到,首先是利用mScroller.computeScrollOffset()来判断当前Scroller的状态——是否已经计算移动位置结束,如果结束返回FALSE,如果还在计算就返回TRUE。
然后,如果还在计算,就说明还在移动过程中,那么就调用Scroller.getCurrX()和mScroller.getCurrY()来提取当前计算出来的应该移动的X轴坐标和Y轴坐标。然后调用itemRoot.scrollTo();来移动其中的子控件。这一次结束了,下一毫秒该怎么办呢?所以我们还要调用invalidate();函数,让它再次重绘,重新走进computeScroll() 函数,找到最新的Scroller位置并移动。最终当移动结束,mScroller.computeScrollOffset()就会返回FALSE,最终结束重绘。

三、优化

1、初步优化

用过这个控件的同学应该都知道,一般情况下的实现方式是,如果有一个ITEM被划出来,其它的ITEM都应该收缩回去。就如下面的这个效果:
ListView滑动删除实现之四——Scroller类与listview缓慢滑动

1、首先,当另一个ITEM滑出来的时候,上一个ITEM缩回去。
2、但最后大家会发现问题:当我连续点击一个ITEM之后,上一个ITEM就会停止滑动。这个效果不是我们想要的,我们先完成第一个效果,后面再解决这个问题。

由于我们要让上一个ITEM滑动,所以我们要在MyListView中另外定义一个变量来保存上次滑动的VIEW

private LinearLayout mPreScrollView;
然后额外添加一个SCROLLER,用来计算mPreScrollView的滚动坐标:
private Scroller mPreScroller;

public MyListView(Context context, AttributeSet attrs) {
  super(context, attrs);
  mContext = context;
  mScroller = new Scroller(context, new LinearInterpolator(context, null));
  mPreScroller = new Scroller(context, new LinearInterpolator(context, null));
}
然后是对mPreScrollView赋值和滚动了。我们在下按一个ITEM的时候,就要把前一个ITEM还原。当我们手指上抬的时候,那这个ITEM就已经滚动结束了,那么它就是下一次的mPreScrollView。
public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {

      if (mPreScrollView != null){
        int preScrollX = mPreScrollView.getScrollX();
        mPreScroller.startScroll(preScrollX,0,0-preScrollX,0);
      }
    }
    break;
    case MotionEvent.ACTION_UP: {
      …………
      mPreScrollView = itemRoot;
      …………
      invalidate();
    }
    break;
  }
  mlastX = x;
  return super.onTouchEvent(event);
}
最后是在computeScroll()中移动mPreScrollView
public void computeScroll() {
  …………
  if (mPreScroller.computeScrollOffset()){
    mPreScrollView.scrollTo(mPreScroller.getCurrX(),mPreScroller.getCurrY());
  }
  invalidate();

}
到这里就结束了,mPreScrollView在下一次滑动ITEM时就会自己收缩了,但这里有个问题,也就是效果图中的第二点问题。当我们对一个ITEM点击两下的时候,它的上一个ITEM就会停止滑动。导致不能完全收缩回去。这是为什么呢?主要是因为我们在MotionEvent.ACTION_UP中对mPreScrollView = itemRoot;进行的赋值。如果我们抬起的太快,而上一个ITEM还没有滑动结束,那么mPreScrollView还没结束就已经被赋值给最新的ITEM了,这时候的computeScroll(),调用的mPreScrollView.scrollTo(mPreScroller.getCurrX(),mPreScroller.getCurrY());就已经是最新的ITEM了,所以上一个ITEM就会停止滑动。
源码在文章底部给出

2、最终优化

关于上面的问题,我们唯一的解决办法就是让每个ITEM负责自己的滑动,只需要上层给它一个通知,它自己负责自己的滑动,这样就不会冲突了。
所以我们要对ITEM的根布局lin_root进行重写。新定义一个LinearLayout派生类MyLInearLayout。然后在其中做操作。

(1)改造:派生MyLInearLayout类

我们单独把ITEM的根布局拿出来,在其中做所有有关的对ITEM的移动操作,代码如下:
public class MyLinearLayout extends LinearLayout {
  private int mlastX = 0;
  private final int MAX_WIDTH = 200;
  private Context mContext;
  private Scroller mScroller;
  public MyLinearLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    mContext = context;
    mScroller = new Scroller(context, new LinearInterpolator(context, null));
  }
  public void disPatchTouchEvent(MotionEvent event){
    int maxLength = dipToPx(mContext, MAX_WIDTH);

    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN: {
      }
      break;
      case MotionEvent.ACTION_MOVE: {
        int scrollX = this.getScrollX();
        int newScrollX = scrollX + mlastX - x;
        if (newScrollX < 0) {
          newScrollX = 0;
        } else if (newScrollX > maxLength) {
          newScrollX = maxLength;
        }
        this.scrollTo(newScrollX, 0);
      }
      break;
      case MotionEvent.ACTION_UP: {
        int scrollX = this.getScrollX();
        int newScrollX = scrollX + mlastX - x;
        if (scrollX > maxLength / 2) {
          newScrollX = maxLength;
        } else {
          newScrollX = 0;
        }
        mScroller.startScroll(scrollX, 0, newScrollX - scrollX, 0);
        invalidate();
      }
      break;
    }
    mlastX = x;
  }

  @Override
  public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    }
    invalidate();

  }

  private int dipToPx(Context context, int dip) {
    return (int) (dip * context.getResources().getDisplayMetrics().density + 0.5f);
  }
}
这里就是集成了所有对ITEM跟随手指移动的操作。

(2)、变更ITEM布局——使用MyLinearLayout

然后就是更改原来ITEM的布局,把根结点的LinearLayout改成MyLinearLayout:
<com.harvic.com.blog3_4_mylinearlayout.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/lin_root"
  android:orientation="horizontal"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:minHeight="120dp">

  <TextView
    android:id="@+id/title"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#0000ff"
    android:gravity="center"
    android:textSize="25dp" />

  <TextView
    android:id="@+id/del"
    android:layout_width="200dp"
    android:layout_height="fill_parent"
    android:background="#ffff00"
    android:text="删除"
    android:textSize="25dp"
    android:textColor="#ffffff"
    android:gravity="center" />

</com.harvic.com.blog3_4_mylinearlayout.MyLinearLayout>

(3)、最后是在MyListView中分发MotionEvent事件:

public class MyListView extends ListView {

  private MyLinearLayout mCurView;
  public MyListView(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN: {
        //我们想知道当前点击了哪一行
        int position = pointToPosition(x, y);
        if (position != INVALID_POSITION) {
          DataHolder data = (DataHolder) getItemAtPosition(position);
          mCurView = data.rootView;
        }
      }
      break;
      default:
        break;
    }
    if (mCurView != null){
      mCurView.disPatchTouchEvent(event);
    }
    return super.onTouchEvent(event);
  }
}
首先根据位置找到所有ITEM的VIEW,然后把事件传给对应的MyLinearLayout自己处理。
到这里改造就完成了,然后就是实现自己滑动缩回去的步骤了。

 

下面的内容可能比较绕,大家可能加上我的讲解,大家可以还要自己理解一下了
首先,我们有几个点先列举一下:
1、我们需要在一个ITEM滑出来的时候,通知到上一个ITEM,让它收缩
2、每一个ITEM都应该具有监听功能,当上一个ITEM滑出来时,它就能对应的收缩回去

(4)、自动收缩之——监听

首先,我们要明白的一点是,我们要监听!!!监听当前ITEM是否全部拉了出来,所以我们在MyLinearLayout中需要加一个回调函数来做监听:

public class MyLinearLayout extends LinearLayout {
  private OnScrollListener mScrollListener;

  public static interface OnScrollListener {
    public void OnScroll(MyLinearLayout view);
  }

  public void disPatchTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
      …………
      case MotionEvent.ACTION_UP: {
        int scrollX = this.getScrollX();
        int newScrollX = scrollX + mlastX - x;
        if (scrollX > maxLength / 2) {
          newScrollX = maxLength;
          //当完全展开时,通知出去
          mScrollListener.OnScroll(this);
        } else {
          newScrollX = 0;
        }
        mScroller.startScroll(scrollX, 0, newScrollX - scrollX, 0);
        invalidate();
      }
      break;
    }
    mlastX = x;
  }

  …………
}
上面的代码,总共做了两件事:
1、创建回调函数
private OnScrollListener mScrollListener;
public static interface OnScrollListener{
  public void OnScroll(MyLinearLayout view);
}
由于我们后面要保存当前的VIEW来做自动收缩,所以我们要把当前伸展出来的ITEM的VIEW传出去,供对方保存。当下一个ITEM出来的时候,让这个VIEW缩回去
2、使用回调函数

当用户手指抬起来,而且ITEM完全展开的时候,调用onScroll()通知出去

case MotionEvent.ACTION_UP: {
  int scrollX = this.getScrollX();
  int newScrollX = scrollX + mlastX - x;
  if (scrollX > maxLength / 2) {
  //当完全展开时,通知出去
    newScrollX = maxLength;
    mScrollListener.OnScroll(this);
  } else {
    newScrollX = 0;
  }
  mScroller.startScroll(scrollX, 0, newScrollX - scrollX, 0);
  invalidate();

}
break;
然后,还有两个额外的函数:
//设置监听器
public void setOnScrollListener(OnScrollListener scrollListener) {
  mScrollListener = scrollListener;
}
//缓慢将ITEM滚动到指定位置
public void smoothScrollTo(int destX, int destY) {
  int scrollX = getScrollX();
  int delta = destX - scrollX;
  mScroller.startScroll(scrollX, 0, delta, 0);
  invalidate();
}

(5)、自动收缩之——为每一个ITEM设置监听

由于我们每一个ITEM都需要监听,所以把监听函数设在Adapter中最合适。所以在MergeListAdapter中为每一个Item的根结点MyLinearLayout添加监听函数:
public View getView(final int position, View convertView, ViewGroup parent) {
  …………
  DataHolder item = mDataList.get(position);
  holder.title.setText(item.title);

  item.rootView = (MyLinearLayout) convertView.findViewById(R.id.lin_root);
  item.rootView.scrollTo(0, 0);
  item.rootView.setOnScrollListener(mScrollListener);
  …………
  return convertView;
}
而Adapter中是没有办法做监听的,所以它的监听函数也只有从MainActivity传过来:
public class MergeListAdapter extends BaseAdapter {
  private View.OnClickListener mDelClickListener;
  private MyLinearLayout.OnScrollListener mScrollListener;

  public MergeListAdapter(Context context, List<DataHolder> dataList, View.OnClickListener delClickListener, MyLinearLayout.OnScrollListener listener) {
    …………
    mDelClickListener = delClickListener;
    mScrollListener = listener;
  }
  …………
}

(6)自动收缩之——MainActivity中处理监听

代码如下:
public class MainActivity extends Activity implements View.OnClickListener,MyLinearLayout.OnScrollListener{
  private MyLinearLayout mLastScrollView;
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    …………
    adapter = new MergeListAdapter(this,items,this,this);
    listView.setAdapter(adapter);
  }
  @Override
  public void OnScroll(MyLinearLayout view) {
    if (mLastScrollView != null){
      mLastScrollView.smoothScrollTo(0,0);
    }
    mLastScrollView = view;
  }
}
首先初始化adapter,当一个ITEM完全展开时,我们的OnScroll()函数就会得到调用。用一个变量:MyLinearLayout mLastScrollView来保存上一次完全展开的ITEM;当一个ITEM完全展开时,就先调用mLastScrollView.smoothScrollTo(0,0);将上一个ITEM收缩回去,然后再将这个VIEW赋值给mLastScrollView;
最终的效果图如下:
ListView滑动删除实现之四——Scroller类与listview缓慢滑动

源码在文章底部给出

四、存在问题:不能将listview的布局参数设为wrap_content

自定义listview的参数必须是layout_width:match_parent/fill_parent,layout_height:match_parent/fill_parent

即下面的代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <com.harvic.com.blog3_4_mylinearlayout.MyListView
    android:id="@+id/listview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

</RelativeLayout>

如果给Mylistview使用wrap_content,将导致scrollTo无效,具体什么原因我也不知道,所以如果listView底部还有什么界面的话,建议大家使用listview::addFooterView()来添加底部布局的方式来做。一定要避免使用wrap_content,不然就真的会滑动无效,大家可以尝试一下,反正我是找不到原因了……

 

好了,到这里有关滑动删除的所有东东都讲完了,这篇文章容量有点大,大家可能要根据源码仔细理解一下了。逻辑确实有点太绕,大家多看看源码应该问题不大。

 

最后,最终优化部分的代码是我们这个系列建议使用的代码结构,即ITEM的根布局使用自定义的MyLinearLayout,由它自己来负责自己的滚动。对应的源码名称为:《ScrollViewUltimate》

 

源码内容:

1、《ScrollView_Scroller》:第二部分:《实现缓慢滑动的ListView》对应源码

2、《ScrollViewUltimate》:第三部分:《优化》的最终代码,也是本系统的最终可用代码

 

如果本文有帮到你,记得加关注哦

源码下载地址:http://download.csdn.net/detail/harvic880925/8642751

请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/45317951 谢谢

 

 
 

本文地址:http://www.45fan.com/a/luyou/100039.html
Tags: 实现 滑动 listView
编辑:路饭网
关于我们 | 联系我们 | 友情链接 | 网站地图 | Sitemap | App | 返回顶部