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);
    }

    @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();
    }

}

关于 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,但是代码里面完全没有悬停相关的操作,那么你看懂了吗?到底哪里实现了悬停?

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

 @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渐变色等等,,这些就留给看客们去拓展了

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值