扣丁書屋

Flutter滑動體驗對齊原生-滑動曲線篇

自從使用了Flutter以來,閑魚在享受著跨端帶來的提效的同時,流暢度一直是困擾了我們許久的問題,也是被外界吐槽得比較多的地方。所以我們在過去半年,重新牽起了流暢度優化這件事情,目標只有一個,那就是拉平Flutter和Native的滑動體驗 。

我們把這個目標拆分為了兩個:

  • 滑動曲線優化,拉齊手感
  • 渲染性能優化,減少掉幀

本文主要跟大家介紹一下我們在優化滑動曲線手感方面的一些經驗。和優化渲染單純的想辦法提升指標不同,滑動曲線感覺是否舒服因人而異,那么我們該如何去評判一個滑動曲線是否優秀呢?說實話我們一開始也沒有很好的答案。既然這樣,那我們的目標首先是需要把Flutter和Native的滑動曲線進行對齊,先找回習慣的感覺再考慮下一步的優化。

滑動對比

我們先使用platform_tests對比一下Flutter的滑動和Native的差異

(視頻的左側列表是Native,右側列表是Flutter)

Android

iOS

從視頻的表現中,我們可以看出來iOS的脫手滑動略有差異但還是基本符合預期的,Android的脫手滑動就比較糟糕了看起來明顯阻尼比Native大很多。

Android滑動曲線阻尼問題

找滑動曲線代碼的過程就不在這邊過多贅述了,如果在創建滑動容器(ListView/GridView/...)時沒有傳入特定的physics,則Flutter會根據平臺使用相應的ScrollPhysics(Android對應ClampingScrollPhysics,iOS對應BouncingScrollPhysics)。在發生Fling的時候會調用ScrollPhysics.createBallisticSimulation()構建一個Simulation,這個Simulation(ClampingScrollSimulation/BouncingScrollSimulation)就是最終控制脫手滑動動畫參數的類。

問題分析

在ClampingScrollSimulation構造函數中,會根據傳入的初速度,并結合一些小學二年級就學過的運動學公式計算出最終的距離以及滑動的總時間(因為沒找到確定性的資料,只能根據參數推測,這邊先不展開討論)

計算出總時長和總距離后,就可以根據當前時長的比例(動畫運行的時間/總時長),帶入公式計算出當前應該位移的距離,最后再加上初始位置即可得到最終Viewport的偏移量。

這個公式是對Android的滑動曲線進行擬合產生的,擬合過程可以看注釋,但是這個擬合并不完美。我們對這個d/t函數進行求導(即速度和時間的關系)可以發現,它的導數在0-1的區間內并不是單調遞減的,這意味著在滑動接近結束的時候會有一個速度增加的情況,體感上在Clamping曲線滑動動畫的末尾感覺上會有一個輕微的吸附感也說明了這一點。所以我們需要把Clamping的實現改一下,把Android的Scroller中計算滑動距離的代碼弄過來。

但是我們在上面的例子中,并沒有發現這個吸附的感覺,這是不是說明了這條曲線根本就沒能完整地跑完呢?我們可以進一步加一些Log看一下。我們在double x(double time)方法中把時間和position打出來,發現了一個問題,time一直在清零,position也一直在變。這說明了這個動畫一直在被重新啟動,我們簡單改一下代碼,強行讓動畫不要重啟,使用同樣的ADB指令進行滑動,通過Log對比可以發現這確實會導致滑動速度更快地衰減。

那么我們就把問題拆分成了兩個:

  • 解決發生Layout時的動畫重啟問題
  • Clamping的實現方式對齊Android的Scroller

問題解決

  • 動畫重啟問題

我們在ClampingScrollSimulation的構造函數中斷點,可以發現當Fling過程中如果Item高度發生改變,則會觸發RenderViewport.performLayout(),從而觸發ScrollPositionWithSingleContext.goBallistic(),這個方法會以當前的狀態為起點重新啟動fling動畫,這顯然是不合理的,特別是當前滑動和邊界無關的時候沒必要因為布局改變而重啟動畫,這會導致一次Fling動畫無法完整做完,移動的軌跡自然也就無法貼合預期中設計好的d/t曲線。

這個問題比較明確,所以解決問題的思路也很明確,就是在Fling的過程中不要重啟動畫了,而是去更新一些相關的變量,使得動畫能夠合理的繼續完成。一開始我是希望更新Simulation中和邊界有關的的相關參數,但是因為這個方案有個始終繞不過去的類型檢查(https://github.com/flutter/flutter/pull/96512)Flutter團隊的同學認為不能通過。所以誕生了下面的新方案(https://github.com/flutter/flutter/pull/100133)

首先解決更新時機的問題,當RenderViewport.performLayout()被調用的時候,會回調ScrollActivity.applyNewDimensions,在慣性滑動的過程中的ScrollActivity是BallisticScrollActivity

之前提到的ScrollPositionWithSingleContext.goBallistic()也就是在這個方法中被調用,所以我們在這邊做一下修改,讓其不再調用goBallistic(),而是調用updateBallisticAnimation()生成一個新的Simulation,并將其更新到AnimationController中。

updateBallisticAnimation()中,我們還是使用createBallisticSimulation()來創建Simulation。這里重要的一點是,因為我們動畫時間沒有清零,所以我們創建Simulation的時候一定要用初始的ScrollMetrics和Velocity來創建。因為布局變化有可能會帶來相應的邊界變化,所以這里只將相應的ScrollExtent(也就是邊界值)更新為最新的值即可。

Android滑動曲線對齊

做完上述的操作后,我們看到了熟悉的吸附的效果,但這并不是我們想要的。為了徹底對齊與Native的體驗,擬合曲線肯定是滿足不了我們的,下一步我們需要將Clamping的公式徹底換成Android的Scroller.java中的實現。這時候有的小朋友可能要問了:“都要換掉它了,那我們剛才費這勁去修復它干嘛呢?”

翻譯Scroller.java的代碼到dart這個工作并不難,并且這部分工作其實有一位Google的同學已經做了(https://github.com/flutter/flutter/pull/77497),但是這個PR在合入后不久就被Revert了,被Revert的原因是在某些場景下會導致滑動停不下來。是的,就是因為上面提到的動畫重啟的問題,導致了這個滑動無法停止。所以我會在動畫重啟的PR被合入后,Reland這個Android曲線的PR?,F在我們先來看看最后的效果,不能說是十分相似只能說是一摸一樣了。

滑動速度引發的問題

我們把曲線修改完成后開開心心地在搜索場景上線了,但是沒過多久就傳來噩耗,產品和交互同學一致覺得我們的滑動比以前快了很多,會影響到用戶體驗。

但是讓我把曲線調回到那滑不動的樣子,我是拒絕的。難道就沒有一個方法能讓用戶在用的時候想仔細看的時候慢想滑走的時候快嗎?算了,我們先想想看怎么讓他慢下來吧。

如何合理的給曲線減速

Android和iOS兩條曲線,這么多的參數,到底該把哪個調低在能在不影響滑動曲線的整體形態的同時降低體感速度呢?在很久以前Flutter其實是給iOS做過一個減速的,在BouncingScrollPhysics創建BouncingScrollSimulation時,給初速度乘了0.91。我們最初使用的曲線也是這個減速版本的,所以切換到正常的曲線后才會顯得比較快。這個0.91在一次PR中被刪除了(https://github.com/flutter/flutter/pull/59623)原因是導致了iOS曲線阻尼過大,但是實際上背后的原因,其實是上面提到的動畫重啟問題,因為動畫重啟導致速度多次乘了0.91,才使得滑動速度加速衰減。那么現在問題解決了,我們是不是也可以嘗試通過這種方式對曲線進行減速。iOS的滑動公式比較簡單,我們就通過iOS來分析,Android也是同樣的道理。

先把初速度衰減后的曲線畫出來看看??梢园l現其實整體曲線的形態是沒有發生變化的,只是滑動的距離減少了,這樣就顯得比較慢了。所以給初速度增加衰減值這個方案,是比較符合我們的預期的。

如何讓滑動能快能慢

減速的問題解決了,接下來該思考一下想快的時候該怎么快起來。那么用戶怎么樣的一個行為才表示他現在想快點滑呢?我認為滑動速度其實一定程度上反映了用戶的意愿,當用戶滑動速度快的時候,天然就表示了他希望能快點滑走。順著這個思路,如果我們給初速度的衰減值再增加一個衰減值(套起來了)讓他在速度慢的時候衰減值大,速度快的時候衰減值小,就可以解決這個問題了。

這時候,靈光一現,突然想到了一個小學二年級就學過的拋物線y=ax^2+bx+c,中間低兩邊高恰好滿足我的訴求,當用戶滑動得很慢或很快的時候不增加衰減,當用戶在中速滑動的時候給出最大衰減。為了方便在線上做實驗以及數據分析,我們盡量把參數縮減到一個,我選擇了對稱的圖形(強迫癥),所以必過兩個點(0,1)和(1,1),并且頂點的x坐標也確定為了0.5,剩下一個頂點y坐標,初中學的頂點式就不多說了吧,直接上代碼。

最終效果

最終滑動的效果符合我們的預期,上線進行了分桶實驗,我們得出了一個最佳實踐的衰減頂點值在0.7左右,效果大概如視頻所示。調整曲線的按鈕是臨時增加的,切換不同曲線并且使用同一個adb命令進行滑動,可以看出來使用新的曲線在快速滑動的情況下會比之前的曲線多滑動一到兩屏。

總結和展望

本文給大家詳細介紹了閑魚優化滑動曲線手感的方案,雖然過程中遇到了不少問題,我們還算圓滿地完成了任務,將Flutter的滑動手感對齊到了原生,并且根據業務場景進行了曲線速度優化。如果大家需要對曲線進行優化可以提前合入以下兩個PR,上文提到的減速方案也可以根據業務情況酌情使用。

  • 修復動畫重啟問題:https://github.com/flutter/flutter/pull/100133
  • Android曲線復刻:https://github.com/flutter/flutter/pull/77497

后續我們會在此基礎上繼續進行優化,包括但不限于以下幾點:

  1. 推進本文提到的幾個PR的合入
  2. 結合業務數據并配合交互設計師,對滑動手感進行更精細的控制
  3. 在滑動的過程中根據速度的不同,結合圖片庫進行圖片加載控制,以提升滑動流暢度

當然了最后一點更偏向于性能優化,下一篇文章也會給大家介紹一下這段時間所做的流暢度相關的性能優化,敬請期待(一定不咕)。


https://mp.weixin.qq.com/s/aqXdYYEArhEj6ikqg2oX9g

最多閱讀

如何有效定位Flutter內存問題? 1年以前  |  11676次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  7605次閱讀
Flutter插件詳解及其發布插件 2年以前  |  6293次閱讀
在Flutter中添加資源和圖片 3年以前  |  5144次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  4341次閱讀
發布Flutter開發的iOS程序 3年以前  |  4317次閱讀
Flutter for Web詳細介紹 3年以前  |  4190次閱讀
在Flutter中發起HTTP網絡請求 3年以前  |  3953次閱讀
使用Inspector檢查用戶界面 3年以前  |  3885次閱讀
Flutter Widget框架概述 3年以前  |  3261次閱讀
Flutter路由詳解 3年以前  |  3103次閱讀
JSON和序列化 3年以前  |  3026次閱讀
Flutter框架概覽 3年以前  |  2990次閱讀
推薦5個Flutter重磅開源項目! 1年以前  |  2951次閱讀
為Flutter應用程序添加交互 3年以前  |  2941次閱讀
處理文本輸入 3年以前  |  2844次閱讀
使用自定義字體 3年以前  |  2843次閱讀
編寫國際化Flutter App 3年以前  |  2831次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>