相关文章:
1、《 ListView滑动删除实现之一——merge标签与LayoutInflater.inflate()》
2、《ListView滑动删除实现之二——scrollTo、scrollBy详解》
3、《 ListView滑动删除实现之三——创建可滑动删除的ListView》
4、《ListView滑动删除实现之四——Scroller类与listview缓慢滑动》
上篇我们已经完成了利用scrollTo实现listview的滑动删除,但我们最后也提到了一个问题:当手指抬起时,scrollTo直接将ITEM滑动到了指定位置,中间没有缓冲过程,用户体验不好。我们想实现一个能缓慢滑动的listview:如下图所示:
![](https://img-blog.csdn.net/20150428192227857)
从示图中明显可以看到,这里并不像上一章一样,一下子还原到初始化状态或者一下子到完全展开的状态了。而是有一个过程,它会慢慢点,一点点地还原或展开。这就是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中的实现方式如下:
-
-
-
-
-
-
- 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;
- }
-
- 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 谢谢