最近在解决一个比较棘手的问题,就是ScrollView与父视图之间滑动冲突的问题。
这里所说的滑动冲突,是指包裹ScrollView的父视图本身就是一个可以滑动的组件,比如说ScrollView嵌套ScrollView的情况(当然,这在实际应用中是没有意义的,因为ScrollView只能包含一个子组件,这里只是举一个栗子)。
问题定义
说要解决这样一个问题,肯定会有点摸不到头脑,但我们可以用分解法,来“大事化小”。
遇到这种滑动事件冲突的嵌套首先要明白一个孰先孰后的问题,就是先让父组件滑动还是先让子组件滑动。按照一般的场景,我们一般会选择让子组件先滑动,因为这样会比较好处理一点。
那么问题就来了,为了先让子组件滑动,我们需要做哪些事情呢?
- 监听子组件滑动到顶部
- 监听子组件滑动到底部
- 在子组件滑动完成后将滑动事件传递回父组件
- 可能的卡顿及优化
好了,根据这几个问题,让我们一个个来解决他们。
解决步骤
1. 子组件滑动到顶部与底部的监听
其实上面的问题1和问题2是可以合并为一个监听子组件滑动靠岸的问题,因为无论是靠顶部还是靠底部其实都是滑动已经完成的信号。
这里我们需要自定义一个自己的ScrollView:
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
/**
* Created by lemondoor on 15/8/8.
*/
public class MyScrollView extends ScrollView {
private View contentView; //ScrollView包含的子组件
private OnBorderListener onBorderListener;
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/* 在滑动发生时监听滑动靠岸 */
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
doOnBorderListener();
}
/**
* 关键函数,判断什么时候滑动靠岸
* 因为ScrollView只包含一个子组件,因此子组件的原始测量高度就是ScrollView的高度,
* 当纵向滑动值(滑动出屏幕的高度)加上屏幕上显示的高度大于或等于ScrollView的高度时,就说明到底了,
* 当纵向滑动值为0的时候,是从顶部开始显示的,因此说明处于顶部位置
*/
private void doOnBorderListener() {
if (contentView != null && contentView.getMeasuredHeight() <= getScrollY() + getHeight()) {
if (onBorderListener != null) {
onBorderListener.onBottom();
}
} else if (getScrollY() == 0) {
if (onBorderListener != null) {
onBorderListener.onTop();
}
}
}
public void setOnBorderListener(final OnBorderListener onBorderListener) {
if(onBorderListener == null) {
this.onBorderTouchListener = null;
return;
} else {
this.onBorderListener = onBorderListener;
}
if(contentView == null) {
contentView = getChildAt(0); //因为ScrollView只能包含一个子组件,因此可以直接通过索引‘0’获取子组件
}
}
/**
* OnborderListener, called when scroll to top or bottom
*/
public static interface OnBorderListener {
/**
* Called when scroll to bottom
*/
public void onBottom();
/**
* Called when scroll to top
*/
public void onTop();
}
}
好了,通过上面的代码,我们已经可以检测到子组件滑动完成的状态了。
2. 在检测到子组件滑动完成后将滑动事件传递给父组件
前面拿到了子组件滑动靠岸的信息,这个时候就需要将事件还给父组件了,毕竟人家已经眼巴巴地让小辈先玩了那么久。
这一步,我们需要在实例化的MyScrollView的类中进行相应地响应处理。
这就用到了requestDisallowInterceptTouchEvent方法:
mScrollView.setOnBorderListener(new MyScrollView.OnBorderListener() {
@Override
public void onBottom() {
//设为false表示已经不关心该事件了,允许父组件进行事件拦截
mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
}
@Override
public void onTop() {
//设为false表示已经不关心该事件了,允许父组件进行事件拦截
mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
}
});
3. 优化
上面的两步其实已经将问题解决了,但如果试着做一下就会发现,在刚进入页面的时候,子组件处于顶部的判断是没有进行处理的,而且有的时候必须回拉一下才能响应到靠岸的事件。
根据上面的问题,我们制定了两步的优化处理步骤:
3.1 处理子组件初始靠岸状态
其实滑动还是需要手指去与屏幕交互的,我们没有必要在看到页面的状态就判断子组件是否靠岸,只需要在滑动的时候进行判断就好了。
回到我们的MyScrollView类,进行如下修改:
private OnTouchListener onBorderTouchListener;
public void setOnBorderListener(final OnBorderListener onBorderListener) {
if(onBorderListener == null) {
this.onBorderTouchListener = null;
return;
} else {
this.onBorderListener = onBorderListener;
}
if(contentView == null) {
contentView = getChildAt(0); //因为ScrollView只能包含一个子组件,因此可以直接通过索引‘0’获取子组件
}
/* 其实这是另一种处理监听滑动靠岸的方法,是在touch事件发生时进行滑动靠岸监听 */
this.onBorderTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
//这里面试了几个动作,最终正明MOVE动作的效果是最好,最及时的
case MotionEvent.ACTION_MOVE :
doOnBorderListener();
break;
}
return false;
}
};
super.setOnTouchListener(onBorderTouchListener);
}
/**
* 重写setOnTouchListener方法,防止在MyScrollView实例中调用该方法的时候,覆盖掉我们上面做的处理
*/
@Override
public void setOnTouchListener(final OnTouchListener l) {
OnTouchListener onTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (onBorderTouchListener != null) {
onBorderTouchListener.onTouch(v, event);
}
return l.onTouch(v, event);
}
};
super.setOnTouchListener(onTouchListener);
}
这里其实已经处理了上面提到的初始化和偶尔需要会拉才会触发靠岸事件的问题,但又会产生新的父组件会跟着子组件一起滑动的问题,这是因为我们只是给出了允许父组件拦截事件的通道,却没有给出不允许父组件拦截的通道。
3.2 继续优化,设置不允许父组件拦截的情况
其实原理很简单,就是子组件没有滑动靠岸的时候就不允许父组件拦截事件,言下之意就是我还没玩够,你在等等。
还是要修改MyScrollView类:
private void doOnBorderListener() {
if (contentView != null && contentView.getMeasuredHeight() <= getScrollY() + getHeight()) {
if (onBorderListener != null) {
onBorderListener.onBottom();
}
} else if (getScrollY() == 0) {
if (onBorderListener != null) {
onBorderListener.onTop();
}
} else { //没靠岸的时候调用onMiddle方法
if (onBorderListener != null) {
onBorderListener.onMiddle();
}
}
}
//在接口中加一个onMiddle方法,来处理没靠岸的情况
public static interface OnBorderListener {
/**
* Called when scroll to bottom
*/
public void onBottom();
/**
* Called when scroll to top
*/
public void onTop();
/**
* Called when scroll in middle
*/
public void onMiddle();
}
继续在实例化的mScrollView中进行如下修改
mScrollView.setOnBorderListener(new MyScrollView.OnBorderListener() {
@Override
public void onBottom() {
mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
}
@Override
public void onTop() {
mScrollView.getParent().requestDisallowInterceptTouchEvent(false);
}
@Override
public void onMiddle() {
//在没靠岸的时候不允许父组件拦截事件
mScrollView.getParent().requestDisallowInterceptTouchEvent(true);
}
});
引用声明
版权声明:本文为博主原创文章,未经博主允许不得转载。