滑动冲突产生的原因:只要在界面中存在内外两层可以同时滑动,就会产生滑动冲突。
解决方案:根据实际情况,判断到底需要谁去响应滑动事件。
1.外部为左右滑动,内部为上下滑动
首先看已经解决过滑动冲突的情况,是可以很顺利的实现左右滑动和上下滑动的。
而没有处理滑动冲突的情况下,是不能够响应外部的左右滑动的。
这两种情况的根本区别在于左右滑动布局的onInterceptTouchEvent
方法,添加了判断逻辑是否需要进行拦截滑动事件。
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;
主要的判断逻辑是,水平方向滑动的距离大于竖直方向,则认为是水平滑动,需要拦截事件,否则认为是竖直滑动,不需要拦截事件,即交由listview
去处理。虽然任玉刚老师的git上有随书的代码,但是很多童鞋也是比较忙,没有时间去下载,我这里就附上用到的代码,其中自己添加了一些注释,也是便于我自己的学习。
自定义左右滑动的类,这里叫做MyViewPage.java
。
package com.tom.viewdemo.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* Title: MyViewPage
* Description:
*
* @author tom
* @date 2018/12/29 08:38
**/
public class MyViewPage extends ViewGroup {
public static final String TAG = "MyViewPage";
//子view的数量
private int mChildrenCount;
//子View的宽度
private int mChildWidth;
private int mChildIndex;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
//追踪屏幕触摸事件的速度
private VelocityTracker mVelocityTracker;
public MyViewPage(Context context) {
super(context);
init();
}
public MyViewPage(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyViewPage(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()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
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;
default:
break;
}
Log.d(TAG, "intercepted = " + intercepted);
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;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
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, mChildrenCount - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
@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);
//没有子View
if (childCount == 0) {
setMeasuredDimension(0, 0);
}
else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, measuredHeight);
}
else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
else {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, measuredHeight);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
//获取子 View 的数量
final int childCount = getChildCount();
mChildrenCount = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
//判断可见性不为GONE
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
这部分代码我也没有仔细的去研读,只是把和滑动冲突有关的看了下,后面学习到《View的工作原理》这一章,再好好过一遍代码。
布局文件activity_main.xml
布局文件content_layout.xml
这个文件就定义了一个title和一个listview,title用来显示是第几页,listview用来竖直方向的滑动。
布局文件content_list_item.xml
用于listview
每一行的展示。也可以使用系统自带的布局文件,只是一个demo,用什么都ok。
主文件MainActivity.java
这里使用了ButterKnife
插件,基本用法可以在仿QQ未读消息的动画效果中找到,也可以自行搜索。
package com.tom.viewdemo;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.tom.viewdemo.ui.MyViewPage;
import java.util.ArrayList;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class MainActivity extends AppCompatActivity {
Unbinder mBinder;
@BindView(R.id.container)
MyViewPage mContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBinder = ButterKnife.bind(this);
initViews();
}
private void initViews() {
LayoutInflater inflater = getLayoutInflater();
//获取屏幕的宽度,因为这两个值为固定值(同一设备),所以使用final关键字
final int screenWidth = getScreenMetrics(this).widthPixels;
//添加3页
for (int i = 0; i < 3; i++) {
ViewGroup layout;
layout = (ViewGroup) inflater.inflate(
R.layout.content_layout, mContainer, false);
//设置宽度为屏幕宽度
layout.getLayoutParams().width = screenWidth;
TextView textView = layout.findViewById(R.id.title);
textView.setText("第 " + (i + 1) + " 页");
layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
createList(layout);
mContainer.addView(layout);
}
}
/**
* 创建 listView
* @param layout 父布局
*/
private void createList(ViewGroup layout) {
ListView listView = layout.findViewById(R.id.list);
ArrayList datas = new ArrayList<>();
//填充测试数据
for (int i = 0; i < 50; i++) {
datas.add("name " + i);
}
ArrayAdapter 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();
}
});
}
/**
* 获取设备屏幕信息
* @param context
* @return
*/
private DisplayMetrics getScreenMetrics(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
return dm;
}
@Override
protected void onDestroy() {
super.onDestroy();
mBinder.unbind();
}
}
2.滑动方向一致,以竖直方向为例
这种情况的产生其实还是比较常见的,比如外部是一个竖直方向的scrollView,里面嵌套了一个listView等,处理的原则也是,根据实际需要,判断父容器是否需要拦截事件即可。
在布局文件中使用如下布局activity_demo.xml
这时的运行结果如图所示:
可以看到,并没有按照布局文件的预期结果,显示“头部”,并且listView
是无法滑动的。要让布局文件显示正确,只需要在Activity
中添加如下代码。
mScrollView.smoothScrollTo(0,0);
接下来处理滑动冲突,需求是点击的如果是listView
,则让listView
滑动。需要重写ScrollView
的onInterceptTouchEvent
方法。这里给出重写后的MyScrollView.java
文件。
package com.tom.viewdemo.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ListView;
import android.widget.ScrollView;
/**
* Title: MyScrollView
* Description:
*
* @author tom
* @date 2019/1/8 10:56
**/
public class MyScrollView extends ScrollView {
private ListView mListView;
public ListView getListView() {
return mListView;
}
public void setListView(ListView listView) {
mListView = listView;
}
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//如果点击的范围在listView内,则由listView来处理
if (mListView != null && checkArea(mListView,ev)) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 判断点击位置是否在view的范围内
* @param view
* @param ev
* @return
*/
private boolean checkArea(View view, MotionEvent ev) {
//获取点击位置的绝对坐标,相对屏幕左上角的位置
float x = ev.getRawX();
float y = ev.getRawY();
int[] locate = new int[2];
view.getLocationOnScreen(locate);
int left = locate[0];
int right = left + view.getWidth();
int top = locate[1];
int bottom = top + view.getHeight();
if (x > left && x < right && y > top && y < bottom) {
return true;
}
return false;
}
}
把布局文件的ScrollView
替换为MyScrollView
,同时在ScrollDemoActivity.java
文件中添加mScrollView.setListView(mList);
。
ScrollDemoActivity.java
文件如下:
package com.tom.viewdemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.tom.viewdemo.ui.MyScrollView;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class ScrollDemoActivity extends AppCompatActivity {
Unbinder mBinder;
@BindView(R.id.list)
ListView mList;
@BindView(R.id.scrollView)
MyScrollView mScrollView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
mBinder = ButterKnife.bind(this);
mScrollView.smoothScrollTo(0, 0);
mScrollView.setListView(mList);
initListData();
}
private void initListData() {
List dataList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
dataList.add("tom" + i);
}
ArrayAdapter arrayAdapter = new ArrayAdapter(ScrollDemoActivity.this, android.R.layout.simple_list_item_1,android.R.id.text1, dataList);
mList.setAdapter(arrayAdapter);
}
@Override
protected void onDestroy() {
super.onDestroy();
mBinder.unbind();
}
}
此时滑动冲突问题已经解决,效果图如下。
虽然已经解决了滑动冲突的问题,ListView
可以滑动,ScrollView
也可以滑动,但是当ListView
滑动到底部或者顶部时,我是希望ScrollView
可以接着滑动,因此进行如下修改。
滑动冲突解决优化改良版
重写listView
里的dispatchTouchEvent
方法,在移动时判断ListView
是否可以继续滑动(根据滑动方向判断),如果不能滑动,则由ScrollView
滑动。
MyListView.java
package com.tom.viewdemo.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;
/**
* Title: MyListView
* Description:
*
* @author tom
* @date 2019/1/8 16:14
**/
public class MyListView extends ListView {
private float mLastY;
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//拦截down事件,由 listView 来处理
getParent().getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
//向上滑动
if (mLastY > ev.getY()) {
//滑动到listView的底部了,手指无法继续向上滑动,即listView无法向下滑动,则交由父布局处理
if (!canScrollList(1)) {
getParent().getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
//向下滑动
else if (mLastY < ev.getY()) {
//滑动到listView的顶部了,手指无法继续向下滑动,即list View无法向上滑动,则交由父布局处理
if (!canScrollList(-1)) {
getParent().getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
break;
case MotionEvent.ACTION_UP:
getParent().getParent().requestDisallowInterceptTouchEvent(false);
break;
}
mLastY = ev.getY();
return super.dispatchTouchEvent(ev);
}
}
ListView
里的canScrollList()
方法用来判断是否能够继续滑动,可以查看源码里的注释。
/**
* Check if the items in the list can be scrolled in a certain direction.
*
* @param direction Negative to check scrolling up, positive to check
* scrolling down.
* @return true if the list can be scrolled in the specified direction,
* false otherwise.
* @see #scrollListBy(int)
*/
如果传入的参数为负,则检查listView
是否可以向上滑动;为正,检查listView
是否可以向下滑动。如果分不清楚向上还是向下,可以根据listview
的scrollBar
的运动方向来判断,或者理解为和手指的运动方向相反。
MyScrollView.java
package com.tom.viewdemo.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ListView;
import android.widget.ScrollView;
/**
* Title: MyScrollView
* Description:
*
* @author tom
* @date 2019/1/8 10:56
**/
public class MyScrollView extends ScrollView {
private ListView mListView;
public ListView getListView() {
return mListView;
}
public void setListView(ListView listView) {
mListView = listView;
}
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//如果点击的范围在listView内,则由listView来处理
// if (mListView != null && checkArea(mListView,ev)) {
// return false;
// }
//滑动冲突改良版
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onTouchEvent(ev);
return false;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 判断点击位置是否在view的范围内
* @param view
* @param ev
* @return
*/
private boolean checkArea(View view, MotionEvent ev) {
//获取点击位置的绝对坐标,相对屏幕左上角的位置
float x = ev.getRawX();
float y = ev.getRawY();
int[] locate = new int[2];
view.getLocationOnScreen(locate);
int left = locate[0];
int right = left + view.getWidth();
int top = locate[1];
int bottom = top + view.getHeight();
if (x > left && x < right && y > top && y < bottom) {
return true;
}
return false;
}
}
MotionEvent.ACTION_DOWN
必须要让给ListView
,这样ListView
才能判断自己是否还能滑动,因为ScrollView
的滑动需要在MotionEvent.ACTION_DOWN
时做一些准备,因此需要主动调用一次onTouchEvent()
方法。
ScrollDemoActivity.java
package com.tom.viewdemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ArrayAdapter;
import com.tom.viewdemo.ui.MyListView;
import com.tom.viewdemo.ui.MyScrollView;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class ScrollDemoActivity extends AppCompatActivity {
Unbinder mBinder;
@BindView(R.id.list)
MyListView mList;
@BindView(R.id.scrollView)
MyScrollView mScrollView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
mBinder = ButterKnife.bind(this);
mScrollView.smoothScrollTo(0, 0);
mScrollView.setListView(mList);
initListData();
}
private void initListData() {
List dataList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
dataList.add("tom" + i);
}
ArrayAdapter arrayAdapter = new ArrayAdapter(ScrollDemoActivity.this, android.R.layout.simple_list_item_1,android.R.id.text1, dataList);
mList.setAdapter(arrayAdapter);
}
@Override
protected void onDestroy() {
super.onDestroy();
mBinder.unbind();
}
}
布局文件activity_demo.xml
最后,附上改良版的效果图。
两种常见的滑动冲突已经解决,主要还是重写父布局的onInterceptTouchEvent()
方法,根据具体的业务,来判断需要由谁去处理事件。