利用Android嵌套滑动机制轻松实现顶部布局置顶

Google在LOLLIPOP(SDK21)后加入的嵌套滑动官方解决方案。

1、问题典型场景

通常是信息流(比如社区、资讯、新闻)页面或者商品详情页的交互设计。 如图:

分解到代码就是一般三个控件:一个头布局,可能是吧banner;一个导航控件;下面一个内容的列表控件。要求头布局和导航布局在内容布局滑动了一定距离(一般是头布局的高度加上导航控件的高度)后,导航控件置顶,然后内容列表继续滑动。

2、Android事件分发机制处理问题的痛点

传统的Android事件分发是子控件消费了事件,那么父控件就不能再处理这个事件了。也就是说一旦内部的滑动控件消费了滑动操作,外部的滑动控件就不能获取到这个滑动动作也就无法做处理了。在我们上一个情景里,滑动内容列表控件要求头布局和导航布局作出响应就是要求他们的共同父布局作出响应,显然用传统的事件分发处理是很困难的。

3、Android嵌套滑动机制基础概念

嵌套滚动中的两个接口,在上文中已经提到。NestedScrollingParent和NestedScrollingChild 接口中的方法如下: NestedScrollingChild

  • startNestedScroll : 起始方法, 主要作用是找到接收滑动距离信息的外控件.
  • dispatchNestedPreScroll : 在内控件处理滑动前把滑动信息分发给外控件.
  • dispatchNestedScroll : 在内控件处理完滑动后把剩下的滑动距离信息分发给外控件.
  • stopNestedScroll : 结束方法, 主要作用就是清空嵌套滑动的相关状态
  • setNestedScrollingEnabled和isNestedScrollingEnabled : 一对get&set方法, 用来判断控件是否支持嵌套滑动.
  • dispatchNestedPreFling和dispatchNestedFling : 跟Scroll的对应方法作用类似

NestedScrollingParent

  • onStartNestedScroll : 对应startNestedScroll, 内控件通过调用外控件的这个方法来确定外控件是否接收滑动信息.
  • onNestedScrollAccepted : 当外控件确定接收滑动信息后该方法被回调, 可以让外控件针对嵌套滑动做一些前期工作.
  • onNestedPreScroll : 关键方法, 接收内控件处理滑动前的滑动距离信息, 在这里外控件可以优先响应滑动操作, 消耗部分或者全部滑动距离.
  • onNestedScroll : 关键方法, 接收内控件处理完滑动后的滑动距离信息, 在这里外控件可以选择是否处理剩余的滑动距离.
  • onStopNestedScroll : 对应stopNestedScroll, 用来做一些收尾工作.
  • onNestedPreFling和onNestedFling : 同上略

4、嵌套滑动关键类源码分析

子view接受到滚动事件后发起嵌套滚动,询问父View是否要先滚动,父View处理了自己的滚动需求后,回到子View处理自己的滚动需求,假如父View消耗了一些滚动距离,子View只能获取剩下的滚动距离做处理。子View处理了自己的滚动需求后又回到父View,剩下的滚动距离做处理。惯性fling的类似。

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

接下来在RecyclerView的onTouchEvent的 MotionEvent.ACTION_MOVE里调用了dispatchNestedPreScroll和scrollByInternal

case MotionEvent.ACTION_MOVE: {
   
    if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
        dx -= mScrollConsumed[0];
        dy -= mScrollConsumed[1];
        vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        // Updated the nested offsets
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    }
    
    if (mScrollState == SCROLL_STATE_DRAGGING) {
        mLastTouchX = x - mScrollOffset[0];
        mLastTouchY = y - mScrollOffset[1];

        if (scrollByInternal(
                canScrollHorizontally ? dx : 0,
                canScrollVertically ? dy : 0,
                vtev)) {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
       
    }
} break;

dispatchNestedPreScroll中调了父View的onNestedPreScroll,并且传入dy 和 consumed。用于做消费计数。

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
        final ViewParent parent = getNestedScrollingParentForType(type);
        if (parent == null) {
            return false;
        }

        if (dx != 0 || dy != 0) {
            ⋯⋯
            consumed[0] = 0;
            consumed[1] = 0;
            ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

            ⋯⋯
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

最终调用了父view的onNestedPreScroll()方法。 依次分析可以看出嵌套滚动执行的方法顺序如下:

(子)startNestedScroll → (父)onStartNestedScroll → (父)onNestedScrollAccepted→ (子)dispatchNestedPreScroll → (父)onNestedPreScroll→ (子)dispatchNestedScroll→ (父)onNestedScroll→ (子)dispatchNestedPreFling → (父)onNestedPreFling→ (子)dispatchNestedFling → (父)stopNestedScroll

5、嵌套滑动典型案例实践

关键方法就两个就可以完成效果,只是和僵硬,为了更好的用户体验,就需要加入手势速度的滑动预判:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mHeaderView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        mMaxScrollHeight = mHeaderView.getMeasuredHeight() - mHeaderRetainHeight;
        //设置主体的高度:代码中设置match_parent
        if (mBodyView.getLayoutParams().height < getMeasuredHeight() - mHeaderRetainHeight) {
            mBodyView.getLayoutParams().height = getMeasuredHeight() - mHeaderRetainHeight;
        }
        setMeasuredDimension(getMeasuredWidth(), mBodyView.getLayoutParams().height + mHeaderView.getMeasuredHeight());
    }

在onMeasure()中计算头部布局和置顶布局高度,完成整个控件的测量,并记下头部布局去掉置顶布局最大可滑动的距离值。

@Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        boolean hiddenTop = dy > 0 && getScrollY() < mMaxScrollHeight;
        boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);
        if (hiddenTop || showTop) {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }

然后重写这个方法就可以实现对应的滑动嵌套,也就是导航栏控件置顶,其实也就是预先知道了导航栏的高度,然后在下滑并且下滑距离大于最大可滑动距离,和上滑并且内容控件不可滑动的时候就全部滑动距离交给父控件也就是实现了NestedScrollParent接口的自己。

相当代码可以参考下我的github实例: StickyNestedScrollLayout

参考: Android NestedScrolling机制完全解析 带你玩转嵌套滑动

嵌套滚动设计和源码分析

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python攻城狮

HTML 5&CSS快速入门1.计算机中的文件2.网页组成4.HTML基础操作

超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页的标准标记语言。

3343
来自专栏Android干货

浅谈GridLayout(网格布局)

3889
来自专栏编程之路

羊皮书APP(Android版)开发系列(二十四)不常用但是很有用的两个属性:clipToPadding 和 clipChildren

1342
来自专栏小筱月

分享:纯 css 瀑布流 和 js 瀑布流

通过 Multi-columns 相关的属性 column-count、column-gap 配合 break-inside 来实现瀑布流布局。

5462
来自专栏web前端

制作H5响应式页面注意事项、微信二次分享

1、H5页面(APP端)      1.1     APP端页面用HTML5制作,头部需要加适配信息:   <meta http-equiv="Content-...

3189
来自专栏我和未来有约会

用silverlight做动画-相机

用silverlight做动画-相机 适合初学者学习 做一个相机的动画 和做flash动画一样,准备好素材 将素材放入项目中 开始正式制作前为了方便以后重用,...

2814
来自专栏Android开发实战

Android自定义View系列 (从小白做起) 二: 相知

上一章节中,主要介绍了三个主要成分 1.LayoutInflater.inflate()的参数及其用法 2.四种构造函数的说明,以及使用的地方 3.工具Pain...

993
来自专栏编程

前端学习自学笔记:day06

今天是第六天的笔记,主要是HTML和CSS的,希望大家支持~ ? 在此之前先为大家显示下前端工程师的路线图: ? 第六天的笔记:HTML AND CSS: te...

2035
来自专栏web前端

HTML+CSS基础

第一章 一、样式      1、行间样式,代码不可维护,不推荐      2、内联样式,不可重用,不推荐      3、外联样式,可重用,可维护,推荐     ...

6319
来自专栏HTML5学堂

移动端 模拟手机联系人触摸A~Z导航

HTML5学堂:今天要与大家分享一个当前移动端很常见的效果,类似于手机联系人的快速导航功能,即当触摸a~z的字母时,能够相对应的显示文字。有些手机的音乐导航也类...

3955

扫码关注云+社区

领取腾讯云代金券