|
1 | | ----------------------------------- |
2 | | -| | |
3 | | -| | |
4 | | -| | |
5 | | -| | cover |
6 | | -| | |
7 | | -| | |
8 | | -| | |
9 | | ----------------------------------- |
10 | | -| | | | | tab |
11 | | ----------------------------------- |
12 | | -| | |
13 | | -| | containerScrollView的header(可能有可能没有) |
14 | | -| | 这个containerScrollView不是指框架内的containerScrollView,是2个子scrollView的父视图 |
15 | | -| | |
16 | | ----------------------------------- |
17 | | -| | | |
18 | | -| | | |
19 | | -| | | |
20 | | -| | | |
21 | | -| | | |
22 | | -| | | |
23 | | -| | | 左collectionView + 右collectionView |
24 | | -| | | |
25 | | -| | | |
26 | | -| | | |
27 | | -| | | |
28 | | -| | | |
29 | | -| | | |
30 | | -| | | |
31 | | -| | | |
32 | | -| | | |
33 | | -| | | |
34 | | -| | | |
35 | | -| | | |
36 | | -| | | |
37 | | ----------------------------------- |
38 | | - |
39 | | - |
40 | | -类似美团、饿了吗的商品点餐页,在第一个tab下的子VC中有2个子scrollView,左边是分类,右边是列表。并切滑动cover区域时有一个小细节:如果左边或者右边子scrollView的偏移量是非初始状态,也就是contentOffset.y是大于0时,这时上下滑动cover的右边区域(手指触摸点的x值在右边子scrollView的区域内),会先滚动右边子scrollView,滑动cover的左边区域(手指触摸点的x值在左边子scrollView的区域内,会先滚动左边子scrollView |
41 | | - |
42 | | -我给出一个实现思路(针对第一个子VC): |
43 | | -最底部是一个容器scrollView(作为本框架中NestedPageScrollable协议里的的nestedPageContentScrollView),其上面添加了2个子scrollView,y值均为0即可,因为本框架会自动设置容器scrollView的contentInset.top和contentOffset.y,2个子scrollVIew会自动显示在tab栏的下方。 |
44 | | - |
45 | | -3个scrollView大致层级+约束关系如下: |
46 | | - |
47 | | -View (Controller.view) |
48 | | -│ |
49 | | -└── containerScrollView (UIScrollView) |
50 | | - │ edges = view.edges |
51 | | - │ |
52 | | - └── contentView (UIView) |
53 | | - │ edges = containerScrollView.contentLayoutGuide.edges |
54 | | - │ width = containerScrollView.frameLayoutGuide.width |
55 | | - │ |
56 | | - ├── leftScrollView (UITableView/UICollectionView) |
57 | | - │ top = contentView.top |
58 | | - │ bottom = contentView.bottom |
59 | | - │ leading = contentView.leading |
60 | | - │ width = 30% screen width |
61 | | - │ |
62 | | - └── rightScrollView (UITableView/UICollectionView) |
63 | | - top = contentView.top |
64 | | - bottom = contentView.bottom |
65 | | - leading = leftTableView.trailing |
66 | | - trailing = contentView.trailing |
67 | | - width = 70% screen width |
68 | | - |
69 | | -布局完成后,需要保证contentView的高度被撑开,这样containScrollView才会有contentSzie,可以手动计算contentView的高度,也可以重写子scrollView的intrinsicContentSize返回contentSize。 |
70 | | -复杂的点在于containerView和2个子scrollView的滚动处理: |
71 | | -1、2个子scrollView禁用滚动,只处理容器scrollView的滚动。这种情况如果右边子scrollView是collectionView并且有多个分组时,sectionHeader需要吸顶,由于collectionView自身不能滚动,可能需要手动计算sectionHeader的吸顶位置 |
72 | | -2、containerView和2个子scrollView的滚动可同时发生,但是在合适的时机需要锁定另一个scrollView的滚动... |
73 | | - |
74 | | -以上思路只是一个初步思考,需要通过实践去验证,由于时间问题,该示例没有去完成,以后有机会再尝试尝试。 |
| 1 | +# 外卖 App 点餐页 |
75 | 2 |
|
| 3 | +## 核心结构 |
76 | 4 |
|
| 5 | +这个页面的关键不是“双列表”,而是“双列表 + 一个明确的主滚动对象”。 |
77 | 6 |
|
| 7 | +架构关系如下: |
| 8 | + |
| 9 | +```text |
| 10 | +PageViewController |
| 11 | +└── 点餐页 VC.view |
| 12 | + ├── rightListView // 右侧商品列表,主滚动对象 |
| 13 | + └── leftListContainer // 左侧列表的父视图,不是 rightListView 的子视图 |
| 14 | + └── leftListView // 左侧分类列表 |
| 15 | +``` |
| 16 | + |
| 17 | +再强调一次: |
| 18 | + |
| 19 | +- 左侧列表不是右侧列表的子视图 |
| 20 | +- 左侧列表和右侧列表是兄弟视图 |
| 21 | +- 左侧列表有自己的父视图 |
| 22 | +- 右侧列表负责承接页面主滚动 |
| 23 | + |
| 24 | +之所以左侧要有单独父视图,不只是为了摆放方便,更是为了做裁剪、层级控制和命中区域控制。否则左侧很容易和顶部吸顶区域打架,也容易让人误以为它只是右侧内容上的一个悬浮子 view。 |
| 25 | + |
| 26 | +## 为什么右侧是主滚动对象 |
| 27 | + |
| 28 | +因为用户真正浏览的是商品,不是分类。 |
| 29 | + |
| 30 | +所以职责应该这样分: |
| 31 | + |
| 32 | +- 右侧商品列表负责页面主滚动 |
| 33 | +- 左侧分类列表负责导航和状态反馈 |
| 34 | + |
| 35 | +这比“左右两个列表地位完全对等”更符合真实交互。 |
| 36 | + |
| 37 | +## 顶部区域和列表的关系 |
| 38 | + |
| 39 | +页面上层还有一套公共结构: |
| 40 | + |
| 41 | +```text |
| 42 | +cover |
| 43 | +tab |
| 44 | +点餐内容区 |
| 45 | +``` |
| 46 | + |
| 47 | +其中点餐内容区内部再拆成左右两列。 |
| 48 | + |
| 49 | +更准确地说,应该理解为: |
| 50 | + |
| 51 | +```text |
| 52 | +页面整体 |
| 53 | +├── cover |
| 54 | +├── tab |
| 55 | +└── 点餐页内容 |
| 56 | + ├── rightListView |
| 57 | + └── leftListContainer |
| 58 | + └── leftListView |
| 59 | +``` |
| 60 | + |
| 61 | +也就是说,左侧列表和右侧列表都属于“点餐页内容”这一层,不属于 `cover`,也不属于彼此。 |
| 62 | + |
| 63 | +## 滚动的核心思想 |
| 64 | + |
| 65 | +这个页面不能理解成“两个列表各滚各的”,也不能理解成“两个列表永远强绑定同一个 offset”。 |
| 66 | + |
| 67 | +正确的理解是分两段。 |
| 68 | + |
| 69 | +### 1. 顶部还没收起时 |
| 70 | + |
| 71 | +这时候页面目标是先把 `cover` 推走,让 `tab` 吸顶。 |
| 72 | + |
| 73 | +因此左右列表要表现得像一个整体: |
| 74 | + |
| 75 | +- 左边滚,右边跟 |
| 76 | +- 右边滚,左边跟 |
| 77 | + |
| 78 | +这时同步的是位移。 |
| 79 | + |
| 80 | +### 2. 顶部收起后 |
| 81 | + |
| 82 | +这时候页面目标从“收起顶部”切换成“浏览商品”。 |
| 83 | + |
| 84 | +因此不应该继续强绑位移,而应该改成: |
| 85 | + |
| 86 | +- 右侧继续独立滚商品 |
| 87 | +- 左侧不再强制同步位移 |
| 88 | +- 左侧只根据右侧当前位置更新高亮 |
| 89 | + |
| 90 | +这时同步的是状态,不是位移。 |
| 91 | + |
| 92 | +这一点是整个方案最核心的地方。 |
| 93 | + |
| 94 | +## 左侧列表为什么不能简单当成右侧的附属视图 |
| 95 | + |
| 96 | +如果把左侧直接理解成右侧上的一个子视图,问题会很多: |
| 97 | + |
| 98 | +- 左侧自己的滚动不好处理 |
| 99 | +- 顶部区域收起时,两边的联动边界不清楚 |
| 100 | +- 吸顶后的层级关系容易混乱 |
| 101 | +- 左侧进入 tab 区域时不容易裁剪 |
| 102 | + |
| 103 | +所以更合理的做法是: |
| 104 | + |
| 105 | +- 右侧列表独立作为主滚动区 |
| 106 | +- 左侧列表独立存在 |
| 107 | +- 两者通过联动逻辑协作,而不是通过父子关系耦合 |
| 108 | + |
| 109 | +## 左右两列真正的关系 |
| 110 | + |
| 111 | +左侧和右侧不是“内容并列”关系,而是: |
| 112 | + |
| 113 | +- 左侧是分类导航 |
| 114 | +- 右侧是商品内容 |
| 115 | + |
| 116 | +所以经典交互应该是: |
| 117 | + |
| 118 | +- 左点右滚 |
| 119 | +- 右滚左亮 |
| 120 | + |
| 121 | +最终以右侧当前位置作为真实内容位置,左侧只是表达这个位置。 |
| 122 | + |
| 123 | +## 分组标题的关键点 |
| 124 | + |
| 125 | +右侧商品列表通常会有分组标题,但这里不能机械地套系统默认吸顶。 |
| 126 | + |
| 127 | +因为页面顶部已经有 `cover + tab` 的吸顶体系,如果分组标题再直接走默认吸顶,很容易出现: |
| 128 | + |
| 129 | +- 吸顶位置不对 |
| 130 | +- 和 tab 冲突 |
| 131 | +- 多层吸顶过渡不自然 |
| 132 | + |
| 133 | +所以分组标题的处理原则应该是: |
| 134 | + |
| 135 | +- 服从整页吸顶结构 |
| 136 | +- 必要时自己接管展示逻辑 |
| 137 | + |
| 138 | +重点不是“必须用系统 sectionHeader”,重点是最终吸顶效果是否正确。 |
| 139 | + |
| 140 | +## 一句话总结 |
| 141 | + |
| 142 | +这个页面的本质是: |
| 143 | + |
| 144 | +```text |
| 145 | +一个主商品列表 |
| 146 | ++ 一个独立分类列表 |
| 147 | ++ 一个左/右分阶段联动机制 |
| 148 | +``` |
| 149 | + |
| 150 | +只要这三个点立住,具体实现细节都可以继续调整。 |
0 commit comments