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

5、andriod开发 专栏收录该内容
157 篇文章 25 订阅

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

 

 

相关文章:

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

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

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

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

 

上篇我们已经完成了利用scrollTo实现listview的滑动删除,但我们最后也提到了一个问题:当手指抬起时,scrollTo直接将ITEM滑动到了指定位置,中间没有缓冲过程,用户体验不好。我们想实现一个能缓慢滑动的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都应该收缩回去。就如下面的这个效果:

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的布局参数设为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 谢谢

 

如果你喜欢我的文章,你可能更喜欢我的公众号

启舰杂谈

  • 12
    点赞
  • 23
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值