主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
自定义ViewGroup的时候,你一定会遇到这种情况——滑动冲突。发生这种情况的前提是你的自定义ViewGroup支持滚动,并且可能也内嵌了一个可以支持滚动的控件。例如:ViewPager嵌套了一个ScrollView、ListView等。当然,用系统的控件是已经处理好这些滚动冲突的。也许你不曾有个这样的经历,接下来我们来看看一个例子,了解一下什么叫滑动冲突吧。
先来看看滑动冲突的效果:
滑动冲突原因分析:外面自定义的ViewGroup自身支持横向滑动,然后内部嵌套了ScrollView就不行了。当我们在ScrollView上左右滑动的时候根本没反应,而在上方的ImageView是不支持滑动的,所以不会造成冲突。
本例子沿用之前的一篇文章的部分代码,文章链接:结合scrollTo、Scroller、GestureDetector的使用方法,自定义ViewGroup打造ViewPager滑动效果
先看一张草图
Activity(分发) ---> ViewGroup(不拦截,分发) ---> ScrollView(消费事件)
Activity分发事件给外层的ViewGroup,ViewGroup不拦截继续分发给ScrollView。最后由ScrollView消费了onTouchEvent(),整个流程结束。那产生左右不滑动的原因就是:ScrollView消费了onTouchEvent(),导致ViewGroup接收不到onTouchEvent(),那么在ViewGroup的onTouchEvent()方法将得不到执行,所以左右无法响应滑动事件。
如果对事件分发不理解的可以看这篇文章:理解View的事件分发、拦截和消费,处理事件冲突的必备技能
既然有问题,我们就得解决问题。我们知道ViewGroup是有一个onInterceptTouchEvent()方法,这个方法的最重要的作用就是拦截当前事件。
思路:
在onInterceptTouchEvent()拦截点击事件,判断手指左右滑动距离 > 上下滑动距离,我们规定这种情况为左右滑动,将此事件拦截,那么ViewGroup的onTouchEvent()将得到执行。我们看一下实现此思路的代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = x;
firstY = y;
break;
case MotionEvent.ACTION_MOVE:
distanceX = Math.abs(x - firstX);
distanceY = Math.abs(y - firstY);
if (distanceX >= distanceY) {
isIntercept = true;
}else {
scrollIndex(currentIndex);
}
break;
case MotionEvent.ACTION_UP:
firstX = 0;
firstY = 0;
break;
}
return isIntercept;
}
首先,我们取得最初按下的点的x,y坐标,然后取得滑动后最末一点的x,y坐标做差,判断x,y滑动距离来确定是否是左右滑动还是上下滑动。
我们的完整的ViewGroup代码:
/**
* @Created by xww.
* @Creation time 2018/8/13.
*/
public class MyViewPager extends ViewGroup {
private int currentIndex;
private int startX;
private int endX;
private Scroller mScroller;
private int lastX;
private int distanceX, distanceY;
private int firstX, firstY;
int count = 0;
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
//对于每个子View进行布局
View childView = getChildAt(i);
childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
break;
case MotionEvent.ACTION_MOVE:
startX = firstX;
endX = x;
int dx = lastX - x;
scrollBy(dx, getScrollY());
lastX = x;
break;
case MotionEvent.ACTION_UP:
int tempIndex = currentIndex;
if (startX - endX > getWidth() / 2) { //从右往左滑动
tempIndex++;
} else if (endX - startX > getWidth() / 2) { //从左往右滑动
tempIndex--;
}
scrollIndex(tempIndex);
startX = 0;
endX = 0;
lastX = 0;
count = 0;
break;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = x;
firstY = y;
break;
case MotionEvent.ACTION_MOVE:
distanceX = Math.abs(x - firstX);
distanceY = Math.abs(y - firstY);
if (distanceX >= distanceY) {
isIntercept = true;
} else {
scrollIndex(currentIndex);
}
break;
case MotionEvent.ACTION_UP:
firstX = 0;
firstY = 0;
break;
}
return isIntercept;
}
/**
* 移动到指定页面
*/
private void scrollIndex(int tempIndex) {
//第一页,无法继续向左滑动
if (tempIndex < 0) {
tempIndex = 0;
}
//同理,最后一页无法向右滑动
if (tempIndex > getChildCount() - 1) {
tempIndex = getChildCount() - 1;
}
currentIndex = tempIndex;
mScroller.startScroll(getScrollX(), 0, currentIndex * getWidth() - getScrollX(), 0);
postInvalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
postInvalidate();
}
}
}
然后在MainActivity中添加的三个页面代码:
private int[] imgs = {R.drawable.bg_01, R.drawable.bg_02};
private void initViewPager() {
for (int img : imgs) {
AppCompatImageView imageView = new AppCompatImageView(getContext());
imageView.setBackgroundResource(img);
myViewpager.addView(imageView);
}
View view = LayoutInflater.from(getContext()).inflate(R.layout.fg_custom_viewpager_childview, null);
myViewpager.addView(view);
}
那么我们看看滑动冲突解决了没有,运行下项目,拭目以待吧。
确实是可以了,但是我们在ScrollView里左右滑动的时候有一个bug,我们看手指点下去一瞬间,上一个页面瞬间跳到了我们手指上,这确实是一个重大bug,这也是一个令我头疼的一个问题,不过我想到了一个解决的办法。那么首先,出问题了我们肯定先得找到问题出在哪里,既然是滑动页面,当然和scrollBy()脱不了关系。我就打印了scrollBy()的移动坐标,然后发现这样一种情况。
一、在ScrollView区域里左右滑动,我简单截取了部分Log
08-17 07:02:51.043 15756-15756/com.example.x.mycustomviews I/-------: onInterceptTouchEvent: ACTION_DOWN 596
08-17 07:02:51.304 15756-15756/com.example.x.mycustomviews I/-------: onInterceptTouchEvent: ACTION_MOVE
08-17 07:02:51.317 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE -603
08-17 07:02:51.334 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE -2
08-17 07:02:51.352 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE -1
08-17 07:02:51.800 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_UP
二、在ImageView区域里左右滑动,我也简单截取了部分Log
07:02:54.746 15756-15756/com.example.x.mycustomviews I/-------: onInterceptTouchEvent: ACTION_DOWN 473
08-17 07:02:54.746 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_DOWN
08-17 07:02:54.800 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE -2
08-17 07:02:54.867 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE -5
08-17 07:02:55.367 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE -5
08-17 07:02:55.544 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_UP
为了更加直观,我用颜色标记了两者不同之处,现在我们来分析一下出现这种情况的原因。
分析:
在ScrollView区域里左右滑动时,发现onTouchEvnet()事件居然没ACTION_DOWN,也就意味了我们标记不到onTouchEvnet()起始的x坐标,那么它将沿用onInterceptTouchEvent()保留下来的x坐标。所以,我们在滑动一瞬间才会看到瞬间移动的效果,这是因为scrollBy(dy,0);中的dy已经被onInterceptTouchEvent()的值替代,由于这个值可能非常大(手指位置决定),这将造成突然的瞬间移动到手指点击的那里。
解决方法:
我们修改onTouchEvnet()的ACTION_MOVE事件的部分代码,代码修改结果如下:
case MotionEvent.ACTION_MOVE:
startX = firstX;
endX = x;
if (count == 0) {
count++;
} else {
int dx = lastX - x;
scrollBy(dx, getScrollY());
}
lastX = x;
break;
首先定义一个count数值,这个做法是屏蔽掉第一次增大的效果,因为后面的移动都是正常的。那么,我们来看看效果如何?
经过测试,我们的代码已经实现了,不会出现瞬间移动的情况。当然,这是我个人的做法,其实我们还可以用GestureDetector(手势识别器),那么我们的代码将改为这样:
/**
* @Created by xww.
* @Creation time 2018/8/13.
*/
public class MyViewPager extends ViewGroup {
private int currentIndex;
private int startX;
private int endX;
private Scroller mScroller;
private int lastX;
private int distanceX, distanceY;
private int firstX, firstY;
private GestureDetector gestureDetector;
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
gestureDetector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
scrollBy((int) distanceX, getScrollY());
return true;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
//对于每个子View进行布局
View childView = getChildAt(i);
childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
gestureDetector.onTouchEvent(event);
int x = (int) event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
break;
case MotionEvent.ACTION_MOVE:
endX = x;
break;
case MotionEvent.ACTION_UP:
int tempIndex = currentIndex;
if (startX - endX > getWidth() / 2) { //从右往左滑动
tempIndex++;
} else if (endX - startX > getWidth() / 2) { //从左往右滑动
tempIndex--;
}
scrollIndex(tempIndex);
break;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
gestureDetector.onTouchEvent(ev);
boolean isIntercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = x;
firstY = y;
break;
case MotionEvent.ACTION_MOVE:
distanceX = Math.abs(x - firstX);
distanceY = Math.abs(y - firstY);
if (distanceX >= distanceY) {
isIntercept = true;
} else {
scrollIndex(currentIndex);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return isIntercept;
}
/**
* 移动到指定页面
*/
private void scrollIndex(int tempIndex) {
//第一页,无法继续向左滑动
if (tempIndex < 0) {
tempIndex = 0;
}
//同理,最后一页无法向右滑动
if (tempIndex > getChildCount() - 1) {
tempIndex = getChildCount() - 1;
}
currentIndex = tempIndex;
mScroller.startScroll(getScrollX(), 0, currentIndex * getWidth() - getScrollX(), 0);
postInvalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
postInvalidate();
}
}
}
那么,实现效果也是如出一辙,我们也看看它的效果吧