什麼是骨架屏
在客戶端開發中,我們總是需要等待拿到服務端的響應後,再將內容呈現到頁面上,那麼在使用者發起請求到客戶端成功拿到響應的這段時間內,應該在螢幕上呈現點什麼好呢?
答案是:骨架屏
那麼什麼是骨架屏呢,來問下 GPT:
骨架屏(Skeleton Screen)是一種現代的使用者介面設計技術,用於提升應用或網站在載入內容時的使用者體驗。在內容的完全載入和呈現之前,骨架屏提供了一種模糊的預覽,形似最終內容的空白版,通常用灰色的塊、線條或元素佔位符表示。這種設計方法可以有效減少使用者的感知等待時間,增強使用者的互動感。
功能和用途
提高感知效能:骨架屏透過立即顯示頁面的基本結構(而非旋轉的載入圖示或完全空白的螢幕),給使用者一種內容即將呈現的感覺,這可以使等待時間感覺上更短。
改善使用者體驗:使用骨架屏可以減少使用者在載入過程中的焦慮,使用者看到介面元素已經在逐步載入,會有更多的耐心等待最終內容的呈現。
提供內容載入的視覺提示:骨架屏體現了頁面內容載入的進度,可以讓使用者知道哪些內容即將出現,這樣使用者就不會感到突然或困惑。
實現方式
骨架屏的實現通常包括以下幾個步驟:
設計:設計與最終內容佈局相似的基本框架,使用灰色或淺色塊代表將要載入的各種元素,如文字行、圖片、按鈕等。
前端實現:在前端程式碼中,可以使用HTML和CSS來建立這些佔位符。對於複雜的動態載入內容,可以使用JavaScript或前端框架如React、Vue等來動態控制骨架屏的顯示和隱藏。
資料載入後的處理:一旦相應的資料載入完成,骨架屏應被實際內容替換。這通常涉及到監聽資料載入的完成事件,然後更新UI。
示例
在一個簡單的網頁應用中,如果你正在載入一個包含標題、幾段文字和圖片的文章,骨架屏可能包括:
- 一個灰色的矩形塊預留給圖片。
- 幾個灰色的條形預留給文字標題和段落。
隨著實際內容的逐漸載入到瀏覽器中,這些灰色佔位符將被實際的圖片和文字內容替換。
結論
骨架屏是一種非常有效的使用者介面技術,尤其適用於網路速度較慢或資料處理較慢的應用場景,能顯著提升使用者的等待體驗和整體滿意度。透過合理設計和實現,開發者可以利用骨架屏減少使用者流失,提升應用的專業感和友好感。
我們要實現的效果
上面是一個 內容展示的卡片,下面的是 載入中狀態的該卡片的骨架圖
如何實現呢?
1. 先定義卡片部分 ui 程式碼
一個 Column
中有三行元素,分別是 第一行: 圖片 、第二行: 卡片標題 、第三行: 頭像 暱稱 瀏覽量
class StarCard extends StatelessWidget {
const StarCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 180,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: Column(
children: [
SizedBox(
width: 180,
child: AspectRatio(
aspectRatio: 9 / 11,
child: Image.network('https://pic1.zhimg.com/80/v2-fc35089cfe6c50f97324c98f963930c9_720w.jpg', fit: BoxFit.cover),
),
),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'魔法少女李知恩!!!',
style: TextStyle(fontSize: 15, color: Colors.black, fontWeight: FontWeight.w500),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8),
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
'https://pic1.zhimg.com/80/v2-1956eeb2c894f1785362411aa306f882_1440w.webp?source=1def8aca',
height: 20,
width: 20,
fit: BoxFit.cover,
),
),
SizedBox(width: 4),
Text('是 IU 吖', style: TextStyle(fontSize: 12, color: const Color(0xff86909C))),
const Spacer(),
Text('21 瀏覽', style: TextStyle(fontSize: 12, color: const Color(0xff86909C))),
],
),
],
),
),
],
),
);
}
}
2. 引入 shimmer
包
在 pubspec.yaml
中加入
dependencies:
shimmer: ^3.0.0
- 該 package 的文件在這: https://pub.dev/packages/shimmer
其作用是為我們的元素加上 閃動 流光 效果,類似於一道光照射到一把光滑的寶劍上,隨著寶劍角度發生變化 光的反射發生位移的現象
我們使用它的 fromColors
構造器,先隨便放進去一個 Container
試試效果:
Shimmer.fromColors(
baseColor: Colors.orange,
highlightColor: Colors.blue,
child: Container(
color: Colors.white,
height: 180,
width: 180,
),
)
效果還不錯,就是配色有點醜
調整下顏色,在用來包裹下我們剛剛定義的 StarCard
:
Shimmer.fromColors(
baseColor: Colors.grey[300]!, // 骨架基色
highlightColor: Colors.grey[100]!, // 骨架高亮色
child: StarCard(),
),
誒?怎麼跟我們要實現的效果有點出入?
這是因為 StarCard
的根元件是一個帶有顏色的 Container
return Container(
width: 180,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
...
);
而這樣 Shimmer 效果便會被加到整個跟元件上,child
也就看不到 Shimmer 了。那我們將根元件的 color
屬性移除試試呢:
嗯...... 底層的元素確實展示出來了,不過我們所期望的並不是展示出文字和資料啊,況且這個時候我們還尚未拿到伺服器返回給我的的資料
3. Magic symbol
這個時候我們就需要用到一個 魔法符號 ,不過在引入之前,先分離下元件:
StarCard
需要一個建構函式,裡面接收一個從服務端 反序列化 來的 model ;再新定義一個 StarCardSkeleton
元件,他有一個無參構造器,用作 StarCard
的骨架圖;也就是說在獲取到資料之前,我們使用一個 StarCardSkeleton
來佔 StarCard
的位,獲取到資料之後使用 StarCard
來展示真實的資料,程式碼如下:
class StarCard extends StatelessWidget {
const StarCard({super.key, required this.starModel});
final StarModel starModel;
@override
Widget build(BuildContext context) {...}
}
class StarCardSkeleton extends StatelessWidget {
const StarCardSkeleton({super.key});
@override
Widget build(BuildContext context) {...}
}
現在該回歸正題了,我們要引入的 魔法符號 就是:
▆
這是一個 全寬 純色 佔位符,數個 ▆
連起來,配合加粗 fontWeight
可以實現我們想要的 一道長條色塊 的效果,且要比定義 SizedBox
少改動更多程式碼,用它來代替Shimmer 文字再合適不過了
我們先將 StarCard
build 中的程式碼全部複製到 StarCardSkeleton
裡面,並改動 卡片標題 、使用者暱稱 、瀏覽量 Text
中的文字為自定義數量的 ▆
class StarCardSkeleton extends StatelessWidget {
const StarCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 180,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
// color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: Column(
children: [
SizedBox(
width: 180,
child: AspectRatio(
aspectRatio: 9 / 11,
child: Container(color: Colors.white),
),
),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'▆▆▆▆▆▆',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w900),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8),
Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white),
),
SizedBox(width: 4),
// NickNameText(articleData.nickName, views: articleData.playTimes),
Text('▆▆▆', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w900)),
const Spacer(),
Text('▆▆', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w900)),
],
),
],
),
),
],
),
);
}
}
如果想進一步最佳化渲染效能,可以把 Image
換成帶背景色的 Container
再看下效果呢
完美!簡直一模一樣!
抽離出 Simmer 元件
為了方便元件複用,可以將 Shimmer 封裝出來
/// 骨架屏閃爍
class BaseShimmer extends StatelessWidget {
const BaseShimmer({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!, // 骨架基色
highlightColor: Colors.grey[100]!, // 骨架高亮色
child: child,
);
}
}
再次用到可以直接:BaseShimmer(child: StarCardSkeleton())
擴充
這章的標題是 骨架屏 ,為什麼從頭到尾一直再講怎麼生成一個骨架圖呢?
先別急罵標題黨!
所謂的 骨架屏 不就是一張一張的骨架圖拼出一個螢幕,不就是一個骨架屏 嘛
BaseShimmer(
child: MasonryGridView.count(
padding: EdgeInsets.only(bottom: 10, left: 12, right: 12, top: 8),
physics: const NeverScrollableScrollPhysics(),
itemCount: 9,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
crossAxisCount: 2,
itemBuilder: (BuildContext context, int index) {
return StarCardSkeleton();
}),
)
這裡用了 flutter_staggered_grid_view
包,這個包還可以做出卡片高度不一的瀑布流佈局效果
注:使用 BaseShimmer 包裹整個
GridView
或ListView
比包裹單個的Card
效果要更好喲~
風險
經過測試發現 在不同的裝置上、或者使用了自定義字型,▆▆▆
之間會出現微小間距,無論將 fontWeight
設定為多大都無法避免,這時只能將 ▆
方案換為帶顏色的 Container
來解決。不過 SkeletonScreen
在頁面停留的時間通常不會太長,這點就要看團隊內部的取捨了