如何在 ListView 加入 Snap 效果

閱讀時間約 16 分鐘


raw-image

在 Flutter 中,ListView 和 PageView 都是用於顯示多個元素的 Widget,使用者可以滑動瀏覽列表中的 Widget。如果單看功能性,ListView 和 PageView 可能沒太大區別。但是實際與其互動之後,就會發現他們在畫面上的表現還是有所不同。

使用 ListView,使用者可以讓 ListView 停在列表中的任意位置,可以讓它停在某個元素的開頭,也可以停在某個元素一半的位置。

raw-image

而 PageView 就有點不同,雖然使用者一樣可以透過滑動來把 PageView 中的元素滑到任意位置,但是只要手一放開,PageView 就會自動的把元素歸位到正中央。

raw-image

大多數情況下,我們可以用 ListView 與 PageView 來完成功能。但是有些時候,我們也會需要客製化一些特別的行為,就像是今天要介紹的:用 ListView 來達到自動對齊的效果。

建立 SnapListView

首先我們需要一個可以水平滑動的 ListView 並先命名為 SnapListView。

class SnapListView extends StatelessWidget {
const SnapListView({super.key});

@override
Widget build(BuildContext context) {
const itemExtent = 200.0;
return ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Container(
width: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
),
child: Text('Item $index', style: const TextStyle(fontSize: 30)),
),
);
}
}

接下來我們要如何實現自動對齊的效果呢?答案就是修改 ListView 中的 physics 參數。physics 參數定義在 ListView 祖父類別 ScrollView 中,型別為 ScrollPhysics。

const ScrollView({
super.key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
ScrollPhysics? physics,
this.scrollBehavior,
this.shrinkWrap = false,
this.center,
this.anchor = 0.0,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
})

ScrollPhysics

Flutter 中原本就內建許多類別繼承自 ScrollPhysics ,例如:Android 預設的 ClampingScrollPhysics 或者是 iOS 預設 BouncingScrollPhysics 。這些 ScrollPhysics 可以套在很多可滑動的 Widget,例如:CustomeScrollView 或 GridView …等,套用不同的 ScrollPhysics 可在滑動的時候產生不同效果。

雖然 Flutter 提供許多不同的 ScrollPhysics,但是這些內建的 ScrollPhysics 並不能幫助我們的目標:自動對齊第一個元素。所以我們需要自定義一個 ScrollPhysics 來幫助我們達到目的。

建立 SnapScrollPhysics

讓我們新增一個 SnapScrollPhysics 類別,並繼承 ScrollPhysics 。我們傳入 itemExtent 來表明列表中每個元素的大小,用於計算模擬滑動的最終位置,畢竟使用者不會每次都停在相同位置,所以我們必須根據停止的位置與元素大小來計算最終位置。

class SnapScrollPhysics extends ScrollPhysics {
final double itemExtent;

const SnapScrollPhysics({
required this.itemExtent,
super.parent,
});

@override
SnapScrollPhysics applyTo(ScrollPhysics? ancestor) {
return SnapScrollPhysics(
itemDimension: itemExtent,
parent: buildParent(ancestor),
);
}

@override
Simulation? createBallisticSimulation(
ScrollMetrics position,
double velocity,
) {
return null;
}
}

在 ScrollPhysics 中有許多方法可提供覆寫。而在這個需求中,我們需要覆寫 createBallisticSimulation ,這個方法可以回傳一個 Simulation,讓使用者手指放開時,讓程式依據回傳的 Simulation,自動的把列表滾到 Simulation 所設定的位置。

值得一提的是,我們也需要覆寫 applyTo 方法,讓 ScrollView 在引入效果時,可以套用到我們定義的 SnapScrollPhysics 。雖然我們只在 ListView 中只傳入了 SnapScrollPhysics ,但是其實 Flutter 底層還會繼續套用其他的 ScrollPhysics ,讓 ListView 具有多種滑動效果。

實現 createBallisticSimulation

首先,我們先處理一些例外狀況,當使用者滑超出列表範圍時,我們呼叫 super 的 createBallisticSimulation 的方法即可,讓其他 ScrollPhysics 來處理超出列表的行為。

class SnapScrollPhysics extends ScrollPhysics {

@override
Simulation? createBallisticSimulation(
ScrollMetrics position,
double velocity,
) {
if (position.pixels <= position.minScrollExtent ||
position.pixels >= position.maxScrollExtent) {
return super.createBallisticSimulation(position, velocity);
}

return null;
}
}

接著我們透過 position.pixels 取得列表當前的位置,並使用 roundToDouble 四捨五入,來決定應該要自動移動到上一個元素或下一個元素。最後建立 ScrollSpringSimulation 並指定起始位置與目標位置,讓 Flutter 在使用者手放開之後,模擬使用者滑動,讓使用者體驗更好。

class SnapScrollPhysics extends ScrollPhysics {

@override
Simulation? createBallisticSimulation(
ScrollMetrics position,
double velocity,
) {
if (position.pixels <= position.minScrollExtent ||
position.pixels >= position.maxScrollExtent) {
return super.createBallisticSimulation(position, velocity);
}

double page = position.pixels / itemExtent;
double target = page.roundToDouble() * itemExtent;
if (target != position.pixels) {
return ScrollSpringSimulation(
spring,
position.pixels,
target,
velocity,
);
}
return null;
}
}

但是當我們實際執行後,卻會發現用起來十分不順,原因是當使用者一放開手,Flutter 就會馬上開始模擬滑動,造成滑動不順暢。在真實滑動的過程中,使用者的手指是會頻繁地離開的手機螢幕,不會一直貼在螢幕上,所以我們必須在使用者手離開螢幕時,依據使用者滑動方向與速率來微調一下目標位置。

class SnapScrollPhysics extends ScrollPhysics {

@override
Simulation? createBallisticSimulation(
ScrollMetrics position,
double velocity,
) {
if (position.pixels <= position.minScrollExtent ||
position.pixels >= position.maxScrollExtent) {
return super.createBallisticSimulation(position, velocity);
}

double page = position.pixels / itemExtent;

var tolerance = toleranceFor(position);
if (velocity < -tolerance.velocity) {
page -= 1;
} else if (velocity > tolerance.velocity) {
page += 1;
}

double target = page.roundToDouble() * itemExtent;
if (target != position.pixels) {
return ScrollSpringSimulation(
spring,
position.pixels,
target,
velocity,
);
}
return null;
}
}

當發現使用者滑動的速率大於容忍值時,表示使用者想要快速滑動。所以我們必須依照使用者快速滑動的方向,創建一個往使用者滑動方向的滑動模擬,避免使用者手指一離開,列表就往反方向滑動。最後我們把完成的 SnapScrollingPhysics 放回 ListView 中,就能得到一個比較順暢的自動對齊效果。

class SnapListView extends StatelessWidget {
const SnapListView({super.key});

@override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.horizontal,
physics: const SnapScrollPhysics(itemExtent: 200.0),
itemCount: 10,
itemBuilder: (context, index) => Container(
width: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
),
child: Text('Item $index', style: const TextStyle(fontSize: 30)),
),
);
}
}

raw-image

最後

雖然我們完成了自動對齊的效果,但其實程式碼還是有一些 Bug。例如:滑到最後一個元素時,列表不會像預期中的自動對齊,因為它採用了其他 ScrollPhysics 而非我們設定的 SnapScrollPhysics ,有興趣的觀眾可以嘗試修改看看。

除此之外,ScrollPhysics 還有許多方法可以覆寫,讓開發人員可以調整許多滑動細節,有興趣的觀眾也可以也可以參考 ScrollPhysics

分享各種 Flutter 開發技巧
留言0
查看全部
發表第一個留言支持創作者!
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
不論是平面設計、介面設計,都能見排版這一詞。而排版的作用,即在明確的設計目標中,將所有元素做有組織的安排,精確地傳達訊息。
Thumbnail
視覺層級並不侷限於平面設計,在用戶體驗及介面上更是一個重要的核心之一。視覺層級除了讓畫面的視覺編排更加精緻好看,更重要的功能是能讓畫面有效地被組織,讓觀者更容易理解。
Thumbnail
本文介紹了在After Effects中使用表達式為圖層添加延遲效果的方法,通過根據層的索引相對於其父層的索引來計算延遲時間,並應用到層的位置屬性上,可以創建連續的延遲效果,使每個層根據其索引值產生不同的延遲,形成鏈條效應。
Thumbnail
最近在嘗試使用不同的AI生圖方式混合出圖的方式,採用A平台的優點,並用B平台後製的手法截長補短,創造出自己更想要的小說場景,效果不錯,現在以這張圖為例,來講一下我的製作步驟。
這個系列的教學會列出基本上所有常見的造型和一些基礎 , 讓各位可以自行搭配造型~在這個第五篇呢 , 我們會來一起了解: 常見的按鈕樣式特性,怎麼使用他們,他們的作用是甚麼。讓我們一起了解如何自訂滑鼠以及使用常見的按鈕樣式特性,學會讓按鈕播放音效,切換滑鼠造型,以及避免圖片透明的地方被點擊到。
Thumbnail
wiggle是一個非常有用的表達式,用於給層的屬性添加隨機抖動效果。這個表達式特別適合於創建自然、隨機的運動,例如攝像機的手持效果、文字的抖動、或者任何需要不規則運動的場景。
Thumbnail
Ae 小技巧:宣紙噪點+抽幀效果 動態後記系列會記錄一些我在製作中的記錄,可能是分解動畫、小技巧、發想、腳本......等等。 每篇都是小短篇,就是補充用的小筆記,沒有前後順序,可跳著閱讀。
Thumbnail
這是一個簡單的工作流,可以對輸入的圖片進行3D重建
Thumbnail
前篇測試如何把提示詞生成的圖像細節提高,這篇要測試的工作流是把任意圖像載入後經由放大模型放大,同時測試放大後重繪看看效果如何。
Thumbnail
先前藉由加入提示詞控制畫面內容與品質後,發現圖像放大後細節感覺糊糊的,這篇就要來測試幾個增加細節的方法,測試使用的工作流是基於A1111算法的工作流,且使用固定種子。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
不論是平面設計、介面設計,都能見排版這一詞。而排版的作用,即在明確的設計目標中,將所有元素做有組織的安排,精確地傳達訊息。
Thumbnail
視覺層級並不侷限於平面設計,在用戶體驗及介面上更是一個重要的核心之一。視覺層級除了讓畫面的視覺編排更加精緻好看,更重要的功能是能讓畫面有效地被組織,讓觀者更容易理解。
Thumbnail
本文介紹了在After Effects中使用表達式為圖層添加延遲效果的方法,通過根據層的索引相對於其父層的索引來計算延遲時間,並應用到層的位置屬性上,可以創建連續的延遲效果,使每個層根據其索引值產生不同的延遲,形成鏈條效應。
Thumbnail
最近在嘗試使用不同的AI生圖方式混合出圖的方式,採用A平台的優點,並用B平台後製的手法截長補短,創造出自己更想要的小說場景,效果不錯,現在以這張圖為例,來講一下我的製作步驟。
這個系列的教學會列出基本上所有常見的造型和一些基礎 , 讓各位可以自行搭配造型~在這個第五篇呢 , 我們會來一起了解: 常見的按鈕樣式特性,怎麼使用他們,他們的作用是甚麼。讓我們一起了解如何自訂滑鼠以及使用常見的按鈕樣式特性,學會讓按鈕播放音效,切換滑鼠造型,以及避免圖片透明的地方被點擊到。
Thumbnail
wiggle是一個非常有用的表達式,用於給層的屬性添加隨機抖動效果。這個表達式特別適合於創建自然、隨機的運動,例如攝像機的手持效果、文字的抖動、或者任何需要不規則運動的場景。
Thumbnail
Ae 小技巧:宣紙噪點+抽幀效果 動態後記系列會記錄一些我在製作中的記錄,可能是分解動畫、小技巧、發想、腳本......等等。 每篇都是小短篇,就是補充用的小筆記,沒有前後順序,可跳著閱讀。
Thumbnail
這是一個簡單的工作流,可以對輸入的圖片進行3D重建
Thumbnail
前篇測試如何把提示詞生成的圖像細節提高,這篇要測試的工作流是把任意圖像載入後經由放大模型放大,同時測試放大後重繪看看效果如何。
Thumbnail
先前藉由加入提示詞控制畫面內容與品質後,發現圖像放大後細節感覺糊糊的,這篇就要來測試幾個增加細節的方法,測試使用的工作流是基於A1111算法的工作流,且使用固定種子。