android 弹性ScrollView(已优化)

来源:互联网 发布:mac打开网页一直转圈 编辑:程序博客网 时间:2024/06/09 18:18
 

android 弹性ScrollView(已优化)

标签: androidScrollVeiw
 6748人阅读 评论(8) 收藏 举报
 分类:

目录(?)[+]

想要的效果

最近项目中想实现一个效果,效果如下: 
这里写图片描述

网上demo展示

就是上滑或者下滑,能实现弹性效果,刚开始在网上找了好几个demo,代码大致如下:

public class BounceScrollView extends ScrollView {    private View inner;// 孩子View      private float y;// 点击时y坐标      private Rect normal = new Rect();// 矩形(这里只是个形式,只是用于判断是否需要动画.)      private boolean isCount = false;// 是否开始计算      public BounceScrollView(Context context, AttributeSet attrs) {          super(context, attrs);      }      /***      * 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate      * 方法,也应该调用父类的方法,使该方法得以执行.      */      @Override      protected void onFinishInflate() {          if (getChildCount() > 0) {              inner = getChildAt(0);          }      }      /***      * 监听touch      */      @Override      public boolean onTouchEvent(MotionEvent ev) {          if (inner != null) {              commOnTouchEvent(ev);          }          return super.onTouchEvent(ev);      }      /***      * 触摸事件      *       * @param ev      */      public void commOnTouchEvent(MotionEvent ev) {          int action = ev.getAction();          switch (action) {          case MotionEvent.ACTION_DOWN:              break;          case MotionEvent.ACTION_UP:              // 手指松开.              if (isNeedAnimation()) {                  animation();                  isCount = false;              }              break;          case MotionEvent.ACTION_MOVE:              final float preY = y;// 按下时的y坐标              float nowY = ev.getY();// 时时y坐标              int deltaY = (int) (preY - nowY);// 滑动距离              if (!isCount) {                  deltaY = 0; // 在这里要归0.              }              y = nowY;              // 当滚动到最上或者最下时就不会再滚动,这时移动布局              if (isNeedMove()) {                  // 初始化头部矩形                  if (normal.isEmpty()) {                      // 保存正常的布局位置                      normal.set(inner.getLeft(), inner.getTop(),                              inner.getRight(), inner.getBottom());                  }                  // 移动布局                  inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,                          inner.getRight(), inner.getBottom() - deltaY / 2);              }              isCount = true;              break;          default:              break;          }      }      /***      * 回缩动画      */      public void animation() {          // 开启移动动画          TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),                  normal.top);          ta.setDuration(200);          inner.startAnimation(ta);          // 设置回到正常的布局位置          inner.layout(normal.left, normal.top, normal.right, normal.bottom);          normal.setEmpty();      }      // 是否需要开启动画      public boolean isNeedAnimation() {          return !normal.isEmpty();      }      /***      * 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的总高度      *       * getHeight():获取的是屏幕的高度      *       * @return      */      public boolean isNeedMove() {          int offset = inner.getMeasuredHeight() - getHeight();          int scrollY = getScrollY();          // 0是顶部,后面那个是底部          if (scrollY == 0 || scrollY == offset) {              return true;          }          return false;      }}
  • 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
  • 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

它的实现原理:

  1. 通过getChildAt(0)方法获取到scrollView的唯一一个子view—inner, 并用矩形Rect–>normal记录子view的初始位置(left,top,right,bottom);
  2. 在onTouchEvent()方法的ACTION_MOVE中,根据子view inner滑动的距离,用view.setLayout(left,top,right,bottom)对子view inner进行滑动;
  3. 在onTouchEvent()方法的ACTION_UP中,对子view –> inner设置帧动画,让其回缩到原始位置。

出现的问题:

  1. scrollView中的子view,必须是溢出全屏的(比如说子view中含有较长的listview之类),才能实现弹性效果,就是设置scrollView或者它的子View的layout_height为match_content也没用。这种效果就看起来很尴尬,在安全中心,退出程序之类的按钮上滑动就没反应,在下面灰色部分上下滑动就有弹性效果。这是个bug,得治!

解决原理:

  1. 根据事件分发机制,重写子view的dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent();
  2. 根据事件分发机制,重写父类->scrollView的dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent() 
    关于android的事件分发机制,在文章http://blog.csdn.net/a394268045/article/details/51889797有详细介绍

在自定义的scrollView和它的子View的三个方法中,分别加上log,会发现,这里写图片描述 
当在上图中画红圈部分上下滑动时,log如下:

BounceScrollView dispatchTouchEvent...ACTION_DOWNBounceScrollView onInterceptTouchEvent...ACTION_DOWNBounceScrollView onTouchEvent...ACTION_DOWNBounceScrollView dispatchTouchEvent...ACTION_MOVEBounceScrollView onTouchEvent...ACTION_MOVEBounceScrollView dispatchTouchEvent...ACTION_MOVEBounceScrollView onTouchEvent...ACTION_MOVEBounceScrollView dispatchTouchEvent...ACTION_MOVEBounceScrollView onTouchEvent...ACTION_MOVEBounceScrollView dispatchTouchEvent...ACTION_UPBounceScrollView onTouchEvent...ACTION_UP
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当在图中类似安全中心等按钮上滑动时,log如下:

BounceScrollView dispatchTouchEvent...ACTION_DOWNBounceScrollView onInterceptTouchEvent...ACTION_DOWN子View           dispatchTouchEvent...ACTION_DOWN子View           onInterceptTouchEvent...ACTION_DOWN子View           onTouchEvent...ACTION_DOWNBounceScrollView dispatchTouchEvent...ACTION_MOVEBounceScrollView onInterceptTouchEvent...ACTION_MOVE子View           dispatchTouchEvent...ACTION_MOVE子View           onTouchEvent...ACTION_MOVEBounceScrollView dispatchTouchEvent...ACTION_MOVEBounceScrollView onInterceptTouchEvent...ACTION_MOVE子View           dispatchTouchEvent...ACTION_MOVE子View           onTouchEvent...ACTION_MOVEBounceScrollView dispatchTouchEvent...ACTION_MOVEBounceScrollView onInterceptTouchEvent...ACTION_MOVE子View           dispatchTouchEvent...ACTION_MOVE子View           onTouchEvent...ACTION_MOVEBounceScrollView dispatchTouchEvent...ACTION_UPBounceScrollView onInterceptTouchEvent...ACTION_UP子View           dispatchTouchEvent...ACTION_UP子View           onTouchEvent...ACTION_UP
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

最终解决方案

如果在子view中处理,那么这个父View BounceScrollView就没有通用性,而且,当子view类型不同时,还得处理各种想不到的问题。所以,在父view中处理弹性效果是最好的。但是,当我在子view上下滑动时,BounceScrollView居然没有调用onTouchEvent()!而不管是在画红圈区域上下滑动,还是在按钮上滑动,都会调用父View的dispatchTouchEvent()方法。OK,那我们就可以将弹性效果放到dispatchTouchEvent()中,并加上上下滑动的判断,最终代码如下:

/*   *@author luoxiaohui *@date 2016年4月18日 *@description 具有弹性的ScrollView **/public class BounceScrollView extends ScrollView {    private static final String TAG = "BounceScrollView";    private View inner;// 孩子View      private float y;// 点击时y坐标      private Rect normal = new Rect();// 矩形(这里只是个形式,只是用于判断是否需要动画.)      private boolean isCount = false;// 是否开始计算      private float lastX = 0;    private float lastY = 0;    private float currentX = 0;    private float currentY = 0;    private float distanceX = 0;    private float distanceY = 0;    private boolean upDownSlide = false; //判断上下滑动的flag    public BounceScrollView(Context context, AttributeSet attrs) {          super(context, attrs);      }      /***      * 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate      * 方法,也应该调用父类的方法,使该方法得以执行.      */      @Override      protected void onFinishInflate() {          if (getChildCount() > 0) {              inner = getChildAt(0);              View view;        }      }      @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        // TODO Auto-generated method stub        currentX = ev.getX();        currentY = ev.getY();        switch(ev.getAction()){        case MotionEvent.ACTION_DOWN:            break;        case MotionEvent.ACTION_MOVE:            distanceX = currentX - lastX;            distanceY = currentY - lastY;            if(Math.abs(distanceX)<Math.abs(distanceY) && Math.abs(distanceY)>12){                upDownSlide = true;            }            break;        case MotionEvent.ACTION_UP:            break;        default:            break;                  }        lastX = currentX;        lastY = currentY;        if (upDownSlide && inner != null) commOnTouchEvent(ev);         return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        // TODO Auto-generated method stub        return super.onInterceptTouchEvent(ev);    }    /***      * 监听touch      */      @Override      public boolean onTouchEvent(MotionEvent ev) {         return super.onTouchEvent(ev);      }    /***      * 触摸事件      *       * @param ev      */      public void commOnTouchEvent(MotionEvent ev) {          int action = ev.getAction();          switch (action) {          case MotionEvent.ACTION_DOWN:              break;          case MotionEvent.ACTION_UP:              // 手指松开.              if (isNeedAnimation()) {                  animation();                  isCount = false;              }              clear0();            break;               */          case MotionEvent.ACTION_MOVE:              final float preY = y;// 按下时的y坐标              float nowY = ev.getY();// 时时y坐标              int deltaY = (int) (preY - nowY);// 滑动距离              if (!isCount) {                  deltaY = 0; // 在这里要归0.              }              y = nowY;              // 当滚动到最上或者最下时就不会再滚动,这时移动布局              if (isNeedMove()) {                  // 初始化头部矩形                  if (normal.isEmpty()) {                      // 保存正常的布局位置                      normal.set(inner.getLeft(), inner.getTop(),                              inner.getRight(), inner.getBottom());                  }                  // 移动布局                  inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,                          inner.getRight(), inner.getBottom() - deltaY / 2);              }              isCount = true;              break;          default:              break;          }      }      /***      * 回缩动画      */      public void animation() {          // 开启移动动画          TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),                  normal.top);          ta.setDuration(200);          inner.startAnimation(ta);          // 设置回到正常的布局位置          inner.layout(normal.left, normal.top, normal.right, normal.bottom);          normal.setEmpty();      }      // 是否需要开启动画      public boolean isNeedAnimation() {          return !normal.isEmpty();      }      /***      * 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的总高度      *       * getHeight():获取的是屏幕的高度      *       * @return      */      public boolean isNeedMove() {          int offset = inner.getMeasuredHeight() - getHeight();          int scrollY = getScrollY();          // 0是顶部,后面那个是底部          if (scrollY == 0 || scrollY == offset) {              return true;          }          return false;      }    private void clear0(){        lastX = 0;        lastY = 0;        distanceX = 0;        distanceY = 0;        upDownSlide = false;    }
  • 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
  • 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

这种处理方式,毫不影响子View的onClick事件,并能完美实现弹性效果,经过反复测试,现已放入项目中~