當(dāng)父容器和子元素都可以滑動時渣磷,就會產(chǎn)生滑動沖突,比如ScrollView里面套一個ListView或者GridView,或者ListView中嵌套了ViewPager等等霹娄。
常見的滑動沖突場景有以下幾種:
- 外部ViewGroup的滑動方向和內(nèi)部元素滑動方向不一致
- 外部滑動方向和內(nèi)部滑動方法一致
- 以上兩種情況的嵌套
第一種場景比如講ViewPager和Fragment配置使用所組成的頁面滑動效果觉啊。這種效果通過左右滑動來切換頁面拣宏,而每個頁面的內(nèi)部往往又是一個豎直滑動的ListView,本來這種情況是有滑動沖突的杠人,但是ViewPager內(nèi)部處理了這種沖突勋乾。如果才使用ScrollView而不是ViewPager,那么我們就必須手動來處理滑動沖突了搜吧,否則出現(xiàn)的后果就是內(nèi)外兩層只有一層能夠滑動市俊,
第二種場景當(dāng)內(nèi)外兩層在同一方向滑動時,如果手指觸摸開始滑動滤奈,但是系統(tǒng)無法知道到底要讓哪一層滑動摆昧,所以會出現(xiàn)這時候會出問題。
滑動沖突的處理規(guī)則
對于場景1的處理規(guī)則:當(dāng)手指左右滑動時蜒程,需要讓外部的元素?cái)r截點(diǎn)擊事件绅你,當(dāng)手指上下滑動時,需要讓內(nèi)部的元素?cái)r截點(diǎn)擊事件昭躺,這時候就可以根據(jù)元素的特性來解決沖突問題忌锯。具體一點(diǎn)就是根據(jù)滑動方向的不同來判斷到底由誰來攔截事件。如果是斜著滑動的领炫,那我們可以計(jì)算出兩點(diǎn)之間的水平距離和豎直距離偶垮,距離較大的那個方向判定為滑動方向,然后決定是攔截對應(yīng)的元素。
對于場景2和場景3的處理規(guī)則:這種情況無法根據(jù)滑動的角度距離差等因素來判斷似舵。但是我們可以根據(jù)不同的狀態(tài)來決定滑動哪個元素脚猾。
解決滑動沖突的方法:
攔截父容器
外部攔截是指所有的點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要這個事件就攔截砚哗,不需要就不攔截龙助。外部攔截需要重寫父容器的onInterceptTouchEvent方法。
public boolean onInterceotTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) evnet.getY();
switch(event.getAction) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器是否需要處理當(dāng)前點(diǎn)擊事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
return intercepted;
}
以上就是外部攔截的偽代碼蛛芥,在ACTION_DOWN事件中提鸟,父容器必須返回false,因?yàn)楦溉萜饕坏r截了ACTION_DOWN事件仅淑,那么后續(xù)一系列的ACTION_MOVE和ACTION_UP事件都會直接交給父容器處理称勋,此時事件就沒辦法再傳遞給子元素了,ACTION_UP一般也不需要攔截漓糙,本身也沒多大意義铣缠,ACTION_MOVE根據(jù)需要來決定是否攔截。
攔截子控件
內(nèi)部攔截是指父容器不攔截任何事件昆禽,所有的事件都交給子元素蝗蛙,如果子元素需要此事件就直接消耗,否則就交給父容器來處理醉鳖,這種方法和Android中事件分發(fā)機(jī)制不一致髓梅,需要配合requestDisallowInterceptTouchEvent方法才能正常工作泛鸟。內(nèi)部攔截需要重寫子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) evnet.getY();
switch(event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此類點(diǎn)擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(evnet);
}
除了子元素需要做上述所示的修改外粥鞋,父元素也要默認(rèn)攔截除了ACTION_DOWN以外的其它事件尚辑,這樣當(dāng)子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false);時父容器才能繼續(xù)攔截所需要的事件。
之所以父容器不能攔截ACTION_DOWN纹因,是因?yàn)锳CTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT這個標(biāo)記位的影響喷屋,一旦父容器攔截了ACTION_DOWN,那么所有的事件都無法傳遞到子元素瞭恰,屯曹,內(nèi)部攔截就無效了。父容器要做的修改如下代碼所示:
public boolean onInterceptTouchEvent(MotionEvent event) {
if(evnet.getAction == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
接下來通過一個demo來分別實(shí)現(xiàn)通過外部攔截和內(nèi)部攔截來解決滑動沖突惊畏。
我們先定義一個可以水平滑動的ViewGroup恶耽,其實(shí)就是仿ViewPager的功能,然后在里面添加若干個豎直滑動的ListView颜启,這就模擬了一個典型的滑動沖突的場景偷俭。根據(jù)滑動策略,我們可以選擇水平和豎直滑動距離差來解決滑動沖突缰盏。
public class CustomViewPager extends ViewGroup {
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private int mChildWidth;// 子View的寬度
private int mChildIndex;// 子View的位置索引
private int mChildrenSize;// 子View個數(shù)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private int mLastX = 0;
private int mLastY = 0;
public CustomViewPager(Context context) {
this(context, null);
}
public CustomViewPager(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
// 如果父容器滑動沒有結(jié)束涌萤,那么下一個事件仍然交給父容器來處理淹遵,
// 避免當(dāng)父容器水平滑動沒有結(jié)束時,用戶立即豎直滑動负溪,而造成界面停留在中間狀態(tài)這個不好的體驗(yàn)合呐。
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 表示水平滑動,父容器需要攔截
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
// 計(jì)算水平速度
float xVelocity = mVelocityTracker.getXVelocity();
// 這里了是模擬ViewPager快速滑動時笙以,即使只滑動了一小段距離,也可以滑到下一頁去
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) /mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
mVelocityTracker.clear();
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
} else {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
content_layout布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:textColor="@android:color/white" />
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff4f7f9"
android:cacheColorHint="#00000000"
android:divider="#dddbdb"
android:dividerHeight="1px"
android:listSelector="@android:color/transparent" />
</LinearLayout>
在Activity的layout中加入我們自定義的ViewGroup:
<com.shenhuniurou.viewdemo.CustomViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_red_light">
</com.shenhuniurou.viewdemo.CustomViewPager>
Activity中的主要代碼:
private void initView() {
LayoutInflater inflater = getLayoutInflater();
mListContainer = (CustomViewPager) findViewById(R.id.container);
int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
// 往ViewGroup中添加ListView冻辩,這里是把含有ListView的一整個布局加進(jìn)去
for (int i = 0; i < 5; i++) {
ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout, mListContainer, false);
layout.getLayoutParams().width = screenWidth;
TextView textView = (TextView) layout.findViewById(R.id.title);
textView.setText("這是第 " + (i + 1) + "頁");
layout.setBackgroundColor(Color.rgb(255 / (i + 10), 255 / (i + 10), 10));
addListView(layout);
mListContainer.addView(layout);
}
}
private void addListView(ViewGroup layout) {
ListView listView = (ListView) layout.findViewById(R.id.list);
List<String> datas = new ArrayList<>();
for (int i = 0; i < 50; i++) {
datas.add("item " + i);
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.content_list_item, R.id.name, datas);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(MainActivity.this, "click item", Toast.LENGTH_SHORT).show();
}
});
}
效果如圖:
上面介紹了使用攔截父容器的方法猖腕,如果要從攔截子控件入手,又該怎么做呢恨闪?其實(shí)也很簡單倘感,原理就是上面所說的重寫ListView的dispatchTouchEvent方法,讓父容器攔截掉ACTION_MOVE事件咙咽,判斷水平方向和豎直方向滑動的距離誰大老玛,如果是水平方向滑動距離大,父容器就攔截MOVE事件钧敞。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 讓父容器不攔截ACTION_DOWN事件
customViewPager.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 如果水平方向滑動的距離多一點(diǎn)蜡豹,那就表示讓父容器水平滑動,子控件不滑動溉苛,讓父容器攔截事件
customViewPager.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
另外為了避免父容器水平方向滑動還未停止但用戶立即開始豎直滑動時镜廉,父容器里面的子控件可能會停留在一個中間狀態(tài)的情況,當(dāng)水平方法還在滑動時愚战,讓父容器攔截事件去處理娇唯,所以這里還是要重寫父容器的onInterceptTouchEvent方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
以上就是解決外部ViewGroup的滑動方向和內(nèi)部元素滑動方向不一致這種場景的兩種方法了。如果是內(nèi)外元素滑動方向一致呢寂玲,又該怎么解決塔插?
這種情況解決辦法和上面的場景一樣,只是滑動規(guī)則不同而已拓哟,比如當(dāng)使用攔截父容器的方法時想许,就是在MOTIONEVENT.ACTION_MOVE事件中的判斷條件不同,根據(jù)我們自己的需求來定制彰檬,看當(dāng)前事件要交給父容器還是子元素來處理伸刃。也就是說上面那兩段偽代碼基本就是解決滑動沖突的通用方案了。