Android回弹效果新思考与更加易用的实现

本文探讨如何在Android中实现iOS风格的回弹效果,避免修改现有控件的事件分发逻辑。作者提出了一种新思路,利用自定义布局继承自`FrameLayout`,通过重写相关方法来判断子视图的滑动可能性,从而实现回弹。文章详细介绍了实现过程中遇到的问题和解决方案,包括如何处理事件连续性、定义不同状态以及兼容性问题。
摘要由CSDN通过智能技术生成

BounceEffect

前言

最近app需要在首页上做一个类似iOS的回弹效果, 我们的首页是一个ExpandableListView, 如果要做到类似iOS的回弹效果, 最先想到的思路就是使用额外添加的Header和Footer配合改写事件分发机制实现. 众所周知, 这种做法非常的不通用, 下次一个页面用ListView, 至少需要把代码复制一遍, 如果是ScrollView, 则要重写一部分逻辑. 如果是LinearLayout, 还得用写好的ScrollView把它包起来. 项目里面有一些地方的listview自身已经有了一个header和一个footer了, 这样还会带来更多的逻辑上的麻烦.
我在想, 有没有一种简单的方法, 让我能不需要改写现有的控件的代码, 尤其是不改事件分发这种复杂的逻辑, 直接实现回弹效果. 于是有了今天这篇博文.
这个控件本质上是一个FrameLayout, 只需要套在最外层的view上就可以有回弹效果了, 支持所有的布局.

新思路

前段时间调查一个ViewPager无法滑动的问题时(原文), 偶然发现Support v4包里有一个叫ViewCompat.canScrollHorizontally的方法, ViewPager遍历子view并调用这个方法来检查可滑动性, 来解决可滑动控件嵌套的问题的.
对应的, 还有一个ViewCompat.canScrollVertically, 在回弹效果中, 不能简单的在外层套一个布局实现的问题在于, 外部不知道内部的滑动情况, 如果有这个方法, 一切都好办了.
但是需要注意的是, 这个方法实际上是调用View.canScrollVertically, 该方法是API 16才加入的, Support包并没有让他兼容到16以下, 所以目前并没有4.0以下的兼容方案, 只能做保守性兼容.
我们的思路就是, 自定义一个布局BounceFrameLayout, 就继承FrameLayout, 只能有一个子view, 每次有滑动出现, 我们首先询问子view是否能在对应方向上滑动, 如果不行, 那么我们通过View.scrollBy的方法进行view的偏移, 如果能滑动, 就直接按正常流程分发事件即可.

具体细节

这个思路说起来容易, 做起来还是要考虑很多的. 下面一个一个来探讨.

是否可以滑动

是否可以滑动可以通过ViewCompat.canScrollVertically判断, 但是这个方法只是对单个view进行判断, 假如BounceFrameLayout内部先有一个LinearLayout, LinearLayout内部才有一个ListView, 那么这个方法报告的可滑动性永远为false.
再考虑一种情况, 子view里面只有一部分是ListView, 这个时候, 能不能滑动还要看MotionEvent.ACTION_DOWN事件的坐标落在什么区域上.
所以我们在判断是否可以滑动之前, 首先需要获取手指点击的地方的一个可滑动view, 而且是一层一层往下找, 直到找到, 或者找完了都没有找到为止.

    protected View findScrollableTopChildUnder(View view, int x, int y) {
        if (view != null) {
            if (ScrollHelper.canScrollVertically(view, -1) || ScrollHelper.canScrollVertically(view, 1)) {
                return view;
            } else if (view instanceof ViewGroup) {
                final ViewGroup group = (ViewGroup) view;
                final int scrollx = group.getScrollX();
                final int scrolly = group.getScrollY();
                final int childCount = group.getChildCount();
                for (int i = childCount - 1; i >= 0; i--) {
                    final View child = group.getChildAt(i);
                    if (scrollx + x >= child.getLeft() && scrollx + x < child.getRight() &&
                            scrolly + y >= child.getTop() && scrolly + y < child.getBottom()) {
                        return findScrollableTopChildUnder(child, x + scrollx - child.getLeft(),
                                y + scrolly - child.getTop());
                    }
                }
            }
        }
        return null;
    }

ScrollHelper.canScrollVertically是我将ViewCompat中的代码抽出来写的类, 主要是改进了一些方法, 防止系统的bug影响我们的判断, 这个在最后说.
这个方法的功能就是不断的遍历子view, 直到找到第一个可在竖直方向上滑动的view为止, 如果没有返回null. 里面还考虑到了父view的scrollY导致的坐标计算的变化.
我们捕获到这个view之后, 在一次触摸事件的过程里面, 这个view就不需要变了, 之后需要询问是否可以滑动时, 就调用下面的方法.

    protected boolean canScroll(View v, int dx, int dy) {
        if (v == null) {
            return false;
        }
        return ScrollHelper.canScrollVertically(v, -dy);
    }

重写哪个方法

一般来讲, 我们需要重写的最多三个方法, 这里考虑到我们要做的是一个类似旁路监听的逻辑, 而且有些时候滑动需要我们自己处理, 有些时候需要子view处理, 所以我们要重写的是dispatchTouchEvent.

事件如何连续

考虑下面的场景, 用户首先手指下滑, 我们的检测到listview无法滑动, 将view下移做overscroll效果, 然后用户不松手, 转上滑, 我们将view移回原位, 用户继续上滑, 此时listview处理事件.
众所周知这其实是违反Android的事件分发逻辑的, 因为一旦一个事件被一个view处理, 那么之后所有的事件都会交给它处理, 如果它处理到一半又不想处理了, 那么这个事件是无法转交给其他view的, 而且如果父view一旦决定拦截事件, 那么这个事件也无法再次下穿, 只有等待下一次事件过程.
这也是我们要重写dispatchTouchEvent的原因, 我们需要一个全程都能收到事件的方法, 针对事件不连续的问题, 我们采取的策略是事件欺骗. 也就是说当我发现我不能继续自己处理事件时, 我将本次event的action改为MotionEvent.ACTION_DOWN后分发下去, 让子view重新开启事件处理流程, 后面的move事件照常分发, 如果我发现需要我来处理事件了, 我就将本次event的action改为MotionEvent.ACTION_CANCEL再分发一次.

定义状态

明白了上面的东西, 其实没必要看事件分发流程, 只是一些业务逻辑而已, 但这里简单讲一下BounceFrameLayout的几个状态, 方便理解.

    private static final int BS_IDLE = 1;
    private static final int BS_DRAG = 2;
    private static final int BS_SETTLE = 3;
    private static final int BS_WAIT = 4;

BS_IDLE
代表空闲, 也就是此时没有触摸事件. 如果发生触摸事件, 根据子view的可滑动性, 如果子view能处理这次滑动, 进入BS_WAIT, 否则进入BS_DRAG.
BS_DRAG
代表此时处在overscroll状态, 所有的事件都由我们自己处理, 如果此时松手, 进入BS_SETTLE, 如果用户又把我们拽回原位, 这时我们进入BS_IDLE
BS_SETTLE
代表在overscroll状态下用户松手, 我们处于回弹状态, 如果回弹没有完成就又收到触摸事件, 直接进入BS_DRAG
BS_WAIT
代表我们正在等待子view处理事件, 一旦子view无法处理, 那么将进入BS_DRAG

兼容性方法

ScrollHelper提供两个方法

    interface ScrollHelperImpl {
        boolean canScrollVertically(View v, int direction);
        void scrollVerticalBy(View v, int dy);
    }

其中在api 19上, 针对AbsListView分别调用它的新方法canScrollListscrollListBy, 在api 16上, scrollVerticalBy不执行任何操作, canScrollVertically使用兼容性方法

        private boolean canAbsListViewScrollVertically(AbsListView abslistview, int direction) {
            final int childCount = ab
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值