iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Mobile Development

Flutter 從零到實戰 - 30 天の學習筆記系列 第 20

[Day20] 實戰新聞 APP - 使用彈出式視窗來顯示新聞吧 (CupertinoPopupSurface)

  • 分享至 

  • xImage
  •  

昨天我們終於把主頁的焦點新聞頁面給完成拉,不過目前這些新聞卡片都只能顯示片面的資訊,理論上應該要有個專門用來閱讀新聞的頁面,如下圖:
https://ithelp.ithome.com.tw/upload/images/20231005/20135082Z3hIqxwT7i.png
在 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( ... 省略 ... )
)

讓我們來看看效果:
https://ithelp.ithome.com.tw/upload/images/20231005/20135082gpEN7fQaKk.png
看起來挺不錯的,剩下的步驟就是要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 成功的將新聞圖片與標題疊加在一起,新聞標題為求顯眼因此設為白色。如下圖:
https://ithelp.ithome.com.tw/upload/images/20231005/20135082U5z2EKNTyD.png
看起來標題在深色背景下效果挺不錯的,不過在淺色背景下就很不顯眼。難道我還要根據背景的顏色來決定新聞標題的顏色嗎?

其實不用,有個簡單的小技巧在我們前面製作「新聞分類」按鈕的時候就已經使用過,不過當時我們並未多提。其實只要將 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: // 省略
)),

如此一來在淺色背景下,因為蓋上了一層黑色遮罩使得白色標題變的更加明顯;深色背景下,則因色系相近所以也不影響顯示效果。
https://ithelp.ithome.com.tw/upload/images/20231005/201350826Ag57Kn0qf.png
這麼一來圖片的區塊就完成囉!

文字區塊

文字區塊需要注意的點為當顯示文章的內容過長時,需要設置為可滾動的頁面,否則會跳出 overflow 的警告,此部份可使用 SingleChildScrollView 達到效果。在此專案我們提供的新聞內文都較短,因此可以把外部 Container 高度設定從螢幕高度 80% 減少成 50% 試試。

讓我們再回過頭看看整個畫面的結構,

  1. 整體的新聞視窗是使用Container 來包裹,並使用 Column 設定佈局
  2. Column 可使用的顯示區域受到了外部 Container 的限制,也就是整體螢幕高度的 80%
  3. 圖片區塊我們已設定了 constraint ,也就是使用 Container 設定圖片區塊高度為螢幕高度的 30%
  4. 文字區塊我們加入了 SingleChildScrollView 並預期可以達成滾動效果,然而你會發現警告依舊存在。原因就是 SingleChildScrollView 並不知道自己可使用的最大高度為何,也因此無法限縮可滾動區域的範圍。

如果看到這裡你還有點不明白問題出在哪,這裡提供一個 Flutter 官方釋出的影片,或許看過影片的你就能更明白。
Yes
所以解決方法就是限縮 SingleChildScrollView 的高度,方式如下:

  1. 使用 SizedBox 或是 Container 包裹,並設定其高度
  2. 使用 Expanded 工具,可以用於填滿在 ColumnRow或是 Flex 的剩餘空間

所以整個頁面的程式碼架構就會變成如下:

Column(
  children: [
    // 圖片區塊
    Container( ... ),
    // 填滿剩餘 Column 可顯示區域
    Expanded(
      // 告訴 Single ChildScrollView 最大高度,一但顯示範圍超過最大高度,則進行捲動
      child: SingleChildScrollView(
        child: ...
      )
    )
  ]
)

各位可以仔細思考看看上述的內容,一開始寫遇到一堆錯誤或是警告真的都很正常 XD 但只要多想一下得到可能引發的原因,會讓你的 debug 能力大大提升。我會將程式碼同樣分享於文末的連結。

這裡先曬一下成果~相當讚!
https://ithelp.ithome.com.tw/upload/images/20231005/201350823RYpcGX5iT.png
到這裡,我們已經將大部分與新聞相關的部分都已經做完了,包括顯示、剩下的篇幅讓我們來補充一些小功能,因為篇幅較短就一併在這裡一起講拉

下拉式更新

製作新聞應用程式最需要的就是即時性,就拿台灣為例,小小一個國家就有著上百上千家的媒體,新聞量一定相當可觀。因此,很可能使用者在瀏覽主頁時看個五分鐘後,又有更多新聞出現。不過按照我們目前主頁頁面的寫法,必須要重新打開應用程式才能觸發更新,通常主流的做法會提供於頁面最上方下拉式更新的功能。請打開 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(),
    // 新聞卡片放置區
    // 省略
  ]
)

顯示效果如下:
https://i.imgur.com/O37DiYU.gif

今日總結

今天我們完成了閱讀新聞的彈出式視窗、下拉式更新的功能,從實作的過程中,也了解到要多方考慮到各種使用情境,才能做出通用的應用程式。譬如上面遇到深、淺色背景顯示標題以及滾動式顯示內文等等。

目前我們僅串接新聞閱讀視窗於主頁的新聞圖卡,至於搜尋新聞頁面的新聞卡片就交給各位自行練習串接。

明天我們會開始進行個人檔案頁面的製作,就只剩下最後一步拉!不過我們還可以延伸很多功能,讓我們的應用程式功能更加豐富,最後的 10 天盡請期待😊

今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day20/micro_news_app


上一篇
[Day 19] 實戰新聞 APP - 無限捲軸
下一篇
[Day 21] 實戰新聞 APP - 認識 CupertinoListSection 與 CupertinoListTile
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言