iT邦幫忙

2022 iThome 鐵人賽

DAY 12
0
Mobile Development

通徹 Flutter 學習路徑系列 第 12

通徹 Flutter 學習路徑 Day 12 - 來仿造一個 Messenger 吧!(2)

  • 分享至 

  • xImage
  •  

前言

今天主要是來分享目前 Messenger UI 克隆計畫的進度
目前仍然是著重 UI 的呈現上
雖然許多細節還有很多不同就是了 Orz
但主要想呈現出來的畫面效果還可以接受
今天就著重分享我完成了哪些效果吧!

  • 完成大綱畫面 (2/3)

    • 聊天室(x)
    • 用戶(x)
    • 設定
  • 重構程式碼 (x) <- 這個叉應該是整個專案進行中都要持續存在的

  • 透過 BottomNavigationBar 進行 Navigator(x)

  • 透過 GridView 來達到平均切割畫面的效果 (x)

  • 透過 Stack 來進行畫面堆疊 (x)

  • 使用套件 username_gen 來產生隨機的英文名字 (x)

  • 實作隨機顏色產生 (x)


完整程式碼

首先讓我們先重構前天的程式碼~
建立 HomeScreen.dart
裡頭主要將後面用到的 Scafford 提到這一層來做呈現
並且透過 BottomNavigatorBar 進行畫面切換~

這邊可以注意到有些變數名稱是用 _ 來命名
原因是 Dart 不像其他具備物件導向特性的語言(ex. C++、C#、Java...)有 publicprivate等關鍵字
而在 Dart 則透過在變數或方法前面加上 _ 來表示其為私有變數或方法

// 篇幅的關係有刪除某些內容,完整請查閱原始碼
class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  late int _currentIndex = 0;
  late String _title = title(0);
  late List<Widget> _actions = [];
  late Widget? _leading;

  @override
  void initState() {
    super.initState();
    setState(() {
      _title = title(_currentIndex);
      _actions = actions(_currentIndex);
      _leading = leading(_currentIndex);
    });
  }

  void _onItemTapped(int index) {
    setState(() {
      _currentIndex = index;
      _title = title(_currentIndex);
      _actions = actions(_currentIndex);
      _leading = leading(_currentIndex);
    });
  }

  // Get body's content
  Widget content(int index) {
    late Widget result;
    switch (index) {
      case 0:
        result = const ChatRoomScreen();
        break;
      case 1:
        result = const UsersScreen();
        break;
      case 2:
        result = const SettingScreen();
        break;
      default:
        throw Exception("No Page in this index.");
    }

    return result;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(40),
        child: AppBar(
          leading: _leading,
          title: Text(_title),
          actions: _actions,
        ),
      ),
      body: content(_currentIndex),
      bottomNavigationBar: BottomNavigationBar(
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.message), label: "聊天室"),
          BottomNavigationBarItem(icon: Icon(Icons.people), label: "用戶"),
          BottomNavigationBarItem(
              icon: CircleAvatar(
                radius: 15,
                backgroundImage: NetworkImage(
                    "https://pbs.twimg.com/profile_images/1187814172307800064/MhnwJbxw_400x400.jpg"),
              ),
              label: "設定"),
        ],
        currentIndex: _currentIndex,
        onTap: _onItemTapped,
      ),
    );
  }
}

在上面完成畫面切換後
讓我們來具體實作 UsersScreen 吧~
這部分用到的材料(Widgets)有

  1. DefaultTabController
  2. TabBar
  3. Tab
  4. GridView
  5. SliverGridDelegateWithFixedCrossAxisCount
  6. Stack
  7. ClipRRect
  8. ElevatedButton
  9. Spacer

首先在實作的時,我們先從大項開始完成
透過 DefaultTabController 我們首先完成了用戶限時動態以及線上用戶兩個畫面的切換
此時需要配合 TabBar 以及 Tab 來完成
前者用來設計 Bar 上面的文字、顏色、字體... 等相關參數
後者則是可以用來呈現切換後的畫面
在這邊我們透過宣告回傳值為 Widget 的兩個函式 story() 以及 onlineUsers() 來完成這兩個畫面

在 story 中
我們透過 GridView 來將畫面分割出來
此時相關 間隔幾個一組 等參數則是透過 SliverGridDelegateWithFixedCrossAxisCount 來完成
而接下來我這邊透過 Stack 來完成畫面上的堆疊效果
最底下我們用 ClipRRect 來產生一個具備圓弧邊框的圖片
而在這之上我們加上了三個 Widgets
分別是 ElevatedButtonSpacer 以及 Text
Spacer 在這邊的用途是將兩個元件隔開達到上下 Widgets 位於邊界兩端的效果

而在 onlineUesrs 中
我們則透過簡易的 LiseView 達到可以捲動畫面的效果
而為了不這麼單調 我在這邊使用了 username_gen 這個套件來隨機產生英文人名
並透過隨機的調色來讓大頭貼的背景顏色不會重複(至少看起來比較開心~)

class UsersScreen extends StatefulWidget {
  const UsersScreen({super.key});

  @override
  State<UsersScreen> createState() => _UsersScreenState();
}

class _UsersScreenState extends State<UsersScreen> {
  Widget story() {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
      child: GridView(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 0.7,
              mainAxisSpacing: 10,
              crossAxisSpacing: 10),
          children: [
            for (int i = 0; i < 20; i++) const StoryCard(),
          ]),
    );
  }

  Widget onlineUsers() {
    return ListView(
      children: [
        const ListTile(
          leading: CircleAvatar(
              backgroundImage: NetworkImage(
                  "https://pbs.twimg.com/profile_images/1455185376876826625/s1AjSxph_400x400.jpg")),
          title: Text("Google"),
        ),
        const ListTile(
          leading: CircleAvatar(
              backgroundImage: NetworkImage(
                  "https://pbs.twimg.com/profile_images/1187814172307800064/MhnwJbxw_400x400.jpg")),
          title: Text("Flutter"),
        ),
        for (int i = 0; i < 49; i++)
          ListTile(
            leading: CircleAvatar(
              backgroundColor: Color(Random.secure().nextInt(16777215) |
                  0xFF000000), // generate random color
              child: Text("F$i"),
            ),
            title: Text(UsernameGen().generate()),
          ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Column(
        children: <Widget>[
          const TabBar(
            indicatorSize: TabBarIndicatorSize.label,
            indicatorWeight: 1,
            labelColor: Colors.black,
            labelStyle: TextStyle(fontSize: 12),
            tabs: <Widget>[
              Tab(
                text: "限時動態",
              ),
              Tab(text: "在線上")
            ],
          ),
          Expanded(
            child: TabBarView(
              children: <Widget>[story(), onlineUsers()],
            ),
          ),
        ],
      ),
    );
  }
}

class StoryCard extends StatelessWidget {
  const StoryCard({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        ClipRRect(
          borderRadius: BorderRadius.circular(15),
          child: Image.network(
            "https://blogger.googleusercontent.com/img/a/AVvXsEiBvTaWkOFFihJud4ctimi-3DXWWjwU_x98aUPlba97hoBkHFASSExnr4U5JatHKG_PTDVeyDJ37dPC1EbAtGLNPZP9ixKznYdrTee8cs8kEiiiDfFdHUJ3JDMg2rGLCDCsmYMxSKzq7ci_PrWr4UEuPW1I5VVPTOHY282HjbC4AU5tCVqnvsu-Ss3p",
            fit: BoxFit.cover,
          ),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(
                  foregroundColor: Colors.grey,
                  backgroundColor: Colors.white,
                  shape: const CircleBorder(), // <-- Splash color
                ),
                onPressed: () => {},
                child: const Icon(
                  Icons.add,
                  color: Colors.black,
                ),
              ),
              const Spacer(),
              const Padding(
                padding: EdgeInsets.symmetric(horizontal: 8.0),
                child: Text(
                  "新增到限時動態",
                  style: TextStyle(color: Colors.white),
                ),
              )
            ],
          ),
        )
      ],
    );
  }
}

DEMO 畫面

DEMO
DEMO


參考資料

網格排列的 GridView
username_gen
《Flutter实战·第二版》


上一篇
通徹 Flutter 學習路徑 Day 11 - 奪回失去的狀態管理
下一篇
通徹 Flutter 學習路徑 Day 13 - 今天讓我們來介紹 Bloc ~
系列文
通徹 Flutter 學習路徑30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言