:方案對(duì)比與工程實(shí)踐)
嗯這句話(huà)幾乎原樣出現(xiàn)在我們項(xiàng)目的群里。產(chǎn)品經(jīng)理指著設(shè)計(jì)稿上那個(gè)滾輪選擇器ScrollWheel問(wèn)我這里面的卡片有的是一行文字有的是兩行文字加一張小圖能不能直接放進(jìn)同一個(gè)滾輪里我第一反應(yīng)是能啊給每個(gè)組件設(shè)置不同的高度不就行了。然后我就收獲了一堆奇奇怪怪的 Bug文字重疊、滾動(dòng)位置錯(cuò)亂、點(diǎn)擊命中不準(zhǔn)。這篇文章就把這個(gè)問(wèn)題的完整鏈路寫(xiě)清楚為什么滾輪組件天然排斥不同高度的子項(xiàng)、實(shí)測(cè)會(huì)踩到哪些坑、幾種可行的破解方案、不同框架下的落地代碼以及我最后在項(xiàng)目里到底怎么選的。1. 滾輪組件默認(rèn)只認(rèn)等高子項(xiàng)問(wèn)題并非你的錯(cuò)覺(jué)1.1 一個(gè)看似簡(jiǎn)單卻反直覺(jué)的需求先還原一下需求。我們當(dāng)時(shí)做的是一個(gè)“目標(biāo)設(shè)定”頁(yè)面用戶(hù)需要在一個(gè)類(lèi)似飛盤(pán)轉(zhuǎn)輪的滾輪里選擇“每周閱讀時(shí)長(zhǎng)”“每周運(yùn)動(dòng)次數(shù)”這類(lèi)選項(xiàng)。大部分選項(xiàng)只有一行字但其中有一個(gè)選項(xiàng)需要附上說(shuō)明文字還有一個(gè)小圖標(biāo)。設(shè)計(jì)稿上它比別的選項(xiàng)高出一截。在我接手之前大家的想法都很一致ScrollWheel 既然是滾輪那就是一個(gè)豎向滾動(dòng)的容器里面放多個(gè)組件天然應(yīng)該支持不同高度。但實(shí)際一跑所有的組件都被壓成了同一個(gè)高度有的內(nèi)容被截?cái)嗔擞械闹車(chē)霈F(xiàn)了一大片空白。這不是我們代碼寫(xiě)錯(cuò)了而是滾輪組件的基本運(yùn)行邏輯決定的。1.2 滾輪的底層布局假設(shè)所有子項(xiàng)共享同一個(gè)高度絕大多數(shù)滾輪組件不管叫什么名字底層布局模型都遵循一個(gè)極簡(jiǎn)規(guī)則把內(nèi)容區(qū)當(dāng)成一條“傳送帶”每一項(xiàng)占用的高度是同一個(gè)固定值滾輪的位置通過(guò)“索引 × 固定高度”計(jì)算出來(lái)。舉個(gè)例子。iOS 里 UIPickerView 的rowHeight就是全局設(shè)定的同一時(shí)間所有行都遵守這個(gè)高度。Flutter 里L(fēng)istWheelScrollView的itemExtent也是整個(gè)列表統(tǒng)一的單位長(zhǎng)度。Web 端常見(jiàn)的 picker 類(lèi)組件更是直接把 item 高度寫(xiě)成了一個(gè)常量比如 32px 或者 40px。為什么要這么設(shè)計(jì)因?yàn)樗屨麄€(gè)布局計(jì)算變得極其簡(jiǎn)單滾動(dòng)偏移量可以直接用索引算出來(lái)不需要實(shí)時(shí)測(cè)量每一個(gè)子項(xiàng)滾輪運(yùn)動(dòng)過(guò)程中的放大縮小效果強(qiáng)烈依賴(lài)“中心位置”概念中心位置如果用統(tǒng)一高度計(jì)算一幀之內(nèi)就能完成命中測(cè)試也簡(jiǎn)單比如當(dāng)前顯示的五個(gè) item哪個(gè)在中間只需要計(jì)算偏移量落在哪個(gè)區(qū)間即可。一旦打破“所有子項(xiàng)等高”這個(gè)假設(shè)上面這套計(jì)算全部要重寫(xiě)每個(gè)子項(xiàng)的位置變成累加高度滾動(dòng)到中間時(shí)該對(duì)齊哪個(gè)點(diǎn)的語(yǔ)義變得模糊點(diǎn)擊命中測(cè)試也必須根據(jù)每個(gè)子項(xiàng)實(shí)際坐標(biāo)來(lái)算。這不是改一個(gè)參數(shù)就能解決的問(wèn)題而是改變整個(gè)布局模型的問(wèn)題。想清楚這一點(diǎn)就明白“Cant the ScrollWheel set components of different heights”這句疑問(wèn)答案不是“不能”而是“默認(rèn)模型不支持需要換一種實(shí)現(xiàn)思路”。2. 實(shí)測(cè)第一版不同高度組件在滾輪里的真實(shí)表現(xiàn)2.1 現(xiàn)象清單比你想的更離譜知道原理是一回事實(shí)際踩坑又是一回事。我在當(dāng)時(shí)直接做了一個(gè)最小 demo強(qiáng)制給滾輪里的某個(gè)組件設(shè)置了不同高度的 frame。結(jié)果是這樣的高度被視覺(jué)壓縮。滾輪為了維持“每個(gè) item 占位一致”會(huì)把所有內(nèi)容強(qiáng)制放進(jìn)同一個(gè)矩形區(qū)域高組件的內(nèi)容被壓扁或裁掉。文本重疊。低組件和高組件相鄰時(shí)高組件的下半部分會(huì)疊到下一個(gè) item 的繪制區(qū)域里視覺(jué)上直接亂掉。滾動(dòng)停止位置錯(cuò)亂。滾輪在慣性滾動(dòng)結(jié)束后會(huì)找一個(gè)“穩(wěn)定點(diǎn)”穩(wěn)定點(diǎn)的計(jì)算以統(tǒng)一高度為周期。如果某個(gè) item 實(shí)際高度是其他 item 的兩倍滾輪停下來(lái)的位置經(jīng)常是兩個(gè) item 的交界處沒(méi)有一個(gè) item 真正居中。點(diǎn)擊命中不準(zhǔn)。用戶(hù)明明點(diǎn)的是第三個(gè) item命中測(cè)試卻命中了第二個(gè)因?yàn)槊袇^(qū)域還是按統(tǒng)一高度算的。這些現(xiàn)象單獨(dú)出現(xiàn)還能忍一起出現(xiàn)的時(shí)候基本就屬于“功能不可用”。我自己的測(cè)試結(jié)論是在不修改布局模型的前提下試圖通過(guò)“直接改高度”讓滾輪支持不同高度的組件是一條死路。2.2 根因追蹤三處計(jì)算都建立在“假高度”上我把問(wèn)題拆了一遍發(fā)現(xiàn)滾輪組件里有三處硬編碼的高度假設(shè)。只要有一個(gè)子項(xiàng)高度不同三處全崩。第一處是偏移量換算。滾輪組件維護(hù)一個(gè)當(dāng)前偏移量然后靠“偏移量 / 固定高度”反推出當(dāng)前中心索引。當(dāng)固定高度是個(gè)假高度時(shí)索引和內(nèi)容的對(duì)齊關(guān)系就錯(cuò)了。比如第五個(gè) item 實(shí)際位置在 200 像素處而組件以為它在 180 像素處滾動(dòng)就會(huì)錯(cuò)位。第二處是視覺(jué)變換。滾輪效果通常會(huì)給每個(gè) item 加上縮放、傾斜、透明度越遠(yuǎn)離中心越小越淡。這個(gè)效果的強(qiáng)弱依賴(lài) item 到中心的距離而距離的計(jì)算又依賴(lài)統(tǒng)一高度。如果 item 高度不一致離中心同樣像素距離的兩個(gè) item視覺(jué)上放大倍率卻完全不同看起來(lái)就像在抖動(dòng)。第三處是 hit test。滾輪組件會(huì)根據(jù)每個(gè) item 的相對(duì)位置計(jì)算點(diǎn)擊區(qū)域計(jì)算邏輯通常是從中心開(kāi)始上下等距劃分。高度不一致后這個(gè)等距劃分的區(qū)域與真實(shí)渲染區(qū)域不重合點(diǎn)擊自然不準(zhǔn)。這三處問(wèn)題不是靠樣式 hack 能解決的必須從根源上改變布局模型。所以后文講的幾種方案本質(zhì)上都是在“繞開(kāi)默認(rèn)布局模型”。3. 破解思路等高網(wǎng)格化、動(dòng)態(tài)測(cè)量、切換容器三選一3.1 方案一等高網(wǎng)格化——把不同內(nèi)容塞進(jìn)同一個(gè)固定高度這個(gè)方法最省事思路是“強(qiáng)迫每個(gè) item 在滾輪里占同一個(gè)高度但內(nèi)容在 item 內(nèi)部自由布局”。比如統(tǒng)一高度設(shè)為 88 像素一行文字的 item 就在垂直方向居中顯示兩行文字加一張小圖的 item 也在 88 像素內(nèi)部排列內(nèi)容。優(yōu)點(diǎn)很明顯完全不需要改滾輪組件的布局邏輯所有默認(rèn)效果都還在改動(dòng)量小穩(wěn)定性高。缺點(diǎn)也同樣明顯會(huì)出現(xiàn)大量空白尤其是內(nèi)容很少的 item 占著很高的位置視覺(jué)密度不均勻。它適合什么場(chǎng)景呢選項(xiàng)數(shù)量不多內(nèi)容差異不大且 UI 對(duì)間距容忍度較高的頁(yè)面。比如設(shè)置頁(yè)里的滾輪選擇器選項(xiàng)都是“低/中/高”這種差異化小時(shí)可以用。但如果設(shè)計(jì)稿明確要求不同 item 高度直接不同比如有的是卡片、有的是文字條目這個(gè)方法就會(huì)被斃掉因?yàn)榭瞻滋?。我?dāng)時(shí)的最初版就是用的這個(gè)方案順利上線(xiàn)但產(chǎn)品反饋“看起來(lái)每個(gè)格子都太大”于是才被迫想后面的方案。3.2 方案二動(dòng)態(tài)測(cè)量——讓每個(gè) item 獲得真實(shí)尺寸動(dòng)態(tài)測(cè)量的邏輯比較直白在把 item 放進(jìn)滾輪之前先測(cè)量?jī)?nèi)容得到真實(shí)高度然后把高度傳給滾輪布局系統(tǒng)。聽(tīng)起來(lái)完美但有一個(gè)前提滾輪布局系統(tǒng)必須支持“每個(gè) item 單獨(dú)設(shè)置尺寸”。如果用的是 UIPickerView 或ListWheelScrollView這種寫(xiě)死itemExtent的組件連傳不同高度的入口都沒(méi)有那動(dòng)態(tài)測(cè)量就只能搭配“自定義滾輪布局”使用。自定義滾輪怎么做核心是把原來(lái)一步到位的布局拆成兩步第一步計(jì)算每個(gè) item 的高度。可以提前測(cè)量也可以在 item 渲染完成后通過(guò)回調(diào)拿到實(shí)際高度再刷新布局。第二步自己維護(hù)一個(gè)內(nèi)容總高度和一個(gè)累積偏移表。任何位置計(jì)算都不再使用“索引 × 固定高度”而是先遍歷累積偏移表找到當(dāng)前偏移量對(duì)應(yīng)的是哪個(gè) item以及 item 內(nèi)部的相對(duì)偏移位置。滾動(dòng)效果也要自己處理每個(gè) item 到中心的距離需要基于 item 真實(shí)位置減去當(dāng)前偏移量來(lái)計(jì)算再根據(jù)距離做縮放和透明度的動(dòng)畫(huà)。這套方案能做到視覺(jué)上的完美適配但工程量不小。它適合 item 內(nèi)容差異大、數(shù)量不多比如少于 10 個(gè)、且交互要求接近原生滾輪的場(chǎng)景。如果內(nèi)容數(shù)量很多動(dòng)態(tài)測(cè)量加自定義布局的計(jì)算量會(huì)明顯上升后面性能篇會(huì)細(xì)說(shuō)。3.3 方案三切換容器——不再用滾輪而是用普通列表加居中高亮很多場(chǎng)景下用戶(hù)真正需要的并不是“滾輪的 3D 旋轉(zhuǎn)效果”而是“上下滑動(dòng)選擇當(dāng)前選中的項(xiàng)高亮”。既然滾輪對(duì)等高有硬性要求那就干脆換成普通列表。普通列表天然支持不同高度的子項(xiàng)因?yàn)樗牟季帜P捅旧砭褪蔷€(xiàn)性的每個(gè)子項(xiàng)用自己的實(shí)際 frame。然后只需要在列表中間畫(huà)一條高亮分隔線(xiàn)或者給當(dāng)前居中的 item 加一個(gè)放大效果用戶(hù)照樣能感知到“正在選擇哪一項(xiàng)”。這個(gè)方案的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單、支持任意高度、性能穩(wěn)定、后續(xù)要加多行文本或復(fù)雜卡片都沒(méi)問(wèn)題。缺點(diǎn)也很現(xiàn)實(shí)失去了滾輪那種“兩邊逐漸縮小消失”的酷炫效果產(chǎn)品上如果很看重這個(gè)視覺(jué)特征就得做取舍。我在后面的落地代碼里會(huì)展示這個(gè)方案的一個(gè)具體形態(tài)它是我最終選型的基礎(chǔ)。4. 不同框架的落地代碼參考4.1 Flutter從 ListWheelScrollView 遷移到居中列表Flutter 里L(fēng)istWheelScrollView是標(biāo)準(zhǔn)的滾輪組件它的itemExtent必須固定。如果非要支持不同高度一種替代方案是自己寫(xiě) Linear 列表 滾動(dòng)定位??梢韵榷x一個(gè)控制器監(jiān)聽(tīng)滾動(dòng)偏移量實(shí)時(shí)計(jì)算當(dāng)前中心項(xiàng)索引然后對(duì)中間項(xiàng)做放大和透明度的處理class CenterListPage extends StatefulWidget { const CenterListPage({super.key}); override StateCenterListPage createState() _CenterListPageState(); } class _CenterListPageState extends StateCenterListPage { final ScrollController _controller ScrollController(); final Listdouble _itemHeights [56, 88, 56, 120, 56, 72]; override Widget build(BuildContext context) { return Stack( children: [ ListView.builder( controller: _controller, itemCount: _itemHeights.length, itemBuilder: (context, index) { return Container( height: _itemHeights[index], alignment: Alignment.center, decoration: BoxDecoration( color: index.isEven ? Colors.blueGrey : Colors.blueGrey[100], ), child: Text(選項(xiàng) ${index 1}), ); }, ), Center( child: Container( height: 2, color: Colors.orange, ), ), ], ); } }這個(gè)例子用了一個(gè)_itemHeights數(shù)組來(lái)模擬不同高度實(shí)際項(xiàng)目中這個(gè)數(shù)組應(yīng)該來(lái)自對(duì)內(nèi)容測(cè)量后的真實(shí)結(jié)果。ListView.builder 支持每個(gè) item 不同高度代碼核心就這么簡(jiǎn)單一個(gè)普通列表加上一條居中指示線(xiàn)。如果想保留滾輪的“中間放大”感覺(jué)可以在滾動(dòng)回調(diào)中根據(jù) item 到中心的像素距離修改 item 的 scale_controller.addListener(() { final offset _controller.offset; for (int i 0; i _itemHeights.length; i) { final itemCenter _getItemCenter(i) - offset; final distance (itemCenter - screenCenter).abs(); final scale (1 - distance / 400).clamp(0.7, 1.0); // 根據(jù) index 用 GlobalKey 找到對(duì)應(yīng) item更新 scale } });需要注意Flutter 里L(fēng)istView的 item 并不保證在所有幀都能拿到自己的 RenderObject如果 item 滑出屏幕再通過(guò) GlobalKey 找就可能為空。所以不要在每個(gè)滾動(dòng)幀里做強(qiáng)依賴(lài)只做視覺(jué)比例更新或者用AnimatedScale配合索引變化來(lái)控制。4.2 SwiftUI用 ScrollView 加 scrollTargetBehavior 解決SwiftUI 里以前做滾輪選擇器普遍用UIPickerView包裝實(shí)際還是固定高度模型。iOS 17 之后有了scrollTargetBehavior配合 fixedSize 可以實(shí)現(xiàn)類(lèi)似“滾輪吸附”的效果而且 item 高度可以不同?;舅悸肥瞧胀?ScrollView每個(gè)子項(xiàng)設(shè)置自己的高度滾動(dòng)行為用.scrollTargetBehavior(.viewAligned)讓列表在停止時(shí)自動(dòng)對(duì)齊到最近的子項(xiàng)ScrollView { LazyVStack(spacing: 8) { ForEach(items) { item in Text(item.title) .padding() .frame(height: item.height) .scrollTargetLayout() } } } .scrollTargetBehavior(.viewAligned) .frame(height: 300)這里面有兩個(gè)關(guān)鍵點(diǎn)。第一scrollTargetLayout()要加在子項(xiàng)的公共容器上讓滾動(dòng)系統(tǒng)知道對(duì)齊的目標(biāo)是哪個(gè)區(qū)域。第二frame(height:)按 item 自己的數(shù)據(jù)設(shè)置不再使用 UIPickerView 那種固定行高。如果項(xiàng)目還停留在低版本 iOS可以用onChange(of: scrollPosition)監(jiān)聽(tīng)滾動(dòng)偏移手動(dòng)計(jì)算當(dāng)前中心 item然后把居中 item 做放大處理。但需要知道手動(dòng)方案里 item 的真實(shí) y 坐標(biāo)也要自己維護(hù)等于把動(dòng)態(tài)測(cè)量那套邏輯在 SwiftUI 里再實(shí)現(xiàn)一遍。我建議能用新 API 就用新 API省心很多。4.3 Web 前端CSS scroll-snap 一行解決Web 端做不同高度的滾輪選擇器最簡(jiǎn)單的方式是 CSSscroll-snap-type它允許每個(gè)子項(xiàng)有自己的高度同時(shí)保持滾動(dòng)吸附的交互。HTML 結(jié)構(gòu)大致如下div classwheel styleheight: 240px; overflow-y: scroll; scroll-snap-type: y proximity; div classitem styleheight: 56px;選項(xiàng) A/div div classitem styleheight: 120px; 選項(xiàng) Bbr/包含兩行說(shuō)明文字 /div div classitem styleheight: 72px;選項(xiàng) C/div /divCSS 部分.wheel { scroll-snap-type: y proximity; } .wheel .item { scroll-snap-align: center; }scroll-snap-align: center保證了每次滾動(dòng)停止時(shí)子項(xiàng)盡量對(duì)準(zhǔn)滾動(dòng)容器的中間位置。不同 item 的高度可以任意設(shè)置瀏覽器會(huì)自動(dòng)計(jì)算對(duì)齊位置。這個(gè)方案比 JS 定義滾輪組件省力得多而且在移動(dòng)端和桌面端現(xiàn)代瀏覽器上支持穩(wěn)定。唯一的痛點(diǎn)是“慣性滾動(dòng)的力度不好控制”有的瀏覽器滾動(dòng)停止位置距離預(yù)期較遠(yuǎn)還是需要一點(diǎn)滾動(dòng)力度補(bǔ)償邏輯但絕大多數(shù)情況下夠用。5. 團(tuán)隊(duì)項(xiàng)目里的避坑清單與性能底線(xiàn)5.1 測(cè)量結(jié)果一定要緩存不管是動(dòng)態(tài)測(cè)量還是普通列表方案都要面對(duì)“高度從哪來(lái)”的問(wèn)題。很多人會(huì)直接在build方法里實(shí)時(shí)測(cè)量結(jié)果每次滾動(dòng)都觸發(fā)一輪文本尺寸計(jì)算列表一長(zhǎng)就掉幀。正確做法是把測(cè)量結(jié)果放進(jìn)一個(gè)緩存字典鍵是 item 的唯一標(biāo)識(shí)值是高度。只在 item 內(nèi)容變化時(shí)重新測(cè)量。如果是圖片異步加載導(dǎo)致的尺寸變化等圖片加載完成后再更新對(duì)應(yīng)高度并主動(dòng)刷新那一個(gè) item而不是刷新整個(gè)列表。我這里有一個(gè)常見(jiàn)的反面案例列表有 30 個(gè) item每個(gè)都包含不等長(zhǎng)的文字和一張圖片圖片尺寸又不一樣。直接實(shí)時(shí)測(cè)量時(shí)每次滾動(dòng)都卡卡到明顯掉幀。加了緩存并只在圖片加載完成后更新對(duì)應(yīng)行的高度后滾動(dòng)的流暢度恢復(fù)到接近原生。5.2 滾動(dòng)過(guò)程中的跳動(dòng)問(wèn)題用普通列表模擬滾輪時(shí)最容易被忽視的問(wèn)題是“滾動(dòng)操作中更新 item 高度”。如果用戶(hù)正在快速滑動(dòng)某個(gè) item 因?yàn)閳D片加載完畢而突然變高整個(gè)列表內(nèi)容高度變化滾動(dòng)位置就會(huì)瞬間跳動(dòng)視覺(jué)上像頁(yè)面閃了一下。處理思路有兩個(gè)方向。一是延遲更新圖片加載完成后不直接改高度而是等頁(yè)面靜置超過(guò) 300ms 后再更新。這樣用戶(hù)滑動(dòng)過(guò)程中不會(huì)被打斷。二是保留舊高度如果新舊高度差距不大可以簡(jiǎn)單保留舊值等下次布局時(shí)再改。大多數(shù)場(chǎng)景都不需要像素級(jí)精確體驗(yàn)優(yōu)先。我自己偏向用“延遲更新”方案因?yàn)樗恍枰幚韽?fù)雜的狀態(tài)同步實(shí)現(xiàn)起來(lái)最直接。5.3 底部留白與慣性邊界普通滾輪的慣性結(jié)束位置通常會(huì)被組件自己糾正到最近的一個(gè) item 中心點(diǎn)。但普通列表的慣性是自由滾動(dòng)最后可能停在任意位置比如停在兩個(gè) item 的交界處或者最后幾個(gè) item 上方還露出很大空白。解決底部空白的方法是給列表內(nèi)容底部補(bǔ)一段 padding讓最后一個(gè) item 也能滾到中心位置。具體數(shù)值大約是容器高度的一半減去最后一個(gè) item 高度的一半。同理頂部也要補(bǔ)一段否則第一個(gè) item 會(huì)頂在容器頂部無(wú)法居中。如果要實(shí)現(xiàn)“自動(dòng)吸附到最近 item”需要監(jiān)聽(tīng)滾動(dòng)結(jié)束事件用animateTo把偏移量移到目標(biāo) item 的中心位置。這一步在 Web 端可以直接用 scroll-snap在原生端就手動(dòng)處理代碼量不大但很關(guān)鍵否則用戶(hù)會(huì)覺(jué)得這個(gè)“滾輪”手感怪怪的。5.4 命中測(cè)試與無(wú)障礙動(dòng)態(tài)測(cè)量方案里自定義命中測(cè)試不要自己實(shí)現(xiàn)直接用組件系統(tǒng)給出的 hitTest 結(jié)果前提是 item 的真實(shí) frame 已更新。普通列表方案則天然命中正確因?yàn)槊總€(gè) item 的 frame 就是實(shí)際 frame。無(wú)障礙也要注意滾輪組件一般會(huì)給輔助功能標(biāo)記一個(gè)“可調(diào)節(jié)值”但不同 item 高度下輔助功能可能讀不出當(dāng)前選中的是哪一項(xiàng)。在普通列表方案里最好手動(dòng)設(shè)置 accessibility 的 value值就是當(dāng)前高亮的 item 文本。6. 我最終的選型建議和實(shí)際心得折騰完這一輪我給團(tuán)隊(duì)寫(xiě)了一個(gè)選型判斷表按需求強(qiáng)度來(lái)推薦方案。需求特征推薦方案原因選項(xiàng)內(nèi)容長(zhǎng)度差異小UI 允許大量留白等高網(wǎng)格化改動(dòng)最小、穩(wěn)定性最高選項(xiàng)內(nèi)容差異大但數(shù)量少10 個(gè)動(dòng)態(tài)測(cè)量 自定義滾輪視覺(jué)最貼近滾輪效果選項(xiàng)內(nèi)容差異大數(shù)量可能很多普通列表 居中高亮性能最穩(wěn)支持任意高度Web 端移動(dòng)端兼容為主CSS scroll-snap一行代碼解決吸附只想要“能展示不同高度”且不要求 3D 滾輪效果普通列表 高亮線(xiàn)最省事接受度最高我最終在項(xiàng)目里選的是普通列表加居中高亮。原因很簡(jiǎn)單選項(xiàng)里有兩行文字和圖片高度差異大而且列表整體不超過(guò) 8 個(gè) item不需要 3D 旋轉(zhuǎn)來(lái)節(jié)省空間。用戶(hù)實(shí)際感知上“中間有一條高亮線(xiàn)”和“滾輪停到中間”的區(qū)別非常小但維護(hù)成本差很多。回頭看這個(gè)問(wèn)題的本質(zhì)ScrollWheel 的等高設(shè)計(jì)是為了用空間換取計(jì)算效率。而產(chǎn)品需求是變化的內(nèi)容永遠(yuǎn)往更多樣化的方向走。所以與其糾結(jié)“怎么讓滾輪支持不同高度”不如想清楚“滾輪效果是不是這個(gè)頁(yè)面必需的”。很多時(shí)候用戶(hù)需要的只是一個(gè)能上下滑動(dòng)、當(dāng)前項(xiàng)清晰可見(jiàn)的選擇控件而不是一個(gè)嚴(yán)格意義上的轉(zhuǎn)盤(pán)。如果哪天產(chǎn)品經(jīng)理說(shuō)你一定要保留 3D 滾輪效果那再去自定義布局也不遲——但請(qǐng)做好連續(xù)調(diào)參的準(zhǔn)備縮放比例、透明度漸變、遠(yuǎn)處 item 的最小尺寸每一個(gè)參數(shù)在高矮混合的列表里都要重新校準(zhǔn)。這一步?jīng)]有捷徑只能拿真機(jī)反復(fù)試。最后再分享一個(gè)小技巧無(wú)論用哪種方案一開(kāi)始就做一個(gè)“高度差異化調(diào)試頁(yè)”把所有極端情況最長(zhǎng)文本、最短文本、帶圖片、超長(zhǎng)說(shuō)明塞進(jìn)去跑一遍。等上線(xiàn)后才發(fā)現(xiàn)某個(gè)特殊內(nèi)容導(dǎo)致滾動(dòng)跳位那就真的晚了。