先上最终效果图:
分析一下要解决的Touch事件
- 当流布局展示时,流布局消费事件,ListView不能消费事件
- 当流布局关闭时,ListView消费事件,并且当滑倒顶部时,下拉的滑动事件交由父控件消费,而上滑的滑动事件依然自己消费。
看代码
public class FoldMenu extends FrameLayout{
private ViewDragHelper mViewDragHelper;
private View mBottomView;
private View mTopView;
public FoldMenu(Context context) {
this(context,null);
}
public FoldMenu(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public FoldMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this,mDragHelper);
}
private ViewDragHelper.Callback mDragHelper =new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//mTopView可以滑动 返回true
return child==mTopView;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
if(childCount!=2){
throw new IllegalArgumentException("只能有两个子布局");
}
mBottomView = getChildAt(0);
mTopView = getChildAt(1);
}
//将事件交给ViewDragHelper处理,应为需要完整的接受到DOWN->MOVE->UP
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
//消费事件
return true;
}
}
目前代码应该非常简单,就是一个FrameLayout,里面两个子控件,其中mTopView是可以上下移动。
效果如下
好像挺简单没什么问题,里面的两个子控件是TextView,TextView的CLICKABLE的属性和LOGNG_CLICKABLE默认都为false。
记得上篇的内容的结论8,View的enable属性不影响onTouchEvent的默认返回值,只要它的clickable或者longCikcable有一个为true,那么它的onTouchEvent就返回true。
而TextView是默认不消费事件的,所以FoldMenu中,onTouchEvent只需要返回true,就可以接收到全部事件了,但是如果我们将两个子View换成Button,或者设置TextView的CLICKABLE属性为true,你会发现滑动失效了。
这是因为子控件消费了事件,mViewDragHelper接收不到Touch事件,从而导致失效。
解决办法,父控件拦截事件,我们只需重写onInterceptTouchEvent即可。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
好了,继续写代码,看最终效果,首先我们的View需要有自动回弹,拉动的最大距离,还有不能划出顶部
public class FoldMenu extends FrameLayout {
private ViewDragHelper mViewDragHelper;
private View mBottomView;
private View mTopView;
private int mBottomHeight;
boolean mMenuIsOpen=false;
public FoldMenu(Context context) {
this(context,null);
}
public FoldMenu(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public FoldMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this,2f,mDragHelper);
}
private ViewDragHelper.Callback mDragHelper =new ViewDragHelper.Callback() {
//捕获可滑动的View
@Override
public boolean tryCaptureView(View child, int pointerId) {
//上面的可以滑动 返回true
return child==mTopView;
}
//固定View滑动的方向
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if(top<=0){
top=0;
}
if(top>=mBottomHeight*1.3){
top= (int) (mBottomHeight*1.3);
}
return top;
}
//抬起手指的回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//弹性滑动
if(mTopView.getTop()<=mBottomHeight/2){
//关闭
mViewDragHelper.settleCapturedViewAt(0,0);
mMenuIsOpen=false;
}else{
//打开
mViewDragHelper.settleCapturedViewAt(0,mBottomHeight);
mMenuIsOpen=true;
}
invalidate();
}
};
/**
* 响应滚动
*/
@Override
public void computeScroll() {
if(mViewDragHelper.continueSettling(true)){
invalidate();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
if(childCount!=2){
throw new IllegalArgumentException("只能有两个子布局");
}
mBottomView = getChildAt(0);
mTopView = getChildAt(1);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mBottomHeight = mBottomView.getMeasuredHeight();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
//消费事件
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
这里有几点需要注意下:
- mBottomHeight 高度的获取,必须在测量之后才能取到值,而onFinishInflate方法是在视图加载完毕,其实是在setContenView中的inflate方法中调用的,此时视图并没用走measure方法,所以这里我们需要在onLayout方法中获取到它的高度。
- onViewReleased方法中,调用ViewDragHelper的settleCapturedViewAt滑动方法,需要重写computeScroll方法。
效果如下:
看效果好像差不多了,那我们把两个子控件分别替换成流布局和ListView试一试,(流布局,前面有写过,可以翻一翻,具体代码,这里不再写了)
直接看结果:
乍一看,好像是可以了,但是可以发现,子控件的点击事件都被拦截了,不管是流布局的点击事件还是ListView的滑动事件都失效了。
- 打开状态 流布局消费事件 ListView不消费事件
- 关闭状态 ListView向上滑动时消费事件,向下滑动时又分两点:
- ListView滑到顶端时,向下滑动事件需要交由父控件,也就是拦截事件
- ListView不在顶端时,向下滑动则由ListView本身消费事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//如果打开状态下,拦截事件
if(mMenuIsOpen){
if(ev.getY()<mBootomHeight){
return false;
}
return true;
}
//关闭状态下
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDownY = ev.getY();
//把down事件传给mViewDragHelper
//onInterceptTouchEvent DOWN--子view的onTouchEvent WOWN
//onInterceptTouchEvent MOVE --返回true 走本身的onTouchEvent 此时mViewDragHelper只有MOVE事件而没有DOWN事件
mViewDragHelper.processTouchEvent(ev);
intercept=false;
break;
case MotionEvent.ACTION_MOVE:
float moveY = ev.getY();
//如果向下滑动,并且不是顶点
if(moveY>mDownY&&!canChildScrollUp()){
intercept=true;
}else{
intercept=false;
}
break;
}
return intercept;
}
//判断所有滑动控件是否滚动到了最顶部
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mListView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mListView;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mListView, -1) || mListView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mListView, -1);
}
}
这里要注意的是,在ACTION_DOWN中,我们需要调用mViewDragHelper.processTouchEvent(ev)方法,把DOWN事件传递给他,这是因为,ViewDragHelper需要完整的事件,而我们调用onInterceptTouchEvent,在DOWN事件的时候,我们并没有拦截事件,而是把它传递给了子控件,调用了child.onTouchEvent方法,这时,到下次MOVE事件,我们拦截了事件,走了本身的onTouchEvent方法,但是这时的事件只有MOVE和后续事件,DOWN事件已经交给了子控件,所以这里需要在调用mViewDragHelper.processTouchEvent(ev)方法,把DOWN事件传递给ViewDragHelper。
为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件,并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截了ACTION_DOWN事件,那么所有的事件都无法传递到子元素中,这样内部拦截就无法起作用了。
而且,拦截DOWN事件之后,后续MOVE UP事件都不会再经过onInterceptTouchEvent方法,那是因为mFirstTouchTarget 没有成功赋值,条件不成立,所以后续逻辑无法进行,也就无法判断滑动是否到顶点,是向下还是向上等逻辑,所以父控件的DOWN事件不能拦截。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}