转载请注明出处:http://blog.csdn.net/u012732170/article/details/54897422
2017年大家新年好,这还是我第一次在csdn上写博客,主要是自己最近正好在看这方面的知识,一作为存根之用,二也是希望对大家有所帮助,如讲解有不妥之处,欢迎大家指出。废话不多说了,进入今天的主题----Android滑动冲突。
在讲滑动冲突前,有一点是必须要提的,就是View的事件分发机制。View的事件分发机制可以看任玉刚大神写的《android开发艺术探索》,里面关于这方面讲解的十分透彻。下面我简单讲一下这方面的知识。
所谓事件分发,其实就是对View中MotionEvent事件的分发,当一个MotionEvent事件产生时,系统需要把这个事件传递给一个具体的View,这个传递的过程就是分发过程。点击事件的分发过程由三个方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。其中dispatchTouchEvent用于传递事件;onInterceptTouchEvent用来拦截事件(只有ViewGroup有,View不存在此方法),如果返回true,即代表拦截事件,在同一事件序列中此方法不会被再次调用;onTouchEvent用来处理事件,返回true代表消耗此次事件,返回false表示不消耗,并且在同一事件序列中将不再接收到事件。
事件分发的规则:首先我们手指触碰屏幕产生的点击事件先传递给根ViewGroup,此时根ViewGroup的dispatchTouchEvent方法将执行,如果此ViewGroup想拦截该事件,那么此ViewGroup的onInterceptTouchEvent将返回true(ViewGroup的onInterceptTouchEvent默认返回都是false,不拦截),该点击事件交由ViewGroup处理,然后此ViewGroup的onTouchEvent方法将被执行,如果根ViewGroup不拦截事件,那么此事件将传递给ViewGroup的子元素,并且子元素的dispatchTouchEvent方法会被调用,如果子元素也是个ViewGroup,那么子元素也可以决定是否拦截该事件,如果是一个View,那么接下来View的onTouchEvent方法将被执行,如果返回false,即不消耗事件,那么事件将返回给根ViewGroup的onTouchEvent执行,如果返回true,那么事件到此就交由该View完成了。(其实也很好理解,boss将任务交给手下,手下能完成最好,完不成还得boss出马来解决)
理解了事件分发机制,那么对于滑动冲突也很好理解了。滑动冲突分为三种情况:
(1)不同方向冲突,比如HorizontalScrollView内嵌ListView
(2)同方向冲突,比如纵向ScrollView嵌套ListView,两个滑动方向相同
(3)包含1、2中情况的冲突。
解决方案其实就是基于事件分发机制的。
(1)对于第一种情况,一个是横向滑动,一个是纵向滑动,那么我们就让它在横向滑动时将事件交由HorizontalScrollView全权处理,在纵向滑动时将事件交由ListView全权处理,也就是说,当横向滑动时,HorizontalScrollView将拦截事件,此时产生的点击事件就由HorizontalScrollView的onTouchEvent来完成;当纵向滑动时,HorizontalScrollView不拦截事件,此时产生的点击事件就由ListView的onTouchEvent来完成。
(2)对于第二种情况,解决方案也是类似,视具体的业务需求来解决,比如ListView上头还有一个View,当滑动距离在那个View的高度之间时,ScrollView拦截事件,由ScrollView处理上下滑动的事件,当滑动距离到达ListView时,就不拦截事件,让ListView继续处理上下滑动事件,当然,这只是其中的一个例子。
(3)对于第三种情况也是类似的,就是要考虑是否拦截的判定条件变得更为复杂,我下面要讲的例子就涉及了这方面的内容。
既然知道了滑动冲突的解决思路,那么该如何解决滑动冲突呢,有2种方法:(1)外部拦截法,顾名思义,就是从父元素着手来拦截,具体实现就是在父元素的onInterceptTouchEvent方法中根据解决思路来拦截事件(2)内部拦截法,类似的,就是从子元素着手,主动告诉父元素是否要拦截,具体实现就是在子元素的dispatchTouchEvent方法中使用
getParent().requestDisallowInterceptTouchEvent(false);
false的意思就是告诉父元素此时需要拦截事件了,true就是不让父元素拦截,至于getParent()方法,视你的布局而定,如果只有一个ViewGroup和一个View(或者是Viewgroup),那么用一个getParent()就可以了,如果有两个ViewGroup,那么对于最底层的那个View(或者ViewGroup)就要使用两次getParent(),即
getParent().getParent().requestDisallowInterceptTouchEvent(false);
下面上今天的实例,需求:
(1)根布局为自定义的ScrollView,模仿ViewPager的功能(至于为什么不用ViewPager,主要是ViewPager中已经处理了和Listview上下冲突的问题)
(2)ScrollView中有3个fragment
(3)第一个fragment包含一个Listview,第二个fragment包含一个自定义ScrollView(也是模仿ViewPager的功能)
这样整个布局既包含了不同方向滑动冲突,也包含了同方向滑动冲突。
先看主布局文件,自定义ScrollView中包含三个fragment
<?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">
<com.bellnet.slidingconflict.CustomHorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/fragment1"
android:name="com.bellnet.slidingconflict.Fragment1"
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>
<fragment
android:id="@+id/fragment2"
android:name="com.bellnet.slidingconflict.Fragment2"
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>
<fragment
android:id="@+id/fragment3"
android:name="com.bellnet.slidingconflict.Fragment3"
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>
</com.bellnet.slidingconflict.CustomHorizontalScrollView>
</LinearLayout>
下面看这个ScrollView如何来实现,采用的是外部拦截法
/**
* Created by bellnett on 17/2/5.
*/
public class CustomHorizontalScrollView extends ViewGroup {
private Scroller mSroller;//用于完成滚动的实例
private int mTouchSlop;//判定为拖动的最小移动像素数
private int leftBorder;//界面可滚动的左边界
private int rightBorder;//界面可滚动的右边界
private float mFirstX = 0;//第一次触碰屏幕的x坐标
private float mFirstY = 0;//第一次触碰屏幕的y坐标
private float mLastX = 0;//滑动屏幕时的x坐标
private float mLastXIntercept = 0;//用于onInterceptTouchEvent中滑动屏幕的x坐标
private float mLastYIntercept = 0;//用于onInterceptTouchEvent中滑动屏幕的y坐标
public CustomHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
/* 创建scroller的实例 */
mSroller = new Scroller(context);
/* 获取判定滑动的最小移动像素数 */
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
mTouchSlop = viewConfiguration.getScaledTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
measureChild(view, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
if (b) {
int childCount = getChildCount();
for (int j = 0; j < childCount; j++) {
View view = getChildAt(j);
view.layout(j * view.getMeasuredWidth(), 0, (j + 1) * view.getMeasuredWidth(), view
.getMeasuredHeight());
}
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(childCount - 1).getRight();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
System.out.println("ev------->" + ev.getAction());
Boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mFirstX = ev.getRawX();
mFirstY = ev.getRawY();
mLastX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
mLastXIntercept = ev.getRawX();
mLastYIntercept = ev.getRawY();
float delX = Math.abs(mLastXIntercept - mFirstX);
float dexY = Math.abs(mLastYIntercept - mFirstY);
mFirstX = mLastXIntercept;
if (getScrollX() + leftBorder < getWidth()) {//此时为第一个fragment的页面
if (delX > dexY) {
intercept = true;//x滑动距离大于y时,此时拦截子控件的事件
} else {
intercept = false;
}
} else if (getScrollX() + leftBorder > getWidth() && getScrollX() + leftBorder <
2 * getWidth()) {//此时为第二个fragment的页面
intercept = false;
} else {//此时是第三个fragment的页面
if (delX > dexY) {
intercept = true;//x滑动距离大于y时,此时拦截子控件的事件
} else {
intercept = false;
}
}
/*
if (delX < mTouchSlop) {//如果滑动的最小距离小于toushSlop,那么不认为滑动,不拦截子控件的事件
intercept = false;
} else {
intercept = true;
}
*/
break;
}
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println("event------->" + event.getAction());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
mLastX = event.getRawX();
int delX = (int) (mFirstX - mLastX);
if (getScrollX() + delX < leftBorder) {
scrollTo(leftBorder, 0);
return true;
} else if (getScrollX() + getWidth() + delX > rightBorder) {
scrollTo(rightBorder - getWidth(), 0);
return true;
}
scrollBy(delX, 0);
mFirstX = mLastX;
break;
case MotionEvent.ACTION_UP:
int index = (getScrollX() + getWidth() / 2) / getWidth();
int dx = index * getWidth() - getScrollX();
mSroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mSroller.computeScrollOffset()) {
scrollTo(mSroller.getCurrX(), mSroller.getCurrY());
invalidate();
}
}
}
主要就是在onInterceptTouchEvent进行判断,当滑动的目标为第一个fragment时,此时fragment中只包含一个ListView,那么根据x、y滑动距离的比较来判定是上下滑动还是左右滑动;当滑动的目标为第二个fragment时,由于第二个fragment也包含一个类似ViewPager的ScrollView,因此就让子元素自己处理事件,父元素不拦截,至于第三个fragment,就只有一个TextView,因此也可以根据x、y滑动的比较来判定拦截条件。至于如何实现类似ViewPager的ScrollView,我就不详解了,自行百度或参照上面代码所写。
下面上子元素的ScrollView的实现,采用的是内部拦截法,主要是外部拦截法我不知道怎么来判定这个子元素的宽度,由于用了fragment,每次获取到的宽度就是手机的屏幕宽,因此从内部着手来判定宽度,方便告诉父元素何时拦截
/**
* Created by bellnett on 17/2/6.
*/
public class CustomBroadcastScrollView extends ViewGroup {
private float mFirstX = 0;//第一次触碰屏幕的x坐标
private float mLastX = 0;//滑动屏幕时的x坐标
private float mDispatchX = 0;//在dispatchTouchEvent中滑动屏幕时的x坐标
private int leftBorder;//界面可滚动的左边界
private int rightBorder;//界面可滚动的右边界
private Scroller mScroller;//用于完成滚动的实例
public CustomBroadcastScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
System.out.println("ev2------->" + ev.getAction());
getParent().getParent().requestDisallowInterceptTouchEvent(true);
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mFirstX = ev.getX();
break;
case MotionEvent.ACTION_MOVE:
mDispatchX = ev.getX();
int delX = (int) (mFirstX - mDispatchX);
if(getScrollX() + delX < leftBorder){//此时为滑动的第一张图片
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}else if(getScrollX() + getWidth() + delX > rightBorder){//此时已经滑动到最后一张图片
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println("event2------->" + event.getAction());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
mLastX = event.getX();
int delX = (int) (mFirstX - mLastX);
if(getScrollX() + delX < leftBorder){
scrollTo(leftBorder,0);
return true;
}else if(getScrollX() + getWidth() + delX > rightBorder){
scrollTo(rightBorder - getWidth(),0);
return true;
}
scrollBy(delX,0);
mFirstX = mLastX;
break;
case MotionEvent.ACTION_UP:
int index = (getScrollX() + getWidth() / 2) / getWidth();
int dx = index * getWidth() - getScrollX();
mScroller.startScroll(getScrollX(),0,dx,0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
measureChild(view, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
if (b) {
int childCount = getChildCount();
for (int j = 0; j < childCount; j++) {
View view = getChildAt(j);
view.layout(j * view.getMeasuredWidth(), 0, (j + 1) * view.getMeasuredWidth
(), view.getMeasuredHeight());
}
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(childCount - 1).getRight();
}
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
}
其子元素就是在dispatchTouchEvent中处理拦截事件,当滑动为第一张图片时,此时如果手指往右滑动,那么就告诉父元素拦截事件,父元素将从第二个fragment滑动到第一个fragment;当滑动为最后一张图片时,此时如果手指往左滑动,那么久告诉父元素拦截事件,父元素将从第二个fragment滑动到第三个fragment,这就是两种边界情况时的判定,其余情况就告诉父元素不要拦截事件,让子元素自己处理滑动事件。(这个视具体的业务需求来定,我这里举的例子是根据第一张和最后一张图片来判定)
第一个fragment布局
<?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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是第一个fragment"
android:textSize="20sp"/>
<ListView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/oneListView"
android:dividerHeight="2dp"
android:layout_weight="1">
</ListView>
</LinearLayout>
第二个fragment布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是第二个fragment"
android:textSize="20sp"/>
<com.bellnet.slidingconflict.CustomBroadcastScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/a"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/b"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/c"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/d"/>
</com.bellnet.slidingconflict.CustomBroadcastScrollView>
</LinearLayout>
第三个就不粘了,只有一个TextView
第一个fragment实现
/**
* Created by bellnett on 17/2/5.
*/
public class Fragment1 extends Fragment {
private ListView listView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one, container, false);
listView = (ListView) view.findViewById(R.id.oneListView);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String[] a = {"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"};
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(), android.R.layout
.simple_list_item_1, a);
listView.setAdapter(adapter);
}
}
第二个fragment实现
/**
* Created by bellnett on 17/2/5.
*/
public class Fragment2 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_two, container, false);
return view;
}
}
同样的,第三个fragment的实现和第二个fragment完全一样,只需要改个布局文件就可以了。到此所有的代码都粘贴出来了,如有不妥或者更好的处理方式,欢迎大家指出,下面看看具体的效果。由于上传图片有大小限制,我拆成了两张图。