如何应对Android面试官->嵌套滚动原理大揭秘,实战京东首页二级联动

前言

本章节主要从 嵌套滑动、滑动冲突方案解决来入手,这里不是基于协调者布局;

嵌套滑动

我们先来看一个示例:

可以看到首页是有一个嵌套滑动的效果,上推的时候,频道先跟着移动,然后置顶不动,这种效果基于协调者布局可以实现,但是如果没有协调者布局的情况下,如何实现呢?

界面如何布局

在不使用 协调者 布局的情况下,界面最终的布局效果应该是类似下面这样的一层结构;

最外层 ScrollVIew/NestedScrollView

嵌套:不能滑动的RecyclerView、TabLayout、ViewPager

经过分析,我们来搭建一个这样的布局来看下

ini 复制代码
<ScrollView    
    android:layout_width="match_parent"    
    android:layout_height="match_parent"    
    android:orientation="vertical">  
      
    <LinearLayout        
        android:layout_width="match_parent"        
        android:layout_height="match_parent"        
        android:orientation="vertical">
        <!-- 这里要稍微改造下,改成不能滑动的 RecyclerView -->     
        <androidx.recyclerview.widget.RecyclerView            
            android:id="@+id/combo_top_view"            
            android:layout_width="match_parent"            
            android:layout_height="wrap_content" />        
        <LinearLayout            
            android:layout_width="match_parent"            
            android:layout_height="match_parent"            
            android:orientation="vertical">            
            <com.google.android.material.tabs.TabLayout                
                android:id="@+id/tablayout"                
                android:layout_width="match_parent"                
                android:layout_height="wrap_content" />            
            <androidx.viewpager2.widget.ViewPager2                
                android:id="@+id/viewpager_view"                
                android:layout_width="match_parent"                
                android:layout_height="match_parent" />        
        </LinearLayout>    
    </LinearLayout>
</ScrollView>

然后我们编译执行下,看下最终的滑动效果,是否能达到我们的预期:

可以看到,并没有达到我们期望的联动效果,那么原因是什么呢?

View 和 ViewGroup 的关系

  1. 代码层面:ViewGroup 继承 View;
  2. 运行层面:ViewGroup 嵌套 View;

事件分发是从运行时角度来分析的;

事件分发的流程

Activity dispatchTouchEvent ->

scss 复制代码
public boolean dispatchTouchEvent(MotionEvent ev) {    
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {        
        onUserInteraction();    
    }    
    if (getWindow().superDispatchTouchEvent(ev)) {        
        return true;    
    }    
    return onTouchEvent(ev);
}

PhoneWindow superDispatchTouchEvent ->

csharp 复制代码
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

DecorView superDispatchTouchEvent ->

typescript 复制代码
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

DecorView 的父类是 FrameLayout,其父类是 ViewGroup,最终调度到 ViewGroup 的 dispatchTouchEvent 方法

这 dispatchTouchEvent 方法中,先进行是否需要拦截判断,通过 onInterceptTouchEvent 方法

java 复制代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    //
    ...
    // 省略部分代码
    // 首先问询自身要不要进行事件的拦截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN        
            || mFirstTouchTarget != null) {    
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;    
        if (!disallowIntercept) {        
            intercepted = onInterceptTouchEvent(ev);        
            ev.setAction(action); // restore action in case it was changed    
        } else {        
            intercepted = false;    
        }
    } else {    
        // There are no touch targets and this action is not an initial down    
        // so this view group continues to intercept touches.    
        intercepted = true;
    }
}

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

这个是用来判断子 View 有没请求父 View(当前View)不要进行事件的拦截

子 View 通过调用这个方法,设置 mGroupFlags 的值,来修改 disallowIntercept 的逻辑;

如果子 View 没有要求父 View 不拦截,则父View(当前View)执行 onInterceptTouchEvent 来问询自身要不要拦截;

如果自身不拦截,则

嵌套滚动的实现原理

需要两个角色,一个角色用来实现 NestedScrollingParent3,一个角色需要实现 NestedScrollingChild3 接口,用来表示一个是父亲,一个是孩子,以此来表示它们之间的嵌套滑动;

那么,有人就提出了,使用 NestedScrollView,因为它实现了 NestedScrollingParent3,那么我们将布局改成 NestedScrollView :

ini 复制代码
<androidx.core.widget.NestedScrollView    
    android:layout_width="match_parent"    
    android:layout_height="match_parent"    
    android:orientation="vertical">  
      
    <LinearLayout        
        android:layout_width="match_parent"        
        android:layout_height="match_parent"        
        android:orientation="vertical">
        <!-- 这里要稍微改造下,改成不能滑动的 RecyclerView -->     
        <androidx.recyclerview.widget.RecyclerView            
            android:id="@+id/combo_top_view"            
            android:layout_width="match_parent"            
            android:layout_height="wrap_content" />        
        <LinearLayout            
            android:layout_width="match_parent"            
            android:layout_height="match_parent"            
            android:orientation="vertical">            
            <com.google.android.material.tabs.TabLayout                
                android:id="@+id/tablayout"                
                android:layout_width="match_parent"                
                android:layout_height="wrap_content" />            
            <androidx.viewpager2.widget.ViewPager2                
                android:id="@+id/viewpager_view"                
                android:layout_width="match_parent"                
                android:layout_height="match_parent" />        
        </LinearLayout>    
    </LinearLayout>
</androidx.core.widget.NestedScrollView>

我们运行看下效果:

虽然实现了嵌套滚动,但是 TabLayout 没有吸顶,随着滑动一起滚出了屏幕,还是不符合预期;

那么如何实现吸顶来达到我们的预期呢?

我们可以将 NestedScrollView 的内容元素进行拆分,将不能滑动的区域作为 NestedScrollView 的 header 部分,将 TabLayout 和 RecyclerView 整体划分成一部分作为 NestedScrollView 的最后一个 View,并且最后一个 View 的高度是整个屏幕的高度,这样当 NestedScrollView 滑动到最后一个 View 的时候,整个View 是充满屏幕的,那么 TabLayout 就能置顶了;

那么我们就需要这个 NestedScrollView 来判断最后是不是一个 View,以及获取最后一个View之后,设置它的高度为屏幕高度;

继承 NestedScrollView 实现下面两个方法:

scss 复制代码
@Override
protected void onFinishInflate() {    
    super.onFinishInflate();    
    // 根据 xml 的层级结构,我们需要获取的是第一个子 View 下的 第二个子 View    
    contentView = ((ViewGroup) getChildAt(0)).getChildAt(1);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
    // 重新设置 contentView 的高度为屏幕高度    
    ViewGroup.LayoutParams layoutParams = contentView.getLayoutParams();    
    layoutParams.height = getMeasuredHeight();    
    contentView.setLayoutParams(layoutParams);
}

我们编译运行看下:

可以看到,达到了我们期望的效果,实现了吸顶,但是还是有滑动冲突的地方,没有达到预期的效果,触摸滑动的时候 NestedScrollView 没有先滑动,而是 RecyclerView 滑动了一些距离才进行 NestedScrollView 的滑动;

我们可以根据嵌套滑动流程图来分析下:

根据流程图,可以知道,调用 startNestedScrolled 的时候,说明开始滑动了,我们进入 RecyclerView 的 startNestedScroll 方法看一下:

java 复制代码
@Override
public boolean startNestedScroll(int axes) {    
    return getScrollingChildHelper().startNestedScroll(axes);
}

这里通过一个 Helper 帮助类,来处理滑动事件,我们进入这个 Helper 看下

less 复制代码
public boolean startNestedScroll(@ScrollAxis int axes) {    
    return startNestedScroll(axes, TYPE_TOUCH);
}

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {    
    if (hasNestedScrollingParent(type)) {        
        // Already in progress        
        return true;    
    }
    // 判断有没有开启嵌套滑动    
    if (isNestedScrollingEnabled()) {
        // 获取当前 View 的 parent        
        ViewParent p = mView.getParent();        
        View child = mView;        
        while (p != null) {
            // 问询这个 parent 是否支持嵌套滑动            
            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;
}

因为是 NestedScrollView 所以肯定支持嵌套滑动,这里遇到 NestedScrollView 的时候就会 return true;

当手指滑动的时候,正常应该是先让 NestedScrollView 滑动,等它不能滑动的时候,RecyclerView 才进行滑动;

开始滑动的时候,RecyclerView 会执行 dispatchNestedPreScroll,而 NestedScrollView 则会执行 onNestedPreScroll 方法,来判断并且决定 NestedScrollView 要不要执行滑动

less 复制代码
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,        
        int type) {    
    dispatchNestedPreScroll(dx, dy, consumed, null, type);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,        
        int type) {    
    return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}

我们发现在 NestedScrollView 中它又去问询它的父亲能不能滑动了,这里就和 RecyclerView 冲突了,这是因为 NestedScrollView 即可以当父亲也可以当孩子导致,那么上面的问题原因也就找到了,我们把 NestedScrollView 当成了父亲,那么它就不需要再去问询自己的父亲了,只需要判断下自己能不能滑动,能滑动就自己滑动,同时记录消费的距离;

所以我们复写 NestedScrollView 的 onNestPresScroll 方法,在 RecyclerView 滑动之前,自己先处理滑动;

less 复制代码
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {    
    // 我们需要处理不可滑动的区域的可见高度    
    boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();    
    if (hideTop) {        
        scrollBy(0, dy);        
        consumed[1] = dy;    
    }
}

编译后效果如下:

可以看到,已经可以吸顶,并且先滑动顶部,再滑动 RecyclerView;但是在快速滑动之后的惯性滑动还是没有;我们需要加上惯性滑动,来保证体验的完美;

我们需要接入 Fling 来实现,重写 fling 方法,让子View RecyclerView fling 同样的值;

scss 复制代码
public void fling(int velocityY) {    
    super.fling(velocityY);    
    if (velocityY > 0) {        
        ViewPager2 viewPager2 = getChildView(this, ViewPager2.class);        
        if(viewPager2 != null) {                      
            RecyclerView childRecyclerView = getChildView(((ViewGroup)viewPager2.getChildAt(0)), RecyclerView.class);            
            if (childRecyclerView != null) {                
                childRecyclerView.fling(0, velocityY);            
            }        
        }    
    }
}

我们运行看下效果:

可以看到,实现了惯性滑动的效果,perfect~~

简历润色

简历上可写:深度理解嵌套滑动实现原理,可实现复杂的嵌套滚动效果;

下一章预告

事件冲突与解决方案大揭秘;

欢迎三连

来都来了,点个关注点个赞吧,你的支持是我最大的动力~~~

相关推荐
小冉在学习15 分钟前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论
Mr Lee_32 分钟前
android 配置鼠标右键快捷对apk进行反编译
android
代码之光_19801 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi1 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
顾北川_野1 小时前
Android CALL关于电话音频和紧急电话设置和获取
android·音视频
&岁月不待人&1 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove1 小时前
G1垃圾回收器日志详解
java·开发语言
对许1 小时前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j
无尽的大道1 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
小鑫记得努力2 小时前
Java类和对象(下篇)
java