文档章节

自定义View----一个Demo带你彻底掌握View的滑动冲突(五)

KingBoxing123
 KingBoxing123
发布于 2017/04/06 11:20
字数 2760
阅读 291
收藏 27

先上图:

这里写图片描述

示例图中是一个常见的下拉回弹,手指向下滑动的时候,整个布局会一起滑动。下拉到一定距离的时候松手,布局会自动回弹到开始的位置;手指向上滑动的时候,布局的子View会滑动到最底部,然后手指再向下滑动,布局的子View会滑动到最顶部,最后手指继续向下滑动,整个布局会一起滑动,下拉到一定距离后松手自动回弹到开始位置。

最终实现的效果如上所示,一起看看怎样一步步实现最终的效果:

一.布局的下拉回弹实现

下拉回弹的实现本质其实就是View的滑动,目前Android中实现View的滑动可以分为三种方式:通过改变View的布局参数使得View重新布局从而实现滑动;通过scrollTo/scrollBy方法来实现View的滑动;通过动画给View施加平移效果来实现滑动。这里我们采用第一种方式来实现,考虑到整个布局是竖直排列,我们可以直接自定义一个LinearLayout来作为父布局。然后调用layout(int l, int t, int r, int b)方法重新布局,达到滑动的效果。

public class MyParentView extends LinearLayout {

    private int mMove;
    private int yDown, yMove;
    private int i = 0;


    public MyParentView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if ((yMove - yDown) > 0) {
                    mMove = yMove - yDown;
                    i += mMove;
                    layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
                }
                break;
            case MotionEvent.ACTION_UP:
                layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
                i = 0;
                break;
        }
        return true;
    }
}

MotionEvent.ACTION_DOWN: 获取刚开始触碰的y坐标 
MotionEvent.ACTION_MOVE: 如果是向下滑动,计算出每次滑动的距离与滑动的总距离,将每次滑动的距离作为layout(int l, int t, int r, int b)方法的参数,重新进行布局,达到布局滑动的效果。 
MotionEvent.ACTION_UP: 将滑动的总距离作为layout(int l, int t, int r, int b)方法的参数,重新进行布局,达到布局自动回弹的效果。

此时的布局文件是这样的:

    <org.tyk.android.artstudy.MyParentView
        android:id="@+id/parent_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="@color/divider"></View>

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="70dp">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerVertical="true"
                        android:layout_marginLeft="10dp"
                        android:background="@drawable/b" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerVertical="true"
                        android:layout_marginLeft="80dp"
                        android:text="回到首页"
                        android:textSize="20sp" />

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_alignParentRight="true"
                        android:layout_centerVertical="true"
                        android:layout_marginRight="10dp"
                       android:background="@drawable/right_arrow" />
                </RelativeLayout>
    </org.tyk.android.artstudy.MyParentView>

中间重复的RelativeLayout就不贴出来了。至此,一个简单的下拉回弹就已经实现了,关于快速滑动以及惯性滑动感兴趣的可以加进去,这里不是本篇博客的重点就不做讨论了。

二.子View的滚动实现

手指向下滑动的时候,布局的下拉回弹已经实现,现在我希望手指向上滑动的时候,布局的子View能够滚动。平时接触最多的能滚动的View就是ScrollView,所以我的第一反应就是在自定义的LinearLayout内,添加一个ScrollView,让子View能够滚动。说干就干:

 <org.tyk.android.artstudy.MyParentView
        android:id="@+id/parent_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">
            </LinearLayout>
        </ScrollView>
 </org.tyk.android.artstudy.MyParentView>

兴高采烈的加上去,最后运行的结果是:布局完全变成了一个ScrollView,之前的下拉回弹效果已经完全消失!!!这显然不是我期待的结果。

仔细分析一下这种现象,其实这就是常见的View滑动冲突场景之一:外部滑动方向与内部滑动方向一致。父布局MyParentView需要响应竖直方向上的向下滑动,实现下拉回弹,子布局ScrollView也需要响应竖直方向上的上下滑动,实现子View的滚动。当内外两层都在同一个方向上可以滑动的时候,就会出现逻辑问题。因为当手指滑动的时候,系统无法知道用户想让哪一层滑动。所以这种场景下的滑动冲突需要我们手动去解决。

解决办法: 
外部拦截法:外部拦截法是指点击事件先经过父容器的拦截处理,如果父容器需要处理此事件就进行拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。外部拦截法需要重写父容器的onInterceptTouchEvent()方法,在内部做相应的拦截即可。

具体实现:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if (yMove - yDown < 0) {
                    isIntercept = false;
                } else if (yMove - yDown > 0) {
                    isIntercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return isIntercept;
    }

实现分析: 
在自定义的父布局中重写onInterceptTouchEvent()方法,MotionEvent.ACTION_MOVE的时候,进行判断。如果手指是向上滑动,onInterceptTouchEvent()返回false,表示父布局不拦截当前事件,当前事件交给子View处理,那么我们的子View就能滚动;如果手指是向下滑动,onInterceptTouchEvent()返回true,表示父布局拦截当前事件,当前事件交给父布局处理,那么我们父布局就能实现下拉回弹。

三.连续滑动的实现

刚开始我以为这样就万事大吉了,可后来我又发现一个很严重的问题:手指向上滑动的时候,子View开始滚动,然后手指再向下滑动,整个父布局开始向下滑动,松手后便自动回弹。也就是说,刚才滚动的子View已经回不到开始的位置。仔细分析一下其实这结果是意料之中的,因为只要我手指是向下滑动,onInterceptTouchEvent()便返回true,父布局会拦截当前事件。这里其实又是上面提到的View滑动冲突:理想的结果是当子View滚动后,如果子View没有滚动到开始的位置,父布局就不要拦截滑动事件;如果子View已经滚动到开始的位置,父布局就开始拦截滑动事件。

解决办法: 
内部拦截法:内部拦截法是指点击事件先经过子View处理,如果子View需要此事件就直接消耗掉,否则就交给父容器进行处理,这样就可以解决滑动冲突的问题。内部拦截法需要配合requestDisallowInterceptTouchEvent()方法,来确定子View是否允许父布局拦截事件。

具体实现:

public class MyScrollView extends ScrollView {


    public MyScrollView(Context context) {
        this(context, null);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:

                int scrollY = getScrollY();
                if (scrollY == 0) {
                    //允许父View进行事件拦截
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    //禁止父View进行事件拦截
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
        }
        return super.onTouchEvent(ev);

    }
}

实现分析: 
自定义一个ScrollView,重写onTouchEvent()方法,在MotionEvent.ACTION_MOVE的时候,得到滑动的距离。如果滑动的距离为0,表示子View已经滚动到开始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(false)方法,允许父View进行事件拦截;如果滑动的距离不为0,表示子View没有滚动到开始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(true)方法,禁止父View进行事件拦截。这样只要子View没有滚动到开始的位置,父布局都不会拦截事件,一旦子View滚动到开始的位置,父布局就开始拦截事件,形成连续的滑动。

好了,针对其他场景更复杂的滑动冲突,解决滑动冲突的原理与方式无非就是这两种方法。希望看完本篇博客能对你有所帮助,下一篇再见~~~

写在最后:

昨天一直忙到下午才有时间去看博客,看到这篇博客评论下面炸开了锅。这里有几个问题说明一下:

关于Denon源码的问题,因为这个Demo的源码不是单独的,合集打包下来有30多M,所以当时就没传上去。我相信按照文章所说的步骤来,肯定会实现最后的效果,最后我上传的源码与文章代码是一模一样的,这一点我是百分百保证的。

关于Demo存在的问题,这个问题是真实存在的:

这里写图片描述

谢谢这位小伙伴,我当时也立即回复了他,今天我把这个问题解决了。

public class MyScrollView extends ScrollView {


    private scrollTopListener listener;

    public void setListener(scrollTopListener listener) {
        this.listener = listener;
    }

    public MyScrollView(Context context) {
        this(context, null);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {


            case MotionEvent.ACTION_MOVE:

                int scrollY = getScrollY();
                if (scrollY == 0) {
                    //允许父View进行事件拦截
                    getParent().requestDisallowInterceptTouchEvent(false);
                    listener.scrollTop();
                } else {
                    //禁止父View进行事件拦截
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
        }
        return super.onTouchEvent(ev);

    }


    public interface scrollTopListener {
        void scrollTop();
    }


}

给自定义的ScrollView添加一个接口,监听是否滑到开始的位置。

public class MyParentView extends LinearLayout {

    private int mMove;
    private int yDown, yMove;
    private boolean isIntercept;
    private int i = 0;
    private MyScrollView myScrollView;
    private boolean isOnTop;


    public MyParentView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        myScrollView = (MyScrollView) getChildAt(0);
        myScrollView.setListener(new MyScrollView.scrollTopListener() {
            @Override
            public void scrollTop() {
                isOnTop = true;
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                //上滑
                if (yMove - yDown < 0) {
                    isIntercept = false;
                    //下滑
                } else if (yMove - yDown > 0) {
                    isIntercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
        return isIntercept;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if (isOnTop) {
                    yDown = y;
                    isOnTop = false;
                }
                if (isIntercept && (yMove - yDown) > 0) {
                    mMove = yMove - yDown;
                    i += mMove;
                    layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
                }
                break;
            case MotionEvent.ACTION_UP:
                layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
                i = 0;
                isIntercept = false;
                break;
        }

        return true;
    }


}

自定义的父布局中,实现这个接口,然后在MotionEvent.ACTION_MOVE的时候,进行判断:

if (isOnTop) { 
yDown = y; 
isOnTop = false; 
}

如果滑动到顶部,就让yDown的初始值为(int) event.getY(),这样就不会出现闪的问题,滑动也更加自然流畅。

关于Demo的优化与改进,我很感谢这位小伙伴:

这里写图片描述

他用不同的方式实现了一样的效果,并且还把源码发到了我的邮箱。实现的效果一模一样,并且只用了自定义的父布局加外部拦截法,贴一下代码:

public class MyParentView extends LinearLayout {

    private int mMove;
    private int yDown, yMove;
    private int i = 0;
    private boolean isIntercept = false;

    public MyParentView(Context context) {
        super(context);
    }

    public MyParentView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyParentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private ScrollView scrollView;

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        scrollView = (ScrollView) getChildAt(0);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        onInterceptTouchEvent(ev);
        return super.dispatchTouchEvent(ev);
    }

       @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int y = (int) ev.getY();
        int mScrollY = scrollView.getScrollY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                if (yMove - yDown > 0 && mScrollY == 0) {
                    if (!isIntercept) {
                        yDown = (int) ev.getY();
                        isIntercept = true;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
                i = 0;
                isIntercept = false;
                break;
        }
        if (isIntercept) {
            mMove = yMove - yDown;
            i += mMove;
            layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
        }
        return isIntercept;
    }
}

这样就不用自定义一个ScrollView,直接将原生的ScrollView放到这个父布局中即可。

源码地址:

https://github.com/18722527635/AndroidArtStudy

© 著作权归作者所有

共有 人打赏支持
KingBoxing123
粉丝 5
博文 95
码字总数 48684
作品 0
成都
私信 提问
加载中

评论(2)

x
xdli
很不错
凝小紫
凝小紫
可以将项目托管到码云上哦~~
自定义view控件效果实现及实践

项目需求讨论 - Android 自定义 Dialog 实现步骤及封装 根据实际项目需求出发。因为项目中的对话框要配合整个项目的 UI 风格,所以进行自定义 Dialog 的实现步骤,及最后写封装类。来快速实现...

掘金官方
2017/12/11
0
0
Android事件分发机制之源码完全解析(上)

学事件分发是为了什么呢?还不是为了解决滑动冲突的。 实际上,如果仅仅是为了解决滑动冲突的,大可不必看源码,只需要掌握事件分发的外在规律即可。 只要记住这张图,再明白内部拦截法和外部...

qq_36523667
2018/02/03
0
0
2017 我所分享的技术文章总结(下)

> 对下半年所分享的文章进行整理,上半年总结的 98 篇好文请点击这里,很多读者当时忘记了收藏,以致于查找一篇历史文章很费劲,因此在这里顺便做下记录。目前就分下下面几个大类,没有更多细...

你未读
2018/01/01
0
0
【Android】掌握自定义LayoutManager(二) 实现流式布局

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 转载请标明出处: http://blog.csdn.net/zxt0601/article/details/52956504 本文出自:【张旭童的博客】 本系列文章相关代码传送门:...

zxt0601
2016/10/28
0
0
第七章 View的滑动冲突

1. 概述   在进行Android开发的时候,都可能会碰到滑动冲突。有时候,下载了一个Demo运行的好好的,但是出现滑动冲突,这样导致Demo运行崩溃。然后,整个人都炸了。下面来分析下滑动冲突是...

忆念成风
2017/11/14
0
0

没有更多内容

加载失败,请刷新页面

加载更多

欧拉公式

欧拉公式表达式 欧拉公式的几何意 cosθ + j sinθ 是个复数,实数部分也就是实部为 cosθ ,虚数部分也就是虚部为 j sinθ ,对应复平面单位圆上的一个点。 根据欧拉公式和这个点可以用 复指...

sharelocked
今天
2
0
burpsuite无法抓取https数据包

1.将浏览器和burpsuite的代理都设置好 2.在浏览器地址栏输入: http://burp 3.下载下面的证书,并将证书导入浏览器 cacert.der

Frost729
今天
2
0
JeeSite4.x 消息管理、消息推送、消息提醒

实现统一的消息推送接口,包含PC消息、短信消息、邮件消息、微信消息等,无需让所有开发者了解消息是怎么发送出去的,只需了解消息发送接口即可。 所有推送消息均通过 MsgPushUtils 工具类发...

ThinkGem
今天
7
0
OpenML

https://www.openml.org/search?type=data

shengjuntu
今天
2
0
java强引用,软引用,弱引用和虚引用

先来简要说一下这四种引用的特性: 强引用:如果一个对象具有强引用,那垃圾回收器绝不会回收它 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它 弱引用:在垃圾...

woshixin
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部