最近公司没事,研究了下多嵌套滚动组件的事件分发,虽然以前也接触过,但都是拿网上的用,也是特别简单的,正好朋友也需要,就研究了下
这个Demo也不是很完善,放上来也是让各位大牛给指点一下,优化优化
使用情景:
小米商城商品详情界面,界面看似ScrollView,但当正常滚动到底部时,提示继续上拉显示更多详情,上拉后直接滚动到第二屏,第二屏是个ViewPager,ViewPager里面的各个pager有的是WebView有的是ListView,有的是ScrollView,一开始想想就特别头晕,后来理清思路后,实现起来却处处碰壁,不是ViewPager不能左右滑动就是ListView不能上拉,网上也搜索了很多相关Demo,但都没有完善一点的,也许根本没几个人使用这样的无脑嵌套吧,好吧,既然这样,就只有自己动手了。
花了1周时间,总算出来点效果了,重写了几个组件:InnerScrollView、InnerWebView、InnerListView
一、InnerScrollView.java
-
思路:
如果内部ScrollView是固定高度,那么需要滚动,外部的当然也需要滚动,所以要判断当内部滚动到顶部并且手指继续下滑时,把事件交父类处理,同样当滚动到底部并继续上滑时也要交出去,如果InnerScrollView的ChildView高度小于等于InnerScrollView高度(就是不出现滚动条)时,把事件交给父类处理。
-
实现:
只需要在onTouchEvent()里做判断即可,其他不重写package com.wuguangxin.morescrolldemo.view; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.ScrollView; /** * 内部ScrollView,解决滑动内部ScrollView时,触发外部滚动问题 * * @author wuguangxin * @date 16/7/1 上午10:34 */ public class XinInnerScrollView extends ScrollView { private final String TAG = "XinInnerScrollView"; private float childHeight = 0; private float downX, downY; // 按下时 private float currX, currY; // 移动时 private float moveY; // 从按下到移动的Y距离 private float scrollViewHeight; private boolean isOnTop; // ScrollView是否处于屏幕顶端 private boolean isOnBottom; // ScrollView是否处于屏幕底端 private boolean debug = true; private Position position = Position.NONE; public XinInnerScrollView(Context context) { this(context, null); } public XinInnerScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public XinInnerScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: getParent().getParent().requestDisallowInterceptTouchEvent(true); downX = ev.getX(); downY = ev.getY(); childHeight = getChildAt(0).getMeasuredHeight(); scrollViewHeight = getHeight(); break; case MotionEvent.ACTION_MOVE: currX = ev.getX(); currY = ev.getY(); moveY = Math.abs(currY - downY); isOnTop = getScrollY() == 0; isOnBottom = (getScrollY() + scrollViewHeight) == childHeight; // 垂直滑动 if (moveY > Math.abs(currX - downX)) { if (childHeight <= scrollViewHeight) { printLog("onTouchEvent ACTION_MOVE 不能滚动 父处理"); getParent().getParent().requestDisallowInterceptTouchEvent(false); } else if (isOnTop) { // 当前处于ScrollView顶部 if (currY - downY > 0) { printLog("onTouchEvent ACTION_MOVE 已到顶部 下滑 父处理"); getParent().getParent().requestDisallowInterceptTouchEvent(false); } else { printLog("onTouchEvent ACTION_MOVE 已到顶部 上滑 子处理"); } } else if (isOnBottom) { // 当前处于ScrollView底部 if (currY - downY < 0) { printLog("onTouchEvent ACTION_MOVE 已到底部 上滑 父处理"); getParent().getParent().requestDisallowInterceptTouchEvent(false); } else { printLog("onTouchEvent ACTION_MOVE 已到底部 下滑 子处理"); } } else { // 当前处于ScrollView中间 printLog("onTouchEvent ACTION_MOVE 在中间 子处理"); } } // 水平滚动 else { if(position.equals(Position.TOP)){ printLog("onTouchEvent ACTION_MOVE 水平滚动 position=TOP 子处理"); } else { if(Math.abs(currX - downX) > 30){ printLog("onTouchEvent ACTION_MOVE 水平滚动 position!=TOP 横向滑动距离>30 父处理"); getParent().getParent().requestDisallowInterceptTouchEvent(false); } else { printLog("onTouchEvent ACTION_MOVE 水平滚动 position!=TOP 横向滑动距离<=30 子处理"); } } } break; case MotionEvent.ACTION_UP: printLog("onTouchEvent ACTION_UP ========================"); getParent().getParent().requestDisallowInterceptTouchEvent(true); break; } return super.onTouchEvent(ev); } /** * 为了更好的处理手势滑动事件,设置该组件所处的位置; * 比如只有上下两屏时,如果该View是在第一屏,那么设置为Position.TOP,如果在第二屏,则设置为Position.BOTTOM * * @param position */ public void setPosition(Position position) { this.position = position; } public static enum Position { /** * 顶部View,横向滑动时将不考虑将事件交给父View。(该设计只为第一屏为纯ScrollView考虑) */ TOP, /** * 底部View, 横向滑动时,将把事件交给父View处理 */ BOTTOM, /** * 不设置,将自动判断(自动判断并不是很精准) */ NONE } public void printLog(String msg) { if (debug) { Log.d(TAG, msg); } } }
说一下Position,因为第一屏或者第二屏中的ViewPager里面也可能用到InnerScrollView,ViewPager里面的需要考虑左右滑动的事件,但第一屏是不需要的,为了在第一屏做横向滑动时(一般第一屏应该只有一个ScrollView),不把事件交给父类,所以需要知道该InnerScrollView是在哪里使用的,设置该标记,做更好的判断。日志中“子处理”处只打日志,不设置getParent().getParent().requestDisallowInterceptTouchEvent(true);是因为在ACTION_DOWN时已经告诉父类不要拦截,只需要在移动时在适合的条件下通知父类自己不再处理。这就是重写的内部ScrollView。 -
还需要解决的问题
如果准备滚动到底部时,这时不抬起手指继续往回滑,这时事件已经交出去了,往回滑动时,内部ScrollView已经无法滚动了,手势如图:
二、InnerWebView.java
-
思路:
垂直滑动:- 当处于顶部,继续下滑时,交出事件;
- 当处于底部,继续上滑时,交出事件;
- 当处于左侧,继续右滑时,交出事件;
- 当处于右侧,继续左滑时,交出事件;
-
实现
package com.wuguangxin.morescrolldemo.view; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.webkit.WebView; /** * 内部WebView, 该View只适合放在最后一屏 * * @author wuguangxin * @date 16/7/1 上午10:34 */ public class XinInnerWebView extends WebView { private final String TAG = "XinInnerScrollView"; private boolean debug = true; private float downX, downY; // 按下时 private float currX, currY; // 移动时 private float moveX; // 移动长度-横向 public XinInnerWebView(Context context) { super(context); } public XinInnerWebView(Context context, AttributeSet attrs) { super(context, attrs); } public XinInnerWebView(Context context, AttributeSet attrs, int defStyleAtt