原文地址:jiar.me/article/Mul…

本文旨在对于SegementSlide库实现原理的讲解,有兴趣的同学,欢迎前往Github地址浏览。


背景

如今的app中,越来越多地采用如下图所示的设计,一般用在诸如『用户主页』、『话题详情页』、『专题详情页』等这些场景。通常,这些场景会带有头部视图(头部视图可能要求支持滚动渐变),下面紧接着的是分页控件,最下面是滚动列表。

如下图所示:

各种方案以及优缺点

为了方便下面的说明,在开始之前,先约定几个说法,下面的各种方案,大都离不开在最底层放上一个UIScrollView(竖直方向滚动),我们称之为rootScrollView。无论分页控件下方有多少个子界面,总有一个当前界面,我们称当前界面下的UIScrollView(竖直方向滚动)为childScrollView

I 控制isScrollEnabled属性

这是我们第一时间能想到的方案,通过给rootScrollViewchildScrollView实现UIScrollViewDelegate,并在func scrollViewDidScroll(_ scrollView: UIScrollView)方法中实时将scrollView.contentOffset.y与临界值进行对比从而修改两者scrollViewisScrollEnabled属性值来达到目的。

大致代码如下

func scrollViewDidScroll(_ scrollView: UIScrollView) {if scrollView == rootScrollView {if scrollView.contentOffset.y >= headerStickyHeight {scrollView.contentOffset.y = headerStickyHeightrootScrollView.isScrollEnabled = falsechildScrollView.isScrollEnabled = true}} else {if scrollView.contentOffset.y <= 0 {scrollView.contentOffset.y = 0childScrollView.isScrollEnabled = falserootScrollView.isScrollEnabled = true}}
}

方法简单,但是有个不太能接受的交互问题,但凡将isScrollEnabled设置为false,这次的滑动手势就会被打断,从表现上来看,就是滑动到临界值时滑动会被中断。

II 自定义滑动手势

在这篇文章这篇文章中,作者提供了一种利用自定义手势的方式来实现。 但是,只是添加普通的滑动手势是不够的,UIScrollView是自带阻尼效果的,因此引入了UIDynamicAnimator来实现阻尼效果。 这是一种不错的思路。不过完全自定义手势来实现UIScrollView的效果,需要考虑的细节过多,挺难处理得跟系统的效果一致(写这篇文章的时候,下载了作者提供的源码,commitIDff7b76f8468bc87fea8ea6975d8b9fe1173ab031,在真机iPhone X上运行,感觉还是有交互上的问题)。此外,因为是自定义手势,手势不是直接作用在UIScrollView上的,UIScrollViewScrollIndicator是无法显示的,通过改变UIScrollViewcontentOffset,其ScrollIndicator也是无法显示的,必须要手势作用在UIScrollView上才行。使用UIScrollViewflashScrollIndicators()来强迫ScrollIndicator显示出来?...可能还真行,不过我没试过,感觉太粗暴了。

III 手势穿透

这应该是目前相对主流的一种实现方式,比如在这篇文章中,便是介绍了这种方式。据我观察Twitter和微博的用户主页可能是使用这种方式实现的(写这篇文章的时候,Twitter版本为:7.41.2,微博版本为:9.2.0,推测错了的话还望见谅)

该方案的核心为有两点:

  • 让滑动手势穿透使得rootScrollViewchildScrollView都能接收到滑动手势(因为手势是作用到UIScrollview上的,自然是能显示ScrollIndicator的)。做法是让rootScrollView实现UIGestureRecognizerDelegate的代理方法func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool,并在适当的时机返回true

这部分的代码大致如下:

class SegementSlideScrollView: UIScrollView, UIGestureRecognizerDelegate{func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {return true}}

当然只是如此的话,是不够的,这样的结果是滑动的时候,导致rootScrollViewchildScrollView一起滚动。

  • 增加两个标志位来控制何时允许rootScrollView滚动,以及何时允许childScrollView

这部分代码大致如下:

func scrollViewDidScroll(_ scrollView: UIScrollView) {if scrollView == rootScrollView {if !canParentViewScroll {rootScrollView.contentOffset.y = headerStickyHeight // point AcanChildViewScroll = true} else if scrollView.contentOffset.y >= headerStickyHeight {rootScrollView.contentOffset.y = headerStickyHeightcanParentViewScroll = falsecanChildViewScroll = true}} else {if !canChildViewScroll {childScrollView.contentOffset.y = 0 // point B} else if scrollView.contentOffset.y <= 0 {canChildViewScroll = falsecanParentViewScroll = true}}
}

如上代码所示,控制rootScrollView或者是childScrollView不可滚动的方式是将两者的contentOffset.y设置为一个固定值(见注释point Apoint B),并不是简单地将isScrollEnabled设置false而已。

没问题了?不,也是有不足之处的: 在第一个界面使用手指向上滑动,让头部视图完全被隐藏后再向上滑动一些,让childScrollViewcontentOffset.y处于大于0的状态,随后,左右切换到第二个界面,使用手指向下滑动,完全拉出头部视图,然后再切换回第一个界面,这个时候,使用手指在屏幕上稍微滑动一下,rootScrollView或是childScrollViewcontentOffset.y会突变,从表现上看,就是发生『位置突变现象』

问题产生的原因是什么? canParentViewScrollchildScrollView始终为一对相反的值,浏览上诉代码,会发现在point Apoint B处,将rootScrollView或者是childScrollViewcontentOffset.y设置为了一个固定值。这样的处理,当始终在同一个界面滑动的时候,不会有问题,但是,在切换界面后,由于rootScrollView是共用的,在新界面改动了rootScrollViewcontentOffset.y,切换回原界面后,稍做滑动,定会执行point A或是point B其中的一处代码,从而导致『位置突变现象』。

在微博和Twitter中对此问题做了简单的处理。微博上,在切换至新界面之前,将原界面的childScrollViewcontentOffset.y值重置为了0。Twitter上,则是在合适的时机做了重置。这也是推测两者可能是使用了该方案的原因。

如下图所示:

SegementSlide的需求

SegementSlide是使用 方案III 来实现的。

此外我希望它还能支持一些别的特性:

  1. 简单易用的接口
  2. 一般使用 方案III 实现的例子,大都只是支持在rootScrollView上实现阻尼效果,我希望也能在childScrollView上实现,可以选择任意一个阻尼来使用。(有阻尼,就可以配套下拉刷新工具来使用了)
  3. 一般使用 方案III 实现的例子,大都是需要手指在子视图部分滑动才能实现联动,希望也能在头部滑动实现联动
  4. 既可以支持使用头部视图,也可以不需要头部视图
  5. 头部视图可以使用简单的接口实现滚动渐变效果(navigation上随着滚动改变背景色、标题、leftItem颜色、rightItem颜色,或是背景色透明之类的),也可以自定义渐变效果
  6. 子控件既可结合一起使用,也可以单独使用
  7. 分页标题旁可以显示红点 ...

对此,大都已经实现:

  1. 看下如下示例代码,是否还算简单易用:
import SegementSlideclass HomeViewController: SegementSlideViewController{......override var headerHeight: CGFloat? {return view.bounds.height/4}override var headerView: UIView? {return UIView()}override var titlesInSwitcher: [String] {return ["Swift", "Ruby", "Kotlin"]}override func segementSlideContentViewController(at index: Int) -> SegementSlideContentScrollViewDelegate? {return ContentViewController()}override func viewDidLoad() {super.viewDidLoad()canCacheScrollState = truereloadData()scrollToSlide(at: 0, animated: false)}}
import SegementSlideclass ContentViewController: UITableViewController, SegementSlideContentScrollViewDelegate{......@objc var scrollView: UIScrollView {return tableView}}
  1. 已经能否支持“父阻尼”和“子阻尼”效果了

重写SegementSlideViewController的属性bouncesType,它是一个枚举类型:

enum BouncesType{case parentcase child
}

默认值为.parent,如下重写,即可实现『子阻尼』效果:

class HomeViewController: SegementSlideViewController {......override var bouncesType: BouncesType {return .child}
}
  1. 如何使得在头部滑动也能实现滚动联动效果? 我在SegementSlideHeaderView中重写了方法func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?,在合适的情况下返回了childScrollView。目前这不是一个最优的方法,因为我没能够在这个方法中判断出这个事件是滑动还是点击事件,这里还可以优化。

  2. 既可以支持使用头部视图,也可以不需要头部视图 SegementSlideViewController是实现这套方案的基类,其中有一个headerView属性,该属性为可选值,返回nil则表示不需要头部视图。我在项目配套的Example工程中,其中的首页便是没有头部视图的示例,不过增加了下拉显示navigation、上滑隐藏navigation的效果。一般使用 方案III 的例子,在rootScrollView上使用了UITableView,为了使用UITableViewtableHeaderView属性,以及吸顶效果。SegementSlidev1版本的时候,使用了UICollectionView,也是处于同样的目的,现v2已经改成了UIScrollView,吸顶效果的话,可以通过增加一条到view.safeAreaLayoutGuide.topAnchor的约束来实现。

  3. 快速应用头部渐变效果? TransparentSlideViewController是继承于SegementSlideViewController的子类,其中的headerView属性已被改成非可选值。其中另外定义了一些属性,用于头部视图处于『显示状态』或是『嵌入状态』时,titleViewnavigationBar对应属性的改动。

如下所示:

typealias DisplayEmbed<T> = (display: T, embed: T)override var isTranslucents: DisplayEmbed<Bool> {return (true, false)
}override var attributedTexts: DisplayEmbed<NSAttributedString?> {return (nil, nil)
}override var barStyles: DisplayEmbed<UIBarStyle> {return (.black, .default)
}override var barTintColors: DisplayEmbed<UIColor?> {return (nil, .white)
}override var tintColors: DisplayEmbed<UIColor> {return (.white, .black)
}

其中DisplayEmbed为一个typealias表示『显示状态』或是『嵌入状态』时的值。

需要注意的是:

  • TransparentSlideViewController中的titleView是使用自定义的方式并赋值给navigationItem.titleView来实现的,最先考虑的是修改navigationBartitleTextAttributes属性,实践下来,发现会出现titleTextAttributes已经修改完毕,但是效果没有改变的情况。
  • TransparentSlideViewController会在viewWillAppear时保存navigation上对应样式的状态,并在viewWillDisappear时进行还原,来保证从一个TransparentSlideViewController(A)进入到另一个TransparentSlideViewController(B)时,navigation上样式的状态不会有错误,所以也不该在viewDidLoad时修改navigation上的样式,因为BviewDidLoad先于AviewWillDisappear执行。

如果需要自定义渐变效果,可以模仿TransparentSlideViewController继承SegementSlideViewController来实现需要的效果。Example中使用的是原生的UINavigationController,和TransparentSlideViewController配合起来,可以做到还算满意的效果。但是,实际情况下每个项目中可能会去改动默认的navigation,如果TransparentSlideViewController不适用,则需要使用自定义的方式来支持已有项目。

  1. 子控件既可结合一起使用,也可以单独使用 目前SegementSlideSwitcherViewSegementSlideContentView既可以作为SegementSlideViewController的子控件来使用,也可以单独拿出来使用,Example工程中的NoticeViewController便是单独使用的例子,实现了将switcher放在navigation上的效果。

  2. 红点显示? SegementSlideSwitcherView支持了红点显示

enum BadgeType{case nonecase pointcase count(Int)
}

红点类型为枚举值,从上述代码可以看出红点是支持『普通红点显示』还有『带数字红点显示』。

还需要优化的点

  1. 上面在第3点已经提到,『头部滑动也能实现滚动联动效果』目前对此的解决方法不是最优。

  2. 方案III 所提到的『位置突变现象』,我在SegementSlideViewController中提供了canCacheScrollState属性,值为true时,在切换界面的时候会缓存当前的canParentViewScrollcanChildViewScroll以及rootScrollViewcontentOffset.y值,并在切换回该界面的时候恢复;值为false时,即为类似微博的处理,在切换到新界面前将当前界面的childScrollViewcontentOffset.y值置为0。设置为true时会有一个效果,担心这个效果难以被接受,故将该值的默认值设置为了false

效果如下:

但这仍不是一个很好的处理方式。

  1. 联动滚动切换的时候,还没有达到完美的流畅效果。由于point Apoint B处将contentOffset.y强制设值来阻止滚动,同时也导致了滚动切换时『动能』不足的结果,也就是还不够流畅。

接下去要做的事

自然是要解决上面提到的三点不足的地方,要想让联动完美般流畅,还是需要使用一个滚动,而不是两个。我在本地开了个v3分支做了个尝试,在视图顶层覆盖一层透明的UIScrollView,借用它的手势、它的contentOffset来控制rootScrollViewchildScrollViewcontentOffset,可以解决上述提到的三个需要优化的点,但是同时也带来了其他好多问题,这里就不细说了,哪天问题都解决了,更新了v3版本,再来补充说明吧。

参考

  • iOS 嵌套UIScrollview的滑动冲突另一种解决方案
  • iOS scrollView嵌套tableView的手势冲突解决方案

结束语

编写本文时,SegementSlide的版本号为2.0-beta-13。另外,本站还未开通评论功能,如对本文中的内容存在疑问,或者发现文中的不正确之处,欢迎在本文的掘金地址评论区中友善提出。如对本项目有任何疑问,欢迎前往issues提出,同时也欢迎来Pull requests,为本项目做贡献。

『欢迎关注我的个人微信订阅号,我将不定期分享编程相关内容』

转载于:https://juejin.im/post/5c63ee7d51882562654aaf37

多层 UIScrollView 嵌套滚动解决方案相关推荐

  1. android studio 显示view树_Android手势分发和嵌套滚动机制

    前言   对于一个Android开发者而言,要开发一个APP你必须要了解事件分发,而要开发一个优秀的APP你就必须要理解嵌套滚动.   在Android的开发体系里面,手势体系是一块非常重要的内容.从 ...

  2. UIScrollView无法滚动可能的原因及解决办法分析

    为什么80%的码农都做不了架构师?>>>    -&: UIScrollView无法滚动可能的原因及解决办法分析 * 没有设置contentSize      -> c ...

  3. UIScrollView无法滚动

    •如果UIScrollView无法滚动,可能是以下原因: Ø没有设置contentSize ØscrollEnabled = NO Ø没有接收到触摸事件:userInteractionEnabled ...

  4. mui 页面滚动解决方案

    mui 页面滚动解决方案 参考文章: (1)mui 页面滚动解决方案 (2)https://www.cnblogs.com/zhangruiqi/p/8193891.html 备忘一下.

  5. java mybatis多层collection嵌套查询

    java mybatis多层collection嵌套查询 1.实体 package com.humi.iem.common.model.equipment;import io.swagger.anno ...

  6. html+css实现多层表格嵌套

    html+css实现多层表格嵌套 https://blog.csdn.net/cb2474600377/article/details/45533555 <div class="res ...

  7. 关于嵌套滚动机制的一点思索

    最近在做折叠式标题栏效果的时候遇到这样一个问题,布局代码如下所示,截取了两个片段, 我们看到这个页面上有几个个可以滑动的控件,AppBarLayout,NestedScrollview不是什么自定义的 ...

  8. android嵌套组合动画,Android 三级NestedScroll嵌套滚动实践

    嵌套滚动介绍 我们知道 NestedScrolling(Parent/Child) 这对接口是用来实现嵌套滚动的,一般实现这对接口的 Parent 和 Child 没有直接嵌套,否则直接用 onInt ...

  9. python从多层循环嵌套中退出只能使用goto_goto语句只能用于退出多层循环.doc

    goto语句只能用于退出多层循环 goto语句只能用于退出多层循环 篇一:goto语句只能用于退出多层循环 for (condition1){ for (condition2) { for (cond ...

最新文章

  1. 3D-VID:基于LiDar Video信息的3D目标检测框架|CVPR20
  2. kmeans聚类算法_聚类算法入门:k-means
  3. 浅谈String和StringBuffer类:
  4. PHP的Postman使用
  5. 佳能2020转印带拆卸图解_RF人文街拍小钢炮 佳能RF35mm F1.8 MACRO IS STM
  6. 路由器笔记 CCNA
  7. 从GWT开发人员的角度概述Scala.js
  8. matlab 中括号
  9. 【APICloud系列|28】 UIChatBox 模块(聊天输入框)的实现
  10. Centos或者Redhet开通telnet
  11. C++ 比较两个字符串的“大小”
  12. RapidMiner tuts
  13. 国土空间规划数据汇交标准_国土空间规划数据治理指南来了
  14. Linux mkdir 与 mkdir -p 的区别
  15. Tomcat优化之配置线程池高并发连接
  16. ES6学习笔记五(对象)
  17. 国产arm芯片CH32F103芯片开发下载使用简介
  18. 手搭深度推荐模型(四) NFM
  19. AD fanout 各选项说明
  20. poi多个模板实现文档合并

热门文章

  1. 全球及中国散热市场应用格局与竞争态势研究报告2022-2027年
  2. 2022年全球及中国木质纤维素纤维行业投资态势与营利前景模式咨询报告
  3. 全球及中国碳纤维风电叶片市场投资分析与运营潜力预测报告2021-2027年
  4. 全球及中国球形活性炭行业十四五发展潜力及投资价值分析报告2022-2027年
  5. NPAPI开发详解,Windows版
  6. 戛古 Kakku Pagodas
  7. django-5-自定义模板过滤器及标签
  8. CPlus的简单线程的制作
  9. matlab 中fft的用法
  10. OleDb执行Oracle带自定义函数的SQL深度历险