iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 14
0
Mobile Development

iOS Developer Learning Flutter系列 第 14

iOS Developer Learning Flutter. Lesson13 裁判~可以讓人列完又列這樣的嗎? (多選與側滑)

我看本節目乾脆改名為《iOS Developer Learning ListView》算了

Today Preview

1. 多選

iOS要做多選列表有兩種方式

左邊由tableView來幫我們管理狀態
只要.allowsMultipleSelectionDuringEditing = true
最後從indexPathsForSelectedRows取得結果即可
而右邊則是用陣列記住每個選項是否選取
然後設定cell.accessoryType = .checkmark

Flutter這邊使用CheckboxListTile來完成多選功能
由於它是StatelessWidget
不像昨天提到的ExpansionTile會自己管理狀態
因此也必須用陣列儲存資料☘️☘️☘️
然後用setState去刷新畫面

  onChanged: (check) {
    setState(() {
      checkList[idx] = check;
    });
  },

接著介紹一下CheckboxListTile一些特別的屬性

  • controlAffinity可以決定checkbox是置左還置右, 如果設定為platform, 則是iOS右, Android左
  • secondary則是放置widget在checkbox相對的位置
  • activeColor決定了選中的顏色
  • value跟selected, 會有點容易搞混
    • value是否選中
    • selected是否要將tile其他部分都變成activeColor

2. 側滑

使用Flutter Favorite program裡面的flutter_slidable
(根本就是Flutter界的SwipeCellKit XD)☘️☘️☘️

看最上面的gif
可以發現我有特別放慢動作
再加上我精心的說明
應該很清楚四種動畫效果的差別了(哈哈)

提一下幾個名詞解釋

  1. Slidable: 要用它來把cell包住
  2. SlideAction: 其實就是那個像按鈕的東西(不是動詞)
    分成SlideAction跟IconSlideAction有無Icon兩種
  3. SlideActionType: 有兩種, primary左; secondary右, 刪除時由call back回傳
  4. SlidableActionPane: 就是上面那四種動畫效果
    (還真厲害每個動畫都取名6個字母(Strech是不是故意拼錯www))

再提一下Slidable的幾個屬性

  1. actions, secondaryActions: 分別是左/右邊有哪些選項
    保持Flutter一貫的特性, 只要是Widget都吃, 不一定要給SlideAction
  2. actionExtentRatio: 決定每個Action寬度是cell的幾分之幾, 超過1會報錯
  3. dismissal: 如果要刪除就要給SlidableDismissal這個類別的物件, 可以設定
    1. dismissThresholds: 拉超過多少比例才刪除
    2. onDismissed / onWillDismiss call back

刪除的過程中卡了幾次

無key不能刪

//這樣才可以
Slidable(
  key: Key(slidableItem.title)

list不能放不同型別

//本來想說把Widget塞進models裡面就好XD

不在list不能刪

//我原本是寫成這樣, 用數量去當作state

    //1.
    int showItemCount = slidableItems.length + 1;

    //2.
    onDismissed: (type) {
      setState(() {
        showItemCount = slidableItems.length;
      });
    },

    //3.
    return ListView.builder(
      itemCount: showItemCount,
      itemBuilder: (ctx, idx) {
        if (idx == slidableItems.length) {
          return dismissalItem;
        } else {
          return _createSlidableListTile(idx);
        }
      }
    );
    
//但後來乖乖寫成這樣才行

    //1.
    onDismissed: (type) {
      setState(() {
        slidableItems.removeLast();
      });
    },
    
    //2.
    return ListView.builder(
      itemCount: slidableItems.length,
      itemBuilder: (ctx, idx) {
        if (slidableItems[idx].canDelete) {
          return _createSlidableDismissalListTile(idx);
        } else {
          return _createSlidableListTile(idx);
        }
      }
    );

同場加映SnackBar

在Flutter裡已經沒有吐司可以吃了XD
取得代之建議的是使用這玩意
它是寄生在Scaffold之中
所以必須先取得Scaffold
像這樣`Scaffold.of(context).showSnackBar(SnackBar(content: Text(tapInfo)));

完整程式碼

import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';

enum SliableType{
  archive,
  share,
  more,
  delete,
}

class SliableItem{

  final String title;
  final String subTitle;
  final Widget actionPane;
  final bool canDelete;

  SliableItem(this.title, this.subTitle, this.actionPane, this.canDelete);
}

class LessonPageListViewSwipe extends StatefulWidget {
  @override
  _LessonPageListViewSwipeState createState() => _LessonPageListViewSwipeState();
}

class _LessonPageListViewSwipeState extends State<LessonPageListViewSwipe> {

  List slidableItems = [
    SliableItem("Behind效果", "Action像在Tile的背面\n(最靠外面的Action最先出現)",
        SlidableBehindActionPane(), false),
    SliableItem("Scroll效果", "Action像在Tile的兩側\n(最靠中間的Action最先出現)",
        SlidableScrollActionPane(), false),
    SliableItem("Strech效果", "Action像在Tile的兩側\n(Action同時出現, 沒有重疊的感覺)",
        SlidableStrechActionPane(), false),
    SliableItem("Drawer效果", "Action像在Tile的兩側\n(Action同時出現, 會有重疊的感覺)",
        SlidableDrawerActionPane(), false),
    SliableItem("送我一程吧~~~~", "", SlidableStrechActionPane(), true),
  ];

  @override
  Widget build(BuildContext context) {

    void _showSnackBar(SliableType actionType, int index) {
      final tapInfo = "$index is ${actionType.toString()}";
      print(tapInfo);
      Scaffold.of(context).showSnackBar(SnackBar(content: Text(tapInfo),));
    }

    Widget _createSlidableListTile(int index) {

      //都用SlideAction
      final leftActionMenu = [
        SlideAction(
          child: FlutterLogo(),
          color: Colors.yellow,
          onTap: () => _showSnackBar(SliableType.archive, index),
        ),
        SlideAction(
          child: Text("tap me", textAlign: TextAlign.center),
          color: Colors.greenAccent,
          onTap: () => _showSnackBar(SliableType.share, index),
        )
      ];

      //都用IconSlideAction
      final rightActionMenu = [
        //無字
        IconSlideAction(
          color: Colors.black45,
          icon: Icons.more_horiz,
          onTap: () => _showSnackBar(SliableType.more, index),
        ),
        //有字
        IconSlideAction(
          caption: 'Delete',
          color: Colors.red,
          icon: Icons.delete,
          onTap: () => _showSnackBar(SliableType.delete, index),
        )
      ];

      final slidableItem = slidableItems[index];
      return Slidable(
        actions: leftActionMenu,
        secondaryActions: rightActionMenu,
        actionPane: slidableItem.actionPane,
        actionExtentRatio: 0.1 * (index + 1),
        child: Container(
          color: Colors.white,
          child: ListTile(
            title: Text(slidableItem.title),
            subtitle: Text(slidableItem.subTitle),
            isThreeLine: true,
            leading: CircleAvatar(
              child: Text('$index'),
              foregroundColor: Colors.white,
              backgroundColor: Colors.orange,
            ),
          ),
        ),
      );
    }

    Widget _createSlidableDismissalListTile(int index) {

      final slidableItem = slidableItems[index];
      return Slidable(
        key: Key(slidableItem.title),
        actionPane: slidableItem.actionPane,
        actionExtentRatio: 0.25,
        child: Container(
          color: Colors.white,
          child: ListTile(
            title: Text(slidableItem.title,
              style: TextStyle(color: Colors.redAccent),
            ),
          ),
        ),
        secondaryActions: [
          IconSlideAction(
            caption: 'Delete',
            color: Colors.red,
            icon: Icons.delete,
          )
        ],
        dismissal: SlidableDismissal(
          child: SlidableDrawerDismissal(),
          dismissThresholds: {SlideActionType.secondary: 0.3},
          onDismissed: (type) {
            setState(() {
              slidableItems.removeLast();
            });
          },
        ),
      );
    }

    return ListView.builder(
      itemCount: slidableItems.length,
      itemBuilder: (ctx, idx) {
        if (slidableItems[idx].canDelete) {
          return _createSlidableDismissalListTile(idx);
        } else {
          return _createSlidableListTile(idx);
        }
      }
    );
  }
}

參考連結


下集預告:網格(總算不是列表了)


上一篇
iOS Developer Learning Flutter. Lesson12 還是~列表(輸入與折疊)
下一篇
iOS Developer Learning Flutter. Lesson14 網格
系列文
iOS Developer Learning Flutter30

1 則留言

0
MarkFly~
iT邦新手 5 級 ‧ 2020-09-29 21:51:13

我薩爾達神廟還解不到60關QQ

我要留言

立即登入留言