UI系列一Android多子view嵌套通用解决方案

这篇具有很好参考价值的文章主要介绍了UI系列一Android多子view嵌套通用解决方案。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.多子view嵌套应用背景

百度App在17年的版本中实现2个子view嵌套滚动,用于Feed落地页(webview呈现文章详情 + recycle呈现Native评论)。原理是在外层提供一个UI容器(我们称之为”联动容器”)处理WebView和Recyclerview连贯嵌套滚动。

当时的联动容器对子view限制比较大,仅支持WebView和Recyclerview进行联动滚动,数量也只支持2个子View。

随着组件化进程的推进,为方便各业务解耦,对联动容器提出了更高的要求,需要支持任意类型、任意数量的子view进行联动滚动,也就是本文要阐述的多子view嵌套滚动通用解决方案。

先直观感受下联动容器嵌套滚动的Demo效果:

2. 多子view嵌套实现原理

同大多数自定义控件类似,联动容器也需要处理子view的测量、布局以及手势处理。测量和布局对联动容器的场景来说非常简单,手势处理相对复杂些。

从demo效果可以看出,联动容器需要处理好和子view嵌套滑动问题。嵌套滑动的处理方案有两种

  • 基于Google的NestedScrolling机制实现嵌套滑动;
  • 是由联动容器内部处理和子view嵌套滑动的逻辑。

百度App早期版本的联动容器采用的方案2实现的,下图为方案2联动容器手势处理流程:

笔者对方案2联动容器的实现代码做了开源,感兴趣的同学可以参考:github.com/baiduapp-te… 基于google的NestedScrolling实现多子view嵌套能节省不少开发量,故笔者对多子view嵌套的实现采用方案一。

3. 核心逻辑

3.1 Google嵌套滑动机制

Google在Android 5.0推出了一套NestedScrolling机制,这套机制滚动打破了对之前Android传统的事件处理的认知,是按照逆向事件传递机制来处理嵌套滚动,事件传递可参考下图:

网上有很多关于NestedScrolling的文章,如果没接触过NestedScrolling的同学可参考下张鸿洋的这篇文章:blog.csdn.net/lmj62356579…

3.2 接口设计

为了保证联动容器中子view的任意性,联动容器需提供完善的接口抽象供子view去实现。下图为联动容器暴露的接口类图:

ILinkageScroll是置于联动容器中的子view必须要实现的接口,联动容器在初始化时如果发现某个子view没实现该接口,会抛出异常。ILinkageScroll中又会涉及两个接口:LinkageScrollHandler、ChildLinkageEvent。

LinkageScrollHandler接口中的方法联动容器会在需要时主动调用,以通知子view完成一些功能,比如:获取子view是否可滚动,获取子view滚动条相关数据等。

ChildLinkageEvent接口定义了子view的一些事件信息,比如子view的内容滚动到顶部或底部。当发生这些事件后,子view主动调用对应方法,这样联动容器收到子view一些事件后会做出相应的反应,保证正常的联动效果。

上面仅简单说明了下接口功能,想更加深入了解的同学请参考:github.com/baiduapp-te…

接下来我们详细分析下联动容器对手势处理细节,根据手势类型,将嵌套滑动分为两种情况来分析:1. scroll手势;2. fling手势;

3.3 scroll手势

先给出scroll手势处理的核心代码:

public class ELinkageScrollLayout extends ViewGroup implements NestedScrollingParent {
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
boolean moveUp = dy > 0;
boolean moveDown = !moveUp;
int scrollY = getScrollY();
int topEdge = target.getTop();
LinkageScrollHandler targetScrollHandler
= ((ILinkageScroll)target).provideScrollHandler();
if (scrollY == topEdge) { // 联动容器scrollY与当前子view的top坐标重合
if ((moveDown && !targetScrollHandler.canScrollVertically(-1))
|| (moveUp && !targetScrollHandler.canScrollVertically(1))) {
// 在对应的滑动方向上,如果子view不能垂直滑动,则由联动容器消费滚动距离
scrollBy(0, dy);
consumed[1] = dy;
}
} else if (scrollY > topEdge) { // 联动容器scrollY大于当前子view的top坐标,也就是说,子view头部已经滑出联动容器
if (moveUp) {
// 如果手指上滑,则由联动容器消费滚动距离
scrollBy(0, dy);
consumed[1] = dy;
}
if (moveDown) {
// 如果手指下滑,联动容器会先消费部分距离,此时联动容器的scrollY会不断减小,
// 直到等于子view的top坐标后,剩余的滑动距离则由子view继续消费。
int end = scrollY + dy;
int deltaY;
deltaY = end > topEdge ? dy : (topEdge - scrollY);
scrollBy(0, deltaY);
consumed[1] = deltaY;
}
} else if (scrollY < topEdge) { // 联动容器scrollY小于当前子view的top坐标,也就是说,子view还没有完全露出
if (moveDown) {
// 如果手指下滑,则由联动容器消费滚动距离
scrollBy(0, dy);
consumed[1] = dy;
}
if (moveUp) {
// 如果手指上滑,联动容器会先消费部分距离,此时联动容器的scrollY会不断增大,
// 直到等于子view的top坐标后,剩余的滑动距离则由子view继续消费。
int end = scrollY + dy;
int deltaY;
deltaY = end < topEdge ? dy : (topEdge - scrollY);
scrollBy(0, deltaY);
consumed[1] = deltaY;
}
}
}
@Override
public void scrollBy(int x, int y) {
// 边界检查
int scrollY = getScrollY();
int deltaY;
if (y < 0) {
deltaY = (scrollY + y) < 0 ? (-scrollY) : y;
} else {
deltaY = (scrollY + y) > mScrollRange ?
(mScrollRange - scrollY) : y;
}
if (deltaY != 0) {
super.scrollBy(x, deltaY);
}
}
}

onNestedPreScroll()回调是google嵌套滑动机制NestedScrollingParent接口中的方法。当子view滚动时,会先通过此方法询问父view是否消费这段滚动距离,父view根据自身情况决定是否消费以及消费多少,并将消费的距离放入数组consumed中,子view再根据数组中的内容决定自己的滚动距离。

代码注释比较详细,这里整体再做个解释:通过对子view的上边沿阈值和联动容器的scrollY进行比较,处理了3种case下的滚动情况。

第10行,当scrollY == topEdge时,只要子view没有滚动到顶或者底,都由子view正常消费滚动距离,否则由联动容器消费滚动距离,并将消费的距离通过consumed变量通知子view,子view会根据consumed变量中的内容决定自己的滑动距离。

第17行,当scrollY > topEdge时,也就是说当触摸的子view头部已经滑出联动容器,此时如果手指向上滑动,滑动距离全部由联动容器消费,如果手指向下滑动,联动容器会先消费部分距离,当联动容器的scrollY达到topEdge后,剩余的滑动距离由子view继续消费。

第32行,当scrollY < topEdge这个和上一个第17行判断类似,这里不做过多解释。scroll手势处理流程图如下:

3.4 fling手势

联动容器对fling手势的处理大致思路如下:如果联动容器的scrollY等于子view的top坐标,则由子view自身处理fling手势,否则由联动容器处理fling手势。

而且在一次完整的fling周期中,联动容器和各子view将会交替去完成滑动行为,直到速度降为0,联动容器需要处理好交替滑动时的速度衔接,保证整个fling的流畅行。接下来看下详细实现:

public class ELinkageScrollLayout extends ViewGroup implements NestedScrollingParent {
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
int scrollY = getScrollY();
int targetTop = target.getTop();
mFlingOrientation = velocityY > 0 ? FLING_ORIENTATION_UP : FLING_ORIENTATION_DOWN;
if (scrollY == targetTop) { // 当联动容器的scrollY等于子view的top坐标,则由子view自身处理fling手势
// 跟踪velocity,当target滚动到顶或底,保证parent继续fling
trackVelocity(velocityY);
return false;
} else { // 由联动容器消费fling手势
parentFling(velocityY);
return true;
}
}
}

onNestedPreFling()回调是google嵌套滑动机制NestedScrollingParent接口中的方法。当子view发生fling行为时,会先通过此方法询问父view是否要消费这次fling手势,如果返回true,表示父view要消费这次fling手势,反之不消费。

第6行根据velocityY正负值记录本次的fling的方向;

第7行,当联动容器scrollY值等于触摸子view的top值,fling手势由子view处理,同时联动容器对本次fling手势的速度进行追踪,目的是当子view内容滚到顶或者底时,能够获得剩余速度以让联动容器继续fling;

第12行,由联动容器消费本次fling手势。下面看下联动容器和子view交替fling的细节:

public class ELinkageScrollLayout extends ViewGroup implements NestedScrollingParent {
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int y = mScroller.getCurrY();
y = y < 0 ? 0 : y;
y = y > mScrollRange ? mScrollRange : y;
// 获取联动容器下个滚动边界值,如果达到边界值,速度会传给下个子view,让子view继续快速滑动
int edge = getNextEdge();
// 边界检查
if (mFlingOrientation == FLING_ORIENTATION_UP) {
y = y > edge ? edge : y;
}
// 边界检查
if (mFlingOrientation == FLING_ORIENTATION_DOWN) {
y = y < edge ? edge : y;
}
// 联动容器滚动子view
scrollTo(x, y);
int scrollY = getScrollY();
// 联动容器最新的scrollY是否达到了边界值
if (scrollY == edge) {
// 获取剩余的速度
int velocity = (int) mScroller.getCurrVelocity();
if (mFlingOrientation == FLING_ORIENTATION_UP) {
velocity = velocity > 0? velocity : - velocity;
}
if (mFlingOrientation == FLING_ORIENTATION_DOWN) {
velocity = velocity < 0? velocity : - velocity;
}
// 获取top为edge的子view
View target = getTargetByEdge(edge);
// 子view根据剩余的速度继续fling
((ILinkageScroll) target).provideScrollHandler()
.flingContent(target, velocity);
trackVelocity(velocity);
}
invalidate();
}
}
/**

  • 根据fling的方向获取下一个滚动边界,
  • 内部会判断下一个子View是否isScrollable,
  • 如果为false,会顺延取下一个target的edge。
    /
    private int getNextEdge() {
    int scrollY = getScrollY();
    if (mFlingOrientation == FLING_ORIENTATION_UP) {
    for (View target : mLinkageChildren) {
    LinkageScrollHandler handler
    = ((ILinkageScroll)target).provideScrollHandler();
    int topEdge = target.getTop();
    if (topEdge > scrollY
    && isTargetScrollable(target)
    && handler.canScrollVertically(1)) {
    return topEdge;
    }
    }
    } else if (mFlingOrientation == FLING_ORIENTATION_DOWN) {
    for (View target : mLinkageChildren) {
    LinkageScrollHandler handler
    = ((ILinkageScroll)target).provideScrollHandler();
    int bottomEdge = target.getBottom();
    if (bottomEdge >= scrollY
    && isTargetScrollable(target)
    && handler.canScrollVertically(-1)) {
    return target.getTop();
    }
    }
    }
    return mFlingOrientation == FLING_ORIENTATION_UP ? mScrollRange : 0;
    }
    /
    *
  • child view的滚动事件
    */
    private ChildLinkageEvent mChildLinkageEvent = new ChildLinkageEvent() {
    @Override
    public void onContentScrollToTop(View target) {
    // 子view内容滚动到顶部回调
    if (mVelocityScroller.computeScrollOffset()) {
    // 从速度追踪器中获取剩余速度
    float currVelocity = mVelocityScroller.getCurrVelocity();
    currVelocity = currVelocity < 0 ? currVelocity : - currVelocity;
    mVelocityScroller.abortAnimation();
    // 联动容器根据剩余速度继续fling
    parentFling(currVelocity);
    }
    }
    @Override
    public void onContentScrollToBottom(View target) {
    // 子view内容滚动到底部回调
    if (mVelocityScroller.computeScrollOffset()) {
    // 从速度追踪器中获取剩余速度
    float currVelocity = mVelocityScroller.getCurrVelocity();
    currVelocity = currVelocity > 0 ? currVelocity : - currVelocity;
    mVelocityScroller.abortAnimation();
    // 联动容器根据剩余速度继续fling
    parentFling(currVelocity);
    }
    }
    };
    }

fling的速度传递分为:

  1. 从联动容器向子view传递;2. 从子view向联动容器传递。

先看速度从联动容器向子view传递。核心代码在computeScroll()回调方法中。第9行,获取联动容器下一个滚动边界值,如果达到下一个滚动边界值,联动容器需要将剩余速度传给下个子view,让其继续滚动。

第46行,getNextEdge()方法内部整体逻辑:遍历所有子view,将联动容器当前的scrollY与子view的top/bottom进行比较来获取下一个滑动边界。

第34行,当联动容器检测到滑动到下个边界时,则调用ILinkageScroll.flingContent()让子view根据剩余速度继续滚动。

再看速度从子view向联动容器传递,核心代码在第76行。当子view内容滚动到顶或者底,会回调onContentScrollToTop()方法或者onContentScrollToBottom()方法,联动容器收到回调后,在第86行和第98行,继续执行后续滚动。fling手势处理流程图如下:

4. 滚动条

4.1 Android系统的ScrollBar

对于内容可滚动的页面,ScrollBar则是一个不可或缺的UI组件,所以,ScrollBar也是联动容器必须要实现的功能。

好在Android系统对滚动条的抽象非常友好,自定义控件只需要重写View中的几个方法,Android系统就能帮助你正确绘制出滚动条。我们先看下View中的相关方法:

/**

  • Compute the vertical offset of the vertical scrollbar's thumb within the horizontal range. This value is used to compute the position

  • of the thumb within the scrollbar’s track.

  • The range is expressed in arbitrary units that must be the same as the units used by {@link #computeVerticalScrollRange()} and

  • {@link #computeVerticalScrollExtent()}.

  • @return the vertical offset of the scrollbar’s thumb
    /
    protected int computeVerticalScrollOffset() {
    return mScrollY;
    }
    /
    *

  • Compute the vertical extent of the vertical scrollbar's thumb within the vertical range. This value is used to compute the length

  • of the thumb within the scrollbar’s track.

  • The range is expressed in arbitrary units that must be the same as the units used by {@link #computeVerticalScrollRange()} and

  • {@link #computeVerticalScrollOffset()}.

  • @return the vertical extent of the scrollbar’s thumb
    /
    protected int computeVerticalScrollExtent() {
    return getHeight();
    }
    /
    *

  • Compute the vertical range that the vertical scrollbar represents.

  • The range is expressed in arbitrary units that must be the same as the units used by {@link #computeVerticalScrollExtent()} and

  • {@link #computeVerticalScrollOffset()}.

  • @return the total vertical range represented by the vertical scrollbar
    */
    protected int computeVerticalScrollRange() {
    return getHeight();
    }

    先自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

android嵌套布局,程序员,ui,android

android嵌套布局,程序员,ui,android

android嵌套布局,程序员,ui,android

android嵌套布局,程序员,ui,android

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以扫码领取!!!!

android嵌套布局,程序员,ui,android

最后

针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

android嵌套布局,程序员,ui,android

android嵌套布局,程序员,ui,android
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!

ter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!**

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

[外链图片转存中…(img-4ABiFY76-1711251238056)]

[外链图片转存中…(img-LJ5iAQZ1-1711251238056)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!文章来源地址https://www.toymoban.com/news/detail-852910.html

到了这里,关于UI系列一Android多子view嵌套通用解决方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Flutter 问题系列第 78 篇】Android Studio 升级后提示 org.gradle.java.home Gradle property is invalid 的解决方案

    这是【Flutter 问题系列第 78 篇】,如果觉得有用的话,欢迎关注专栏。 Flutter SDK:3.3.5,Dart SDK:2.18.2, 操作系统:macOS Ventura 13.0.1 Intel Core i9,Android Studio 版本:Flamingo 2022.2.1 Patch 2 一:问题描述 在公司的 M2 电脑上,直接使用 Android Studio 的检查更新,升级到 Flamingo 2022.2.1 版

    2024年02月05日
    浏览(50)
  • Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺

    我们看到内部又调用了父类 dispatchTouchEvent 方法, 所以最终是交给 ViewGroup 顶级 View 来处理分发了。 顶级 View 对点击事件的分发过程 在上一小节中我们知道了一个事件的传递流程,这里我们就大致在回顾一下。首先点击事件到达顶级 ViewGroup 之后,会调用自身的 dispatchTouchE

    2024年04月14日
    浏览(67)
  • Android 动态代码设置view宽高参数,运行后UI大小没有改变问题

         日常开发中遇到一个需求,就是根据业务逻辑,动态改变一个view控件的大小。这种需求也是比较常见的,但是小白比较容易遇到一个小问题,就是代码重新设置了view的宽高大小,运行后发现view没有发生改变。          如下图,1,横屏 2,正方形,3,竖屏      

    2024年02月16日
    浏览(45)
  • 跨平台应用开发进阶(五十)uni-app ios web-view嵌套H5项目白屏问题分析及解决

    应用 uni-app 框架开发好APP上架使用过程中,发现应用经过长时间由后台切换至前台时,通过 webview 方式嵌套的H5页面发生白屏现象。 任何手机设备上,当手机内存不足时,os都会回收资源。一般是先回收后台打开的资源。如果当前应用占用的资源过高,当前应用也有可能崩溃

    2024年02月14日
    浏览(51)
  • Android进阶系列:八、自定义View之音频抖动动效

    ####绘制矩形抖动 我们要绘制音频抖动的效果,矩形的高度肯定不能一样,而是要根据声音的大小来显示,这里我们没有声音,简单模拟一下给高度乘上for循环里的i效果如图: 至此我们已经知道了如何绘制多个矩形,并控制不同的高度,那我们要如何动态的控制高度呢?比如

    2024年04月25日
    浏览(40)
  • 如何应对Android面试官->实战高级UI,用自定义View画一条锦鲤(下)

    上一章我们用自定义View绘制了一条小鱼,本章我们让这条小鱼游动起来; 涉及的知识点 实现小鱼的摆动,我们可以通过属性动画 ValueAnimator 来实现,这里先简单介绍下属性动画 ValueAnimator 没有重绘,所以需要自己调用 addUpdateListener 方法,结合 AnimatorUpdateListener 使用; 操作

    2024年02月22日
    浏览(42)
  • 微信小程序之scroll-view自适配屏幕高度解决方案

    需求说明: 一般用于列表数据的展示(带有搜索框),根据官方文档,scroll-view需要固定一个高度,那么,对于不同的手机分辨率来说,可能显示的效果就不一样了,有的没到底,有的显示不全等等 解决方案:   不同的手机,但是可以通过计算,获取到scroll-view的填充高度。

    2024年02月12日
    浏览(59)
  • Vue3 or: Unknown variable dynamic import: ../views/的解决方案

    目录 ​编辑 错误信息 原来的代码 修改后的代码     这样的写法在Vue2中是可以正常运行的但是在Vue3中就不可以了的。 我们注意到,我们是先将所有的vue文件读取出来放到一个数组之中的。 然后再去数组中取值,这样才能动态的加载组件实现动态路由的效果。

    2024年02月16日
    浏览(55)
  • Qt中postevent造成内存泄漏问题的通用解决方案

    在Qt中由QCoreApplication统一管理Qt事件的收发和销毁,其中sendEvent为阻塞式发送,用于单线程的事件发送;postevent为非阻塞式发送,构造事件的线程和接受事件的线程可以为两个线程。 最近在做一个个人项目ShaderLab 需要绘制OpenGL实时渲染的图像,由于OpenGL渲染基本都放在循环语

    2024年02月15日
    浏览(43)
  • 【微服务】springboot 整合mysql实现版本管理通用解决方案

    目录 一、前言 1.1 单独执行初始化sql 1.2 程序自动执行 二、数据库版本升级管理问题

    2024年02月13日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包