昨天我們終於把主頁的焦點新聞頁面給完成拉,不過目前這些新聞卡片都只能顯示片面的資訊,理論上應該要有個專門用來閱讀新聞的頁面,如下圖:
在 Cupertino UI 庫中提供了 CupertinoPopupSurface
這個工具,可用於創建浮動視窗。其中我們常用的選擇器、時間選擇器等都是使用其作為基礎的。
我們來看看要怎麼使用吧
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) {
return CupertinoPopupSurface(
child: // 欲顯示於該視窗的內容
)
}
)
透過呼叫此函式,便可成功的呼叫出浮動視窗。剩下的就把內容全數的交給 child
來負責,請在 screens
底下建立一個 article_screen.dart
檔案,並參考下列程式碼,我們先來看看效果:
class PostScreen extends StatelessWidget {
// 由於是用於顯示文章,該文章必定是從外部傳入
final NewsPost post;
const PostScreen({super.key, required this.post});
@override
Widget build(BuildContext context) {
return SizedBox(
// 建立一個高度為螢幕 80% 的 widget
height: MediaQuery.of(context).size.height * 0.8,
child: Center(
// 置中顯示新聞標題
child: Text(post.title),
),
);
}
}
接下來要觸發點按的行為來顯示浮動視窗,這個技巧我們前面有使用過,也就是將最外層的 widget 再包一層 CupertinoButton
,透過 onPressed
參數來指定點擊觸發的事件。
// 新聞卡片 widget
return CupertinoButton(
onPressed: () {
// 當點擊時,觸發此函式
showCupertinoModalPopup(
context: context,
// 設定視窗彈出時,視窗外的顏色
barrierColor: const Color.fromRGBO(0, 0, 0, 0.7),
builder: (BuildContext builder) {
// 回傳彈出式視窗,並且該視窗中便是方才建立的文章 widget
return const CupertinoPopupSurface(child: PostScreen(post: post));
});
},
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Container( ... 省略 ... )
)
讓我們來看看效果:
看起來挺不錯的,剩下的步驟就是要PostScreen
弄的接近設計稿的風格拉!
首先是畫面佈局,分為上下兩部分:
1. 圖片區塊:用於疊加顯示新聞圖片、標題、分類
2. 文字區塊:顯示新聞來源、內文
很明顯要使用 Column
將上下兩者分開,我們分開來講設計上需要注意的小細節:
可以先參考下列程式碼,並請觀看成果
// 圖片顯示區域
Container(
// 圖片高度設為螢幕高度的 30%
height: MediaQuery.of(context).size.height * 0.3,
width: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(image: NetworkImage(post.cover), fit: BoxFit.cover)
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 新聞分類
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: CupertinoColors.systemBlue,
borderRadius: BorderRadius.circular(14)),
child: Text(post.category,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: CupertinoColors.white))),
const SizedBox(height: 8),
// 標題
Text(post.title,
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 22,
fontWeight: FontWeight.w700)),
]),
)),
我們使用 Container
這個 widget 成功的將新聞圖片與標題疊加在一起,新聞標題為求顯眼因此設為白色。如下圖:
看起來標題在深色背景下效果挺不錯的,不過在淺色背景下就很不顯眼。難道我還要根據背景的顏色來決定新聞標題的顏色嗎?
其實不用,有個簡單的小技巧在我們前面製作「新聞分類」按鈕的時候就已經使用過,不過當時我們並未多提。其實只要將 Container
的背景設為黑底,圖片設定成半透明的樣子就能模擬出圖片蓋上一層黑色遮罩的樣子,請稍微修改一下程式:
Column(children: [
Container(
height: MediaQuery.of(context).size.height * 0.3,
width: double.infinity,
decoration: BoxDecoration(
// 將 Container 設為黑底
color: CupertinoColors.black,
image: DecorationImage(
// 調整圖片透明度為 70 %
opacity: 0.7,
image: NetworkImage(post.cover),
fit: BoxFit.cover,
child: // 省略
)),
如此一來在淺色背景下,因為蓋上了一層黑色遮罩使得白色標題變的更加明顯;深色背景下,則因色系相近所以也不影響顯示效果。
這麼一來圖片的區塊就完成囉!
文字區塊需要注意的點為當顯示文章的內容過長時,需要設置為可滾動的頁面,否則會跳出 overflow 的警告,此部份可使用 SingleChildScrollView
達到效果。在此專案我們提供的新聞內文都較短,因此可以把外部 Container 高度設定從螢幕高度 80% 減少成 50% 試試。
讓我們再回過頭看看整個畫面的結構,
Container
來包裹,並使用 Column
設定佈局Column
可使用的顯示區域受到了外部 Container
的限制,也就是整體螢幕高度的 80%constraint
,也就是使用 Container
設定圖片區塊高度為螢幕高度的 30%SingleChildScrollView
並預期可以達成滾動效果,然而你會發現警告依舊存在。原因就是 SingleChildScrollView
並不知道自己可使用的最大高度為何,也因此無法限縮可滾動區域的範圍。如果看到這裡你還有點不明白問題出在哪,這裡提供一個 Flutter 官方釋出的影片,或許看過影片的你就能更明白。
所以解決方法就是限縮 SingleChildScrollView
的高度,方式如下:
SizedBox
或是 Container
包裹,並設定其高度Expanded
工具,可以用於填滿在 Column
、Row
或是 Flex
的剩餘空間所以整個頁面的程式碼架構就會變成如下:
Column(
children: [
// 圖片區塊
Container( ... ),
// 填滿剩餘 Column 可顯示區域
Expanded(
// 告訴 Single ChildScrollView 最大高度,一但顯示範圍超過最大高度,則進行捲動
child: SingleChildScrollView(
child: ...
)
)
]
)
各位可以仔細思考看看上述的內容,一開始寫遇到一堆錯誤或是警告真的都很正常 XD 但只要多想一下得到可能引發的原因,會讓你的 debug 能力大大提升。我會將程式碼同樣分享於文末的連結。
這裡先曬一下成果~相當讚!
到這裡,我們已經將大部分與新聞相關的部分都已經做完了,包括顯示、剩下的篇幅讓我們來補充一些小功能,因為篇幅較短就一併在這裡一起講拉
製作新聞應用程式最需要的就是即時性,就拿台灣為例,小小一個國家就有著上百上千家的媒體,新聞量一定相當可觀。因此,很可能使用者在瀏覽主頁時看個五分鐘後,又有更多新聞出現。不過按照我們目前主頁頁面的寫法,必須要重新打開應用程式才能觸發更新,通常主流的做法會提供於頁面最上方下拉式更新的功能。請打開 home_screen.dart
讓我們來實作吧!
在 Cupertino 中提供了一個工具 CupertinoSliverRefreshControl
便是用於達成此操作,在更新時會顯示讀取動畫,並於更新完成時隱藏。讓我們看看其類別定義:
const CupertinoSliverRefreshControl({
super.key,
this.refreshTriggerPullDistance = _defaultRefreshTriggerPullDistance,
this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent,
this.builder = buildRefreshIndicator,
this.onRefresh, // 更新時執行的操作
})
因此我們需要在 onRefresh
中重置整個主頁的 state 狀態,以及重新獲取第一頁的新聞資訊。如下:
CupertinoSliverRefreshControl(onRefresh: () async {
// 重新獲取第一頁的新聞文章
final firstPagePosts = await NewsPostRepository().getPosts(page: 1, limit: limit);
// 重設 state 至初始化狀態
setState(() {
page = 1;
_posts = firstPagePosts;
firstPagePosts.length < limit ? isBottom = true : isBottom = false;
});
}),
接下來就是將其放置於合適的位置,如下:
CustomScrollView(
// 以上省略
slivers: [
// 頂端導覽列
CupertinoSliverNavigationBar(),
// 放置於此
CupertinoSliverRefreshControll(),
// 顯示焦點新聞標題
SliverPadding(),
// 新聞卡片放置區
// 省略
]
)
顯示效果如下:
今天我們完成了閱讀新聞的彈出式視窗、下拉式更新的功能,從實作的過程中,也了解到要多方考慮到各種使用情境,才能做出通用的應用程式。譬如上面遇到深、淺色背景顯示標題以及滾動式顯示內文等等。
目前我們僅串接新聞閱讀視窗於主頁的新聞圖卡,至於搜尋新聞頁面的新聞卡片就交給各位自行練習串接。
明天我們會開始進行個人檔案頁面的製作,就只剩下最後一步拉!不過我們還可以延伸很多功能,讓我們的應用程式功能更加豐富,最後的 10 天盡請期待😊
今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day20/micro_news_app