关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 安卓开发 »Android NestedScrolling 实战

    Android NestedScrolling 实战

    2015-08-24 00:00:00 出处:子墨博客
    分享

    从 Android 5.0 Lollipop 开始提供一套 API 来支持嵌入的滑动效果。同样在最新的 Support V4 包中也提供了前向的兼容。有了嵌入滑动机制,就能实现很多很复杂的滑动效果。在 Android Design Support 库中非常总要的 CoordinatorLayout 组件就是使用了这套机制,实现了 Toolbar 的收起和展开功能,如下图所示:

    Android NestedScrolling 实战

    NestedScrolling提供了一套父 view 和子 View 滑动交互机制。要完成这样的交互,父 View 需要实现 NestedScrollingParent 接口,而子 View 需要实现 NestedScrollingChild 接口。

    实现 NestedScrollingChild

    首先来说NestedScrollingChild。假如你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View 中,包含一个 NestedScrollingChildHelper 辅助类。NestedScrollingChild接口的实现,基本上就是调用本 Helper 类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变。

    需要做的就是,假如要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用dispatchNestedPreScroll()。假如父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,假如滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll()。

    以上是一些基本原理,有了上面的基本思路,可以参考这篇 文章 ,这里面有原理的详细解析。假如还是不清楚, 这里 有对应的代码可以参考。

    实现 NestedScrollingParent

    作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现NestedScrollingParent,这个接口方法和NestedScrollingChild大致有一一对应的关系。同样,也有一个 NestedScrollingParentHelper 辅助类来默默的帮助你实现和 Child 交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。

    从上面的 Child 分析可知,滑动开始的调用startNestedScroll(),Parent 收到onStartNestedScroll()回调,决定是否需要配合 Child 一起进行处理滑动,假如需要配合,还会回调onNestedScrollAccepted()。

    每次滑动前,Child 先询问 Parent 是否需要滑动,即dispatchNestedPreScroll(),这就回调到 Parent 的onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。

    Child 滑动以后,会调用onNestedScroll(),回调到 Parent 的onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。

    最后,滑动结束,调用onStopNestedScroll()表示本次处理结束。

    其实,除了上面的 Scroll 相关的调用和回调,还有 Fling 相关的调用和回调,处理逻辑基本一致。

    实战

    有了这一套官方的嵌套滑动的解决方案,打算把我的 FlyRefresh 的滑动和下来部分用 NestedScrolling 来实现。我在这篇博客中讲了,之前是通过在PullHeaderLayout的dispatchTouchEvent()中小心处理 Touch 事件来实现的。现在回想起来,这种方法相对复杂,需要清楚知道 Parent 和 Child 的滑动状态,这就导致了,只能支持有限的 Child 类型,例如当时只支持 ListView 和 RecyclerView,为了支持更多的类型,还定义了一个IScrollHandler接口来支持。

    让 FlyRefresh 实现NestedScrollingParent,就可以支持所有的NestedScrollingChild作为FlyRefreshLayout的子 View。另外,因为CoordinatorLayout是如此的重要,大部分的 App 都需要使用它作为顶层的 Layout,为了让FlyRefreshLayout能够在 CoordinatorLayout 也能使用,所以我还打算同时实现NestedScrollingChild接口。关键实现代码如下:

    public class PullHeaderLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild {
    
        private final int[] mScrollOffset = new int[2];
        private final int[] mScrollConsumed = new int[2];
        private final NestedScrollingParentHelper mParentHelper;
        private final NestedScrollingChildHelper mChildHelper;
        ...
    
        // NestedScrollingChild
    
        @Override
        public void setNestedScrollingEnabled(boolean enabled) {
            mChildHelper.setNestedScrollingEnabled(enabled);
        }
    
        @Override
        public boolean isNestedScrollingEnabled() {
            return mChildHelper.isNestedScrollingEnabled();
        }
    
        @Override
        public boolean startNestedScroll(int axes) {
            return mChildHelper.startNestedScroll(axes);
        }
    
        @Override
        public void stopNestedScroll() {
            mChildHelper.stopNestedScroll();
        }
    
        @Override
        public boolean hasNestedScrollingParent() {
            return mChildHelper.hasNestedScrollingParent();
        }
    
        @Override
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                            int dyUnconsumed, int[] offsetInWindow) {
            return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                    offsetInWindow);
        }
    
        @Override
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
            return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
        }
    
        @Override
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
            return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
        }
    
        @Override
        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
        }
    
        // NestedScrollingParent
        @Override
        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
            return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
    
        @Override
        public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
            mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
        }
    
        @Override
        public void onStopNestedScroll(View target) {
            stopNestedScroll();
        }
    
        @Override
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                                   int dyUnconsumed) {
            final int myConsumed = moveBy(dyUnconsumed);
            final int myUnconsumed = dyUnconsumed - myConsumed;
            dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
        }
    
        @Override
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
            if (dy > 0 && mHeaderController.canScrollUp()) {
                final int delta = moveBy(dy);
                consumed[0] = 0;
                consumed[1] = delta;
                //dispatchNestedScroll(0, myConsumed, 0, consumed[1], null);
            }
        }
    
        @Override
        public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
            if (!consumed) {
                flingWithNestedDispatch((int) velocityY);
                return true;
            }
            return false;
        }
    
        private boolean flingWithNestedDispatch(int velocityY) {
            final boolean canFling = (mHeaderController.canScrollUp() && velocityY > 0) ||
                    (mHeaderController.canScrollDown() && velocityY < 0);
            if (!dispatchNestedPreFling(0, velocityY)) {
                dispatchNestedFling(0, velocityY, canFling);
                if (canFling) {
                    fling(velocityY);
                }
            }
            return canFling;
        }
    
        @Override
        public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
            return flingWithNestedDispatch((int) velocityY);
        }
    
        @Override
        public int getNestedScrollAxes() {
            return mParentHelper.getNestedScrollAxes();
        }
    
        // Touch event hanlder
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            MotionEvent vtev = MotionEvent.obtain(ev);
            final int actionMasked = MotionEventCompat.getActionMasked(ev);
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                mNestedYOffset = 0;
            }
            vtev.offsetLocation(0, mNestedYOffset);
    
            switch (actionMasked) {
                ...
                case MotionEvent.ACTION_MOVE:
                	...
                    final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
                    int deltaY = mLastMotionY - y;
                    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                        deltaY -= mScrollConsumed[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    }
                    if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                        final ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                        mIsBeingDragged = true;
                        if (deltaY > 0) {
                            deltaY -= mTouchSlop;
                        } else {
                            deltaY += mTouchSlop;
                        }
                    }
                    if (mIsBeingDragged) {
                        // Scroll to follow the motion event
                        mLastMotionY = y - mScrollOffset[1];
    
                        final int scrolledDeltaY = moveBy(deltaY);
                        final int unconsumedY = deltaY - scrolledDeltaY;
                        if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                            mLastMotionY -= mScrollOffset[1];
                            vtev.offsetLocation(0, mScrollOffset[1]);
                            mNestedYOffset += mScrollOffset[1];
                        }
                    }
                    break;
                ...
            }
            ...
            return true;
        }
    
        ...
    }

    完整的修改,可以看这个 commit 。整个修改下来,代码减少了不少,而且更加整洁了。

    总结

    总体来说, NestedScroll 初看起来有些让人费解,但是真的理解以后,就发现这种设计的优秀之处。把滑动整体封装起来,通过 Helper 来实现 Child 和 Parent 之间的连接和交互。通过接口来回调,实现了 Child 和 Parent 的逻辑独立。

    Android 5.0的大部分可以滑动的控件都支持了 NestScrolling 接口,最新的 Support V4 中也一样,相信以后越来越多的第三方库都会支持,到时候各种控件的嵌套滑动就能无缝集成了。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    SQL汉字转换为拼音的函数

    windows 7系统无法运行Photoshop CS3的解决方法

    巧用MySQL加密函数对Web网站敏感数据进行保护

    MySQL基础知识简介

    Windows7和WinXP下如何实现不输密码自动登录系统的设置方法介绍

    windows 7系统ip地址冲突怎么办?windows 7系统IP地址冲突问题的

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。