自定义ScrollView实现反弹效果(以及解决和ListView之间的冲突)

来源:互联网 发布:有限元法的软件 编辑:程序博客网 时间:2024/04/30 08:54

原文地址 http://blog.csdn.net/jj120522/article/details/8204434

首先还是一贯作风,先看一些案例:(微信界面)

          

玩过微信的朋友想必很熟悉,其实就是界面可以拖拽,会有反弹效果,看起来很炫酷.(总之比拖拽没反应的死板要好.)

下面我来一一讲解如何实现这样效果:

原理:自定义ScrollView对其Touch监听,对布局时时更改.

一:不包含孩子,就是只针对普通的布局:

MyScrollView.java

[java] view plaincopy
  1. package com.rebound.myscroll;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Rect;  
  5. import android.util.AttributeSet;  
  6. import android.view.MotionEvent;  
  7. import android.view.View;  
  8. import android.view.animation.TranslateAnimation;  
  9. import android.widget.ScrollView;  
  10.   
  11. /** 
  12.  * 自定义ScrollView 
  13.  */  
  14. public class MyScrollView extends ScrollView {  
  15.     private View inner;// 孩子  
  16.   
  17.     private float y;// 坐标  
  18.   
  19.     private Rect normal = new Rect();// 矩形空白  
  20.   
  21.     public MyScrollView(Context context, AttributeSet attrs) {  
  22.         super(context, attrs);  
  23.     }  
  24.   
  25.     /*** 
  26.      * 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate 
  27.      * 方法,也应该调用父类的方法,使该方法得以执行. 
  28.      */  
  29.     @Override  
  30.     protected void onFinishInflate() {  
  31.         if (getChildCount() > 0) {  
  32.             inner = getChildAt(0);// 获取其孩子  
  33.         }  
  34.     }  
  35.   
  36.     @Override  
  37.     public boolean onTouchEvent(MotionEvent ev) {  
  38.         if (inner != null) {  
  39.             commOnTouchEvent(ev);  
  40.         }  
  41.         return super.onTouchEvent(ev);  
  42.     }  
  43.   
  44.     /*** 
  45.      * 触摸事件 
  46.      *  
  47.      * @param ev 
  48.      */  
  49.     public void commOnTouchEvent(MotionEvent ev) {  
  50.         int action = ev.getAction();  
  51.         switch (action) {  
  52.         case MotionEvent.ACTION_DOWN:  
  53.             y = ev.getY();// 获取点击y坐标  
  54.             break;  
  55.         case MotionEvent.ACTION_UP:  
  56.             if (isNeedAnimation()) {  
  57.                 animation();  
  58.             }  
  59.             break;  
  60.         case MotionEvent.ACTION_MOVE:  
  61.             final float preY = y;  
  62.             float nowY = ev.getY();  
  63.             int deltaY = (int) (preY - nowY);// 获取滑动距离  
  64.   
  65.             y = nowY;  
  66.             // 当滚动到最上或者最下时就不会再滚动,这时移动布局  
  67.             if (isNeedMove()) {  
  68.                 if (normal.isEmpty()) {  
  69.                     // 填充矩形,目的:就是告诉this:我现在已经有了,你松开的时候记得要执行回归动画.  
  70.                     normal.set(inner.getLeft(), inner.getTop(),  
  71.                             inner.getRight(), inner.getBottom());  
  72.                 }  
  73.                 // 移动布局  
  74.                 inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,  
  75.                         inner.getRight(), inner.getBottom() - deltaY / 2);  
  76.             }  
  77.             break;  
  78.   
  79.         default:  
  80.             break;  
  81.         }  
  82.     }  
  83.   
  84.     /*** 
  85.      * 开启动画移动 
  86.      */  
  87.     public void animation() {  
  88.         // 开启移动动画  
  89.         TranslateAnimation ta = new TranslateAnimation(00, inner.getTop(),  
  90.                 normal.top);  
  91.         ta.setDuration(300);  
  92.         inner.startAnimation(ta);  
  93.         // 设置回到正常的布局位置  
  94.         inner.layout(normal.left, normal.top, normal.right, normal.bottom);  
  95.         normal.setEmpty();// 清空矩形  
  96.   
  97.     }  
  98.   
  99.     /*** 
  100.      * 是否需要开启动画 
  101.      *  
  102.      * 如果矩形不为空,返回true,否则返回false. 
  103.      *  
  104.      *  
  105.      * @return 
  106.      */  
  107.     public boolean isNeedAnimation() {  
  108.         return !normal.isEmpty();  
  109.     }  
  110.   
  111.     /*** 
  112.      * 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的高度 
  113.      * getHeight():获取的是当前控件在屏幕中显示的高度 
  114.      *  
  115.      * @return 
  116.      */  
  117.     public boolean isNeedMove() {  
  118.         int offset = inner.getMeasuredHeight() - getHeight();  
  119.         int scrollY = getScrollY();  
  120.         // 0是顶部,后面那个是底部  
  121.         if (scrollY == 0 || scrollY == offset) {  
  122.             return true;  
  123.         }  
  124.         return false;  
  125.     }  
  126.   
  127. }  


注释已经很明确,我也不过多解释,如有不明请留言。

效果:


下拉效果,松开会自动回缩.


效果图是我随便弄的,这个比较简单,下面我们看下如果自定义的ScrollView里面包含ViewGroup类的控件如何办?

二:自定义ScrollView里面包含ListView.

想必这种效果比较常见,用处也比较广.但是单存的用上面自定义的ScrollView是行不通的,滑动时候相当不灵敏,发生错乱.为何:因为Touch受到影响,因为要继续向下传递嘛.

我们要对上面自定义稍做修改:

MyScrollView.java

[java] view plaincopy
  1. package com.jj.drag;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Rect;  
  5. import android.util.AttributeSet;  
  6. import android.util.Log;  
  7. import android.view.MotionEvent;  
  8. import android.view.View;  
  9. import android.view.animation.TranslateAnimation;  
  10. import android.widget.ScrollView;  
  11.   
  12. /** 
  13.  * ScrollView反弹效果的实现 
  14.  */  
  15. public class MyScrollView extends ScrollView {  
  16.     private View inner;// 孩子View  
  17.   
  18.     private float y;// 点击时y坐标  
  19.   
  20.     private Rect normal = new Rect();// 矩形(这里只是个形式,只是用于判断是否需要动画.)  
  21.   
  22.     private boolean isCount = false;// 是否开始计算  
  23.   
  24.     public MyScrollView(Context context, AttributeSet attrs) {  
  25.         super(context, attrs);  
  26.     }  
  27.   
  28.     /*** 
  29.      * 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate 
  30.      * 方法,也应该调用父类的方法,使该方法得以执行. 
  31.      */  
  32.     @Override  
  33.     protected void onFinishInflate() {  
  34.         if (getChildCount() > 0) {  
  35.             inner = getChildAt(0);  
  36.         }  
  37.     }  
  38.   
  39.     /*** 
  40.      * 监听touch 
  41.      */  
  42.     @Override  
  43.     public boolean onTouchEvent(MotionEvent ev) {  
  44.         if (inner != null) {  
  45.             commOnTouchEvent(ev);  
  46.         }  
  47.   
  48.         return super.onTouchEvent(ev);  
  49.     }  
  50.   
  51.     /*** 
  52.      * 触摸事件 
  53.      *  
  54.      * @param ev 
  55.      */  
  56.     public void commOnTouchEvent(MotionEvent ev) {  
  57.         int action = ev.getAction();  
  58.         switch (action) {  
  59.         case MotionEvent.ACTION_DOWN:  
  60.             break;  
  61.         case MotionEvent.ACTION_UP:  
  62.             // 手指松开.  
  63.             if (isNeedAnimation()) {  
  64.                 animation();  
  65.                 isCount = false;  
  66.             }  
  67.             break;  
  68.         /*** 
  69.          * 排除出第一次移动计算,因为第一次无法得知y坐标, 在MotionEvent.ACTION_DOWN中获取不到, 
  70.          * 因为此时是MyScrollView的touch事件传递到到了LIstView的孩子item上面.所以从第二次计算开始. 
  71.          * 然而我们也要进行初始化,就是第一次移动的时候让滑动距离归0. 之后记录准确了就正常执行. 
  72.          */  
  73.         case MotionEvent.ACTION_MOVE:  
  74.             final float preY = y;// 按下时的y坐标  
  75.             float nowY = ev.getY();// 时时y坐标  
  76.             int deltaY = (int) (preY - nowY);// 滑动距离  
  77.             if (!isCount) {  
  78.                 deltaY = 0// 在这里要归0.  
  79.             }  
  80.   
  81.             y = nowY;  
  82.             // 当滚动到最上或者最下时就不会再滚动,这时移动布局  
  83.             if (isNeedMove()) {  
  84.                 // 初始化头部矩形  
  85.                 if (normal.isEmpty()) {  
  86.                     // 保存正常的布局位置  
  87.                     normal.set(inner.getLeft(), inner.getTop(),  
  88.                             inner.getRight(), inner.getBottom());  
  89.                 }  
  90.                 Log.e("jj""矩形:" + inner.getLeft() + "," + inner.getTop()  
  91.                         + "," + inner.getRight() + "," + inner.getBottom());  
  92.                 // 移动布局  
  93.                 inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,  
  94.                         inner.getRight(), inner.getBottom() - deltaY / 2);  
  95.             }  
  96.             isCount = true;  
  97.             break;  
  98.   
  99.         default:  
  100.             break;  
  101.         }  
  102.     }  
  103.   
  104.     /*** 
  105.      * 回缩动画 
  106.      */  
  107.     public void animation() {  
  108.         // 开启移动动画  
  109.         TranslateAnimation ta = new TranslateAnimation(00, inner.getTop(),  
  110.                 normal.top);  
  111.         ta.setDuration(200);  
  112.         inner.startAnimation(ta);  
  113.         // 设置回到正常的布局位置  
  114.         inner.layout(normal.left, normal.top, normal.right, normal.bottom);  
  115.   
  116.         Log.e("jj""回归:" + normal.left + "," + normal.top + "," + normal.right  
  117.                 + "," + normal.bottom);  
  118.   
  119.         normal.setEmpty();  
  120.   
  121.     }  
  122.   
  123.     // 是否需要开启动画  
  124.     public boolean isNeedAnimation() {  
  125.         return !normal.isEmpty();  
  126.     }  
  127.   
  128.     /*** 
  129.      * 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的总高度 
  130.      *  
  131.      * getHeight():获取的是屏幕的高度 
  132.      *  
  133.      * @return 
  134.      */  
  135.     public boolean isNeedMove() {  
  136.         int offset = inner.getMeasuredHeight() - getHeight();  
  137.         int scrollY = getScrollY();  
  138.         Log.e("jj""scrolly=" + scrollY);  
  139.         // 0是顶部,后面那个是底部  
  140.         if (scrollY == 0 || scrollY == offset) {  
  141.             return true;  
  142.         }  
  143.         return false;  
  144.     }  
  145.   
  146. }  
这个与上面那个稍有不同,我简单讲解下不同之处:

我们要排除出第一次ACTION_MOVE移动计算,因为第一次无法得知y坐标, 在MotionEvent.ACTION_DOWN中是获取不到, 因为此时是MyScrollView的touch事件传递到到了LIstView的孩子item上面.所以从第二次计算开始.(这里其实不应该这么搞,但是想不到更好的方法就只有这么搞了.如有好方法,记得share). 然而我们也要进行对第一次移动的距离deltaY进行初始化,就是第一次移动的时候让滑动距离归0. 之后的在进行记录执行相应的计算.

哈哈,明白了吧.其实也不难,可是也花了我好些时间.看下效果图吧.

效果图:

                    

              原样                                      下拉会自动缩回                       上拉会自动缩回


看着勉强凑合吧,重要的是实现.




/********************************仔细品读想必对你有帮助*****************************************/

在这里我额外补充一点,不管你是否遇到过,总之掌握了对你没有害处,为之,我可是花费了2个小时才发现错误在哪里,原因:自己很土鳖.呵呵,不瞎扯了.

adapter.notifyDataSetChanged();这句话想必大家都不陌生, 作用:动态更新UI数据用的.

用法简单说明:异步或开线程对数据源BaseAdapter里面的数据进行更新.然后在主线程中执行adapter.notifyDataSetChanged();(其实就是从新执行getView方法,自己可以调试try 一下.)

但是我们这里用的不是单纯的ListView,而是自定义的ScrollView包裹着ListView,而ListView和ScrollView本来是冤家,想必现在大家都明白怎么解决,就是我setAdapter();后再次对ListView布局从新布局,这样有效的解决了二者的冲突.

方法如下:(大家都看的明白,这里就不过多解释)

[java] view plaincopy
  1. /*** 
  2.      * 动态设置listview的高度 
  3.      *  
  4.      * @param listView 
  5.      */  
  6.     public void setListViewHeightBasedOnChildren(ListView listView) {  
  7.         ListAdapter listAdapter = listView.getAdapter();  
  8.         if (listAdapter == null) {  
  9.             return;  
  10.         }  
  11.         int totalHeight = 0;  
  12.         for (int i = 0; i < listAdapter.getCount(); i++) {  
  13.             View listItem = listAdapter.getView(i, null, listView);  
  14.             listItem.measure(00);  
  15.             totalHeight += listItem.getMeasuredHeight();  
  16.         }  
  17.         ViewGroup.LayoutParams params = listView.getLayoutParams();  
  18.         params.height = totalHeight  
  19.                 + (listView.getDividerHeight() * (listAdapter.getCount() - 1))  
  20.                 + 15;  
  21.         listView.setLayoutParams(params);  
  22.     }  
重点:我们在其中执行了adapter.notifyDataSetChanged();UI没有做出任何反映,这个为什么呢?

牛逼人物我想一目了然就知道哪里,但是我却花了2个小时,原因:还是原理没搞懂.

解释:我们首先解释下显示的效果原理:首先我们先把ListView show出来了,因为和ScrollView冲突然后我们又调用setListViewHeightBasedOnChildren();进行重新排版布局,才使得显示我们要的那种效果,之后我们对数据源做的修改,调用了adapter.notifyDataSetChanged();原理上应该显示出来了(此时数据源已经更新),因为此时我们setListViewHeightBasedOnChildren();只是显示我们修改数据之前,所以UI是不会更新的,所以要想更新,那么我们得从新调用一下setListViewHeightBasedOnChildren();根据最新的数据源绘图,这样就不会出错了。大家明白了吧,只要明白道理了,其实很简单.

效果图就不展示了,想必大家都知道是what.


上诉有一个小BUG,就是如果当数据超出一屏的话,你下拉的时候不松开,然后慢慢向上移动,你会发现跑得那是相当的快,如果想做成微信那种效果就是下拉的时候刷新,那么相当不好控制,原因其实很简单:当我们下拉的时候用的根本不是ScrollView的滑动.我们是通过对layout布局进行时时更新,此时的getScrollY()始终为0,但是你停止下拉的时候反而向上滑动的时候这个时候getScrollY()就不为0,原因为什么呢,因为我们下拉其实就是让布局向下移动了,抛开这里其实就相当于我滑动了,所以你上滑动的时候会ScrollView滑动会影响你,(自己可以测试看下log.)这就是bug原因,解决办法:我们只需要在滑动中添加: scrollBy(0, -deltaY);当超过一屏的时候ScrollView就会有自己的滑动,但是必须取消该滑动,因为我们有自己的滑动,作用:抵消ScrollView自带滑动.从而至运用对布局时时更新.

问题解决了,不过又有一个扯淡的问题,我到底在瞎忙什么,直接用简单的布局文件不就行了,为何要用ScrollView呢,还有ScrollView这个东西和ListView冲突在加上对Touch事件的分发处理不是那么协调,NND,搞了一下午,现在头都大了.


就说到这里,如有疑问请留言。

另外,如果对您有帮助的话,记得赞一个哦.

在此:Thanks for you !


0 0