Android View深入解析滑动冲突与解决
Posted Ruffian-痞子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View深入解析滑动冲突与解决相关的知识,希望对你有一定的参考价值。
Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller
Android View深入解析(二)事件分发机制
Android View深入解析(三)滑动冲突与解决
任玉刚老师 写的《android开发艺术探索》是一本非常不错的进阶书籍,强烈推荐看看。也因为看完这本书,导致写博客的时候大概的思路有点跟着书的内容走了,也经常引用书中的内容和图片,在此谢过。如有侵权~你告我去啊。哈哈哈
开发中经常会遇到自定义控件的需求,因此滑动嵌套,滑动冲突也变得不可避免,那么这篇博文就来看看关于View的滑动冲突,以及解决办法。
前面了解View的基础知识,也认识了View的事件分发机制,现在终于要实践一下,把学习到的理论实战起来了~
常见的滑动冲突应该可以概括为以下3中情况
- 场景1 —– 外部滑动方向和内部方向不一致
- 场景2 —– 外部滑动方向和内部方向情况一致
- 场景3 —– 以上两种情况的嵌套
(图片摘自任玉刚老师)
场景2在开发中是很常见的比如说,外面一个ScrollView里面嵌套一个ListView,由于ScrollView跟ListView都是可以滑动的,所以当它们嵌套在一起使用的时候就会出现各种问题,ListView高度不能正确显示,滑动事件有问题等等
下面我们就自定义一个这样的控件,一个头部View,一个悬浮控件,一个ListView。看一下效果
看着这样的一个效果,先停下来想一想要怎么实现?为了让大家更详尽的了解自定义控件的思路,接下来一步一步来写这个效果。
个人认为,初学的知识点,不要上来就贴代码一脸懵逼的看,一头扎进代码中,连想要得到的效果都忘记了~~
分析:
- 实现内容滑动
- 内容滑动边界控制
- 解决ScrollView嵌套ListView,滑动冲突问题
- 实现布局悬停
1. 实现内容滑动
自定义一个StickyLayout
public class StickyLayout extends LinearLayout
private String TAG = "StickyLayout";
private int mLastY = 0;
public StickyLayout(Context context)
this(context, null);
public StickyLayout(Context context, AttributeSet attrs)
super(context, attrs);
@Override
public boolean onTouchEvent(MotionEvent event)
int y = (int) event.getY();
switch (event.getAction())
case MotionEvent.ACTION_MOVE:
int dy = y - mLastY;
scrollBy(0, -dy);
Log.e(TAG, " deltaY=" + dy + " mLastY=" + mLastY);
break;
mLastY = y;
return true;
代码超级简单,直接继承LinearLayout
,重写 onTouch
方法的 ACTION_MOVE
事件,实现ViewGroup内容(TextView)跟随触摸滑动。
xml布局
<?xml version="1.0" encoding="utf-8"?>
<com.ruffian.cn.view.StickyLayout 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/id_content_view"
android:layout_width="match_parent"
android:layout_height="700dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@android:color/white" />
</com.ruffian.cn.view.StickyLayout>
使用自定义的布局控件,添加一个TextView方便查看拖动效果。在Activity中直接引用布局,运行看效果。
2. 内容滑动边界控制
我们看到这是一个非常粗糙的滑动效果,上下边界都没限制,可以无限制的上下滑动,这个肯定不行,得加上边界限制。根据ViewGroup内容大小限制能够滑动的最大最小距离。
修改后代码
public class StickyLayout extends LinearLayout
private String TAG = "StickyLayout";
private int mLastY = 0;
//内容View
private View mContentView;
//内容View的高度
private int mContentHeight = 0;
//内容View可见高度
private int mContentShowHeight = 0;
public StickyLayout(Context context)
this(context, null);
public StickyLayout(Context context, AttributeSet attrs)
super(context, attrs);
/**
* 布局加载完成
*/
@Override
protected void onFinishInflate()
super.onFinishInflate();
mContentView = findViewById(R.id.id_content_view);
/**
* 计算控件高度
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mContentShowHeight = getMeasuredHeight();
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
super.onSizeChanged(w, h, oldw, oldh);
mContentHeight = mContentView.getMeasuredHeight();
@Override
public boolean onTouchEvent(MotionEvent event)
int y = (int) event.getY();
switch (event.getAction())
case MotionEvent.ACTION_MOVE:
int dy = y - mLastY;
scrollBy(0, -dy);
Log.e(TAG, " deltaY=" + dy + " mLastY=" + mLastY);
break;
mLastY = y;
return true;
/**
* 重写scrollTo方法,进行边界控制
*/
@Override
public void scrollTo(int x, int y)
if (y < 0)
y = 0;
if (y > mContentHeight - mContentShowHeight)
y = mContentHeight - mContentShowHeight;
if (y != getScrollY())
super.scrollTo(x, y);
这里我们重写 scrollTo
控制 y 的最小值为 0;最大值是内容高度 mContentHeight
- mContentShowHeight
在函数 onFinishInflate()
中获取内容View
在函数onMeasure
中获取内容View的实际高度 mContentHeight
在函数 onSizeChanged
中获取获取内容View可见高度 mContentShowHeight
y 可以滚动的 范围 0
-> mContentHeight - mContentShowHeight
y 最大可以滚动的值:(假如)内容View的高度假如有 1000px ,内容View的可见高度有700px ,那么只需要滚动 y = 1000 - 700 = 300 就可以滚动到底。
OK,逻辑也是很简单,重新看一下运行效果
从效果上看已经达到了边界的控制
3. 解决ScrollView嵌套ListView,滑动冲突问题
接着向着开篇的效果进发,添加ListView,制造滑动冲突,并解决
StickyLayout
完整代码
public class StickyLayout extends LinearLayout
private String TAG = "StickyLayout";
private int mLastY = 0;
private View mHeader;
private View mContent;
private View mSticky;
private int mTouchSlop;
//头部View是否隐藏
boolean isTopHidden = false;
private boolean mDragging = false;
private Scroller mScroll;
private VelocityTracker mVelocityTracker;
private int mTopViewHeight;
public StickyLayout(Context context)
this(context, null);
public StickyLayout(Context context, AttributeSet attrs)
super(context, attrs);
init(context);
private void init(Context context)
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mScroll = new Scroller(context);
mVelocityTracker = VelocityTracker.obtain();
/**
* 布局加载完成
*/
@Override
protected void onFinishInflate()
super.onFinishInflate();
mHeader = findViewById(R.id.id_header_view);
mContent = findViewById(R.id.id_content_view);
mSticky = findViewById(R.id.id_sticky_view);
/**
* 计算控件高度
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams params = mContent.getLayoutParams();
params.height = getMeasuredHeight() - mSticky.getMeasuredHeight();
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
super.onSizeChanged(w, h, oldw, oldh);
mTopViewHeight = mHeader.getMeasuredHeight();
@Override
public boolean onTouchEvent(MotionEvent event)
mVelocityTracker.addMovement(event);
int y = (int) event.getY();
switch (event.getAction())
case MotionEvent.ACTION_DOWN:
if (!mScroll.isFinished())
mScroll.abortAnimation();
break;
case MotionEvent.ACTION_CANCEL:
mDragging = false;
if (!mScroll.isFinished())
mScroll.abortAnimation();
break;
case MotionEvent.ACTION_MOVE:
int dy = y - mLastY;
if (!mDragging && Math.abs(dy) > mTouchSlop)
mDragging = true;
if (mDragging)
scrollBy(0, -dy);
Log.e(TAG, " deltaY=" + dy + " mLastY=" + mLastY);
break;
case MotionEvent.ACTION_UP:
mDragging = false;
mVelocityTracker.computeCurrentVelocity(1000);
int yVelocity = (int) mVelocityTracker.getYVelocity();
fling(-yVelocity);
mVelocityTracker.clear();
break;
mLastY = y;
return true;
/**
* 滑动
*
* @param dy
*/
private void fling(int dy)
mScroll.fling(0, getScrollY(), 0, dy, 0, 0, 0, mTopViewHeight);
invalidate();
/**
* 计算滑动
*/
@Override
public void computeScroll()
super.computeScroll();
if (mScroll.computeScrollOffset())
scrollTo(0, mScroll.getCurrY());
postInvalidate();
/**
* 重写scrollTo方法,进行边界控制
*/
@Override
public void scrollTo(int x, int y)
if (y < 0)
y = 0;
if (y > mTopViewHeight)
y = mTopViewHeight;
if (y != getScrollY())
super.scrollTo(x, y);
isTopHidden = getScrollY() == mTopViewHeight;
/**
* 事件拦截
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
boolean intercept = false;
int y = (int) ev.getY();
switch (ev.getAction())
case MotionEvent.ACTION_MOVE:
/**
* 控制权交换逻辑
* 1.头部view 没有隐藏 本身控制
* 2.头部view 隐藏 子view滚动到最顶部再往下滑动 : 本身控制
*/
int dy = y - mLastY;
ListView lv = (ListView) mContent;
View c = lv.getChildAt(lv.getFirstVisiblePosition());
if (!isTopHidden || (c != null && c.getTop() == 0 && isTopHidden && dy > 0))
intercept = true;
break;
mLastY = y;
return intercept;
@Override
protected void onDetachedFromWindow()
super.onDetachedFromWindow();
mVelocityTracker.recycle();
关于 VelocityTracker
,Scroller
这里就不在过多解释了,如果还没有掌握查看第一篇博文
Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller
重点看看事件拦截的逻辑,根据开篇效果图,我们的需求是:
- 当头部View 没有隐藏的时候,直接拦截上下拖动
- 还有一点需要拦截,当头部View完全隐藏了,此时ListView本身滑动,当ListView滑到顶部再往下拉的时候,头部View需要可以滑下来
这么分析完后,再看一下onInterceptTouchEvent
方法,逻辑超级简单,一目了然
public boolean onInterceptTouchEvent(MotionEvent ev)
boolean intercept = false;
int y = (int) ev.getY();
switch (ev.getAction())
case MotionEvent.ACTION_MOVE:
/**
* 控制权交换逻辑
* 1.头部view 没有隐藏 本身控制
* 2.头部view 隐藏 子view滚动到最顶部再往下滑动 : 本身控制
*/
int dy = y - mLastY;
ListView lv = (ListView) mContent;
View c = lv.getChildAt(lv.getFirstVisiblePosition());
if (!isTopHidden || (c != null && c.getTop() == 0 && isTopHidden && dy > 0))
intercept = true;
break;
mLastY = y;
return intercept;
这里有个需要注意的地方,c.getTop() == 0
是ListView是否滚动到顶部的一种判断方法,也可以用其他方式实现,这里拓展一下,如果ListView换成其他的View,例如 RecycleView
那么此处逻辑需要更改为 RecycleView
滚动到顶部的逻辑判断。不是重点,这里不再深入。
其他代码主要是判断头部View是否隐藏,View内容是否正在滚动的一些辅助判断,都很简单就不再一一分析。有木有发现,滑动冲突原来这么简单的解决了?所以说,原理要先理解,代码写起来就很顺畅啦。
回过神来问看客一个问题,这个控件叫 StickyLayout
确实也存在悬浮的View,但是代码里面完全没有悬停相关的操作,那么你看懂了吗?到底哪里实现了悬停?
其实这个悬停,采用了一个投机取巧的方式实现的
@Override
public void scrollTo(int x, int y)
if (y < 0)
y = 0;
if (y > mTopViewHeight)
y = mTopViewHeight;
if (y != getScrollY())
super.scrollTo(x, y);
isTopHidden = getScrollY() == mTopViewHeight;
这里限制了 y 大滑动的距离是头部View的高度,而悬停View紧接着头部View,所以当头部View完全隐藏的时候,悬停的View刚好停在顶部,接着事件交给ListView,ListView本身滚动,因此造成了一个View悬停的效果,怎么样?Get到这个没有?是不是很神奇很有意思呢?好好体会一下
看看xml文件代码
<?xml version="1.0" encoding="utf-8"?>
<com.ruffian.cn.view.StickyLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:rtv="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/id_header_view"
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="centerCrop"
android:src="@mipmap/icon_header" />
<RelativeLayout
android:id="@+id/id_sticky_view"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/colorPrimary">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="16dp"
android:text="0首付 0利息 分期付款" />
<!--https://github.com/RuffianZhong/RTextView-->
<com.ruffian.library.RTextView
android:layout_width="70dp"
android:layout_height="28dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="16dp"
android:gravity="center"
android:text="立即抢购"
android:textColor="@android:color/white"
rtv:background_normal="@color/colorAccent"
rtv:corner_radius="5dp" />
</RelativeLayout>
<ListView
android:id="@+id/id_content_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff4f7f9"
android:cacheColorHint="#00000000"
android:divider="#dddbdb"
android:dividerHeight="1.0px"
android:listSelector="@android:color/transparent" />
</com.ruffian.cn.view.StickyLayout>
再瞄一眼Activity的代码
public class MainActivity extends AppCompatActivity
private ListView mListView;
private List<String> mList;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
private void initView()
mListView = (ListView) findViewById(R.id.id_content_view);
mList = new ArrayList<>();
for (int i = 0; i < 20; i++)
mList.add("iPhone " + (i + 1));
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, mList);
mListView.setAdapter(adapter);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener()
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
Toast.makeText(MainActivity.this, mList.get(position), Toast.LENGTH_SHORT).show();
);
再看一眼效果,嗯,,还不错,美美的,实际开发中不少能用上这个效果,其实在这基础上可以拓展一些下拉头部View图片缩放的效果,以及上滑过程中ActionBar渐变色等等,,这些就留给看客们去拓展了
以上是关于Android View深入解析滑动冲突与解决的主要内容,如果未能解决你的问题,请参考以下文章
Android View深入解析基础知识VelocityTracker,GestureDetector,Scroller