Android View深入解析(三)滑动冲突与解决

编程入门 行业动态 更新时间:2024-10-24 20:14:36

Android View深入解析(三)滑动<a href=https://www.elefans.com/category/jswz/34/1770789.html style=冲突与解决"/>

Android View深入解析(三)滑动冲突与解决

Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller
Android View深入解析(二)事件分发机制
Android View深入解析(三)滑动冲突与解决

任玉刚老师 写的《Android开发艺术探索》是一本非常不错的进阶书籍,强烈推荐看看。也因为看完这本书,导致写博客的时候大概的思路有点跟着书的内容走了,也经常引用书中的内容和图片,在此谢过。如有侵权~你告我去啊。哈哈哈

开发中经常会遇到自定义控件的需求,因此滑动嵌套,滑动冲突也变得不可避免,那么这篇博文就来看看关于View的滑动冲突,以及解决办法。

前面了解View的基础知识,也认识了View的事件分发机制,现在终于要实践一下,把学习到的理论实战起来了~
常见的滑动冲突应该可以概括为以下3中情况

  1. 场景1 —– 外部滑动方向和内部方向不一致
  2. 场景2 —– 外部滑动方向和内部方向情况一致
  3. 场景3 —– 以上两种情况的嵌套

(图片摘自任玉刚老师)

场景2在开发中是很常见的比如说,外面一个ScrollView里面嵌套一个ListView,由于ScrollView跟ListView都是可以滑动的,所以当它们嵌套在一起使用的时候就会出现各种问题,ListView高度不能正确显示,滑动事件有问题等等

下面我们就自定义一个这样的控件,一个头部View,一个悬浮控件,一个ListView。看一下效果

看着这样的一个效果,先停下来想一想要怎么实现?为了让大家更详尽的了解自定义控件的思路,接下来一步一步来写这个效果。

个人认为,初学的知识点,不要上来就贴代码一脸懵逼的看,一头扎进代码中,连想要得到的效果都忘记了~~

分析:

  1. 实现内容滑动
  2. 内容滑动边界控制
  3. 解决ScrollView嵌套ListView,滑动冲突问题
  4. 实现布局悬停
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);}@Overridepublic 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.view.StickyLayout xmlns: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.view.StickyLayout>

使用自定义的布局控件,添加一个TextView方便查看拖动效果。在Activity中直接引用布局,运行看效果。

2. 内容滑动边界控制

我们看到这是一个非常粗糙的滑动效果,上下边界都没限制,可以无限制的上下滑动,这个肯定不行,得加上边界限制。根据ViewGroup内容大小限制能够滑动的最大最小距离。

修改后代码

public class StickyLayout extends LinearLayout {private String TAG = "StickyLayout";private int mLastY = 0;//内容Viewprivate 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);}/*** 布局加载完成*/@Overrideprotected void onFinishInflate() {super.onFinishInflate();mContentView = findViewById(R.id.id_content_view);}/*** 计算控件高度*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);mContentShowHeight = getMeasuredHeight();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mContentHeight = mContentView.getMeasuredHeight();}@Overridepublic 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方法,进行边界控制*/@Overridepublic 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();}/*** 布局加载完成*/@Overrideprotected 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);}/*** 计算控件高度*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);ViewGroup.LayoutParams params = mContent.getLayoutParams();params.height = getMeasuredHeight() - mSticky.getMeasuredHeight();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mTopViewHeight = mHeader.getMeasuredHeight();}@Overridepublic 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;mVelocityTrackerputeCurrentVelocity(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();}/*** 计算滑动*/@Overridepublic void computeScroll() {superputeScroll();if (mScrollputeScrollOffset()) {scrollTo(0, mScroll.getCurrY());postInvalidate();}}/*** 重写scrollTo方法,进行边界控制*/@Overridepublic 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;}/*** 事件拦截*/@Overridepublic 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;}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();mVelocityTracker.recycle();}}

关于 VelocityTrackerScroller 这里就不在过多解释了,如果还没有掌握查看第一篇博文
Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller

重点看看事件拦截的逻辑,根据开篇效果图,我们的需求是:

  1. 当头部View 没有隐藏的时候,直接拦截上下拖动
  2. 还有一点需要拦截,当头部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,但是代码里面完全没有悬停相关的操作,那么你看懂了吗?到底哪里实现了悬停?

其实这个悬停,采用了一个投机取巧的方式实现的

 @Overridepublic 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.view.StickyLayout xmlns:android=""xmlns:rtv=""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利息 分期付款" /><!--;<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.view.StickyLayout>

再瞄一眼Activity的代码

public class MainActivity extends AppCompatActivity {private ListView mListView;private List<String> mList;@Overrideprotected 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() {@Overridepublic 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深入解析(三)滑动冲突与解决

本文发布于:2024-03-04 16:16:57,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1709766.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:冲突   Android   View

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!