Flutter 常用UI及技巧

image.png

底部导航

final List<Widget> pages = [HomePage(), CategoryPage(), CartPage(), MePage()];

final List<BottomNavigationBarItem> items = [
  BottomNavigationBarItem(icon: Icon(Icons.home), label: "home"),
  BottomNavigationBarItem(icon: Icon(Icons.category), label: "category"),
  BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: "cart"),
  BottomNavigationBarItem(icon: Icon(Icons.people), label: "people")
];

class GOBottomNavigationBar extends StatefulWidget {
  const GOBottomNavigationBar({Key? key}) : super(key: key);

  @override
  State<GOBottomNavigationBar> createState() => _GOBottomNavigationBarState();
}

class _GOBottomNavigationBarState extends State<GOBottomNavigationBar> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // body: pages[_currentIndex],
      body: IndexedStack(
        index: _currentIndex,
        children: pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        items: items,
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.black,
        type: BottomNavigationBarType.fixed,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }
}
image.png

不规则底部导航

class GOBottomNavigationBar extends StatefulWidget {
  const GOBottomNavigationBar({Key? key}) : super(key: key);

  @override
  State<GOBottomNavigationBar> createState() => _GOBottomNavigationBarState();
}

class _GOBottomNavigationBarState extends State<GOBottomNavigationBar> {
  int _currentIndex = 0;
  final List<Widget> _pages = [HomePage(), CategoryPage()];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () {
            Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) {
                      return AddPage();
                    },
                    fullscreenDialog: true));
          },
        ),
        body: _pages[_currentIndex],
        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
        bottomNavigationBar: BottomAppBar(
          color: Colors.blue,
          shape: CircularNotchedRectangle(),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              IconButton(
                  onPressed: () {
                    setState(() {
                      _currentIndex = 0;
                    });
                  },
                  icon: Icon(Icons.home)),
              IconButton(
                  onPressed: () {
                    setState(() {
                      _currentIndex = 1;
                    });
                  },
                  icon: Icon(Icons.category)),
            ],
          ),
        ));
  }
}
image.png

路由跳转的动画效果

渐隐渐现
class CustomRouter extends PageRouteBuilder {
 final Widget _widget;

 CustomRouter(this._widget)
     : super(
           transitionDuration: Duration(seconds: 1),
           pageBuilder: (context, animation, secondaryAnimation) {
             return _widget;
           },
           transitionsBuilder:
               (context, animation, secondaryAnimation, child) {
             return FadeTransition(
                 child: child,
                 opacity: Tween(begin: 0.0, end: 2.0).animate(CurvedAnimation(
                     parent: animation, curve: Curves.linear)));
           });
}
Navigator.push(context, CustomRouter(SecondPage()));
渐隐渐现.gif
缩放
class CustomRouter extends PageRouteBuilder {
  final Widget _widget;

  CustomRouter(this._widget)
      : super(
            transitionDuration: Duration(seconds: 1),
            pageBuilder: (context, animation, secondaryAnimation) {
              return _widget;
            },
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) {
              return ScaleTransition(
                child: child,
                scale: Tween(begin: 0.0, end: 1.0).animate(
                    CurvedAnimation(parent: animation, curve: Curves.linear)),
              );
            });
}
缩放.gif
旋转+缩放
class CustomRouter extends PageRouteBuilder {
  final Widget _widget;

  CustomRouter(this._widget)
      : super(
            transitionDuration: Duration(seconds: 1),
            pageBuilder: (context, animation, secondaryAnimation) {
              return _widget;
            },
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) {
              return RotationTransition(
                  child: ScaleTransition(
                    child: child,
                    scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
                        parent: animation, curve: Curves.linear)),
                  ),
                  turns: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
                      parent: animation, curve: Curves.linear)));
            });
}
旋转缩放.gif
push
class CustomRouter extends PageRouteBuilder {
  final Widget _widget;
 
  CustomRouter(this._widget)
      : super(
            transitionDuration: Duration(seconds: 1),
            pageBuilder: (context, animation, secondaryAnimation) {
              return _widget;
            },
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) {
              return SlideTransition(
                child: child,
                position: Tween(begin: Offset(-1.0, 0.0), end: Offset(0.0, 0.0))
                    .animate(CurvedAnimation(
                        parent: animation, curve: Curves.linear)),
              );
            });
}
push.gif

毛玻璃效果

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Stack(
      children: [
        ConstrainedBox(
          constraints: const BoxConstraints.expand(),
          child: Image.network("https://picsum.photos/500/500"),
        ),
        Center(
          child: ClipRect(
            child: BackdropFilter(
              filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
              child: Opacity(
                opacity: 0.5,
                child: Container(
                  width: 300,
                  height: 300,
                  decoration: BoxDecoration(
                    color: Colors.grey.shade200,
                  ),
                  child: Center(
                      child: Text(
                        "我是毛玻璃",
                        style: TextStyle(fontSize: 30.0, color: Colors.red),
                      )),
                ),
              ),
            ),
          ),
        )
      ],
    ),
  );
}
image.png

保持页面状态

class GOPage extends StatefulWidget {
  const GOPage({Key? key}) : super(key: key);

  @override
  State<GOPage> createState() => _GOPageState();
}

class _GOPageState extends State<GOPage> with SingleTickerProviderStateMixin {
  late TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("A"),
        bottom: TabBar(
          controller: _controller,
          tabs: [
            Tab(
              icon: Icon(Icons.home),
            ),
            Tab(
              icon: Icon(Icons.category),
            ),
            Tab(
              icon: Icon(Icons.people),
            ),
          ],
        ),
      ),
      body: TabBarView(
        controller: _controller,
        children: [
          HomePage(),
          HomePage(),
          HomePage(),
        ],
      ),
    );
  }
}
class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with AutomaticKeepAliveClientMixin {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              "$_counter",
              style: TextStyle(fontSize: 30, color: Colors.red),
            ),
            MaterialButton(
                color: Colors.lightBlue,
                child: Icon(Icons.add),
                onPressed: () {
                  setState(() {
                    _counter++;
                  });
                })
          ],
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}
image.gif

搜索条

class GOSearchDelegate extends SearchDelegate<String> {
  @override
  List<Widget>? buildActions(BuildContext context) {
    return [
      IconButton(
          onPressed: () {
            query = "";
          },
          icon: Icon(Icons.clear))
    ];
  }

  @override
  Widget? buildLeading(BuildContext context) {
    return IconButton(
        onPressed: () {
          close(context, "1");
        },
        icon: AnimatedIcon(
          icon: AnimatedIcons.menu_arrow,
          progress: transitionAnimation,
        ));
  }

  @override
  Widget buildResults(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: Card(
        color: Colors.redAccent,
        child: Center(
          child: Text(query),
        ),
      ),
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    final List<String> _suggestionsList = query.isEmpty
        ? recList
        : searchList.where((element) => element.startsWith(query)).toList();

    return ListView.builder(
        itemCount: _suggestionsList.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: RichText(
              text: TextSpan(
                  style: TextStyle(
                      color: Colors.redAccent, fontWeight: FontWeight.bold),
                  text: _suggestionsList[index].substring(0, query.length),
                  children: [
                    TextSpan(
                      text: _suggestionsList[index].substring(query.length),
                      style: TextStyle(color: Colors.black38),
                    )
                  ]),
            ),
          );
        });
  }
}
showSearch(context: context, delegate: GOSearchDelegate());
image.gif

Wrap流式布局

class GOWarpPage extends StatefulWidget {
  const GOWarpPage({Key? key}) : super(key: key);

  @override
  State<GOWarpPage> createState() => _GOWarpPageState();
}

class _GOWarpPageState extends State<GOWarpPage> {
  List<Widget> _list = [];

  @override
  void initState() {
    super.initState();
    _list.add(buildAddButton());
  }

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.of(context).size.width;
    final height = MediaQuery.of(context).size.height;

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter"),
      ),
      body: Center(
        child: Opacity(
          opacity: 0.8,
          child: Container(
            width: width,
            height: height * 0.5,
            color: Colors.black26,
            child: Wrap(
              spacing: 20,
              children: _list,
            ),
          ),
        ),
      ),
    );
  }

  Widget buildPhoto() {
    return Padding(
      padding: EdgeInsets.all(8),
      child: Container(
        width: 80,
        height: 80,
        color: Colors.teal,
        child: Center(
          child:
              Image.network("https://picsum.photos/300/300", fit: BoxFit.fill),
        ),
      ),
    );
  }

  Widget buildAddButton() {
    return GestureDetector(
      onTap: () {
        if (_list.length < 9) {
          setState(() {
            _list.insert(_list.length - 1, buildPhoto());
          });
        }
      },
      child: Padding(
        padding: EdgeInsets.all(8),
        child: Container(
          width: 80,
          height: 80,
          color: Colors.grey,
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}
image.gif

ExpansionTile

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter"),
      ),
      body: Center(
        child: ExpansionTile(
          title: Text("Expansion Tile"),
          leading: Icon(Icons.ac_unit),
          backgroundColor: Colors.amberAccent,
          children: [
            ListTile(
              title: Text("title"),
              subtitle: Text("subtitle"),
            )
          ],
          initiallyExpanded: true, // 默认打开
        ),
      ),
    );
  }
}
image.gif

ExpansionPanelList

class ExpansionPanelListPage extends StatefulWidget {
  const ExpansionPanelListPage({Key? key}) : super(key: key);

  @override
  State<ExpansionPanelListPage> createState() => _ExpansionPanelListPageState();
}

class _ExpansionPanelListPageState extends State<ExpansionPanelListPage> {
  List<int> _list = [];
  List<ExpansionStateModel> _models = [];

  _ExpansionPanelListPageState() {
    for (int i = 0; i < 20; i++) {
      _list.add(i);
      _models.add(ExpansionStateModel(i, false));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter"),
      ),
      body: SingleChildScrollView(
        child: ExpansionPanelList(
          expansionCallback: (panelIndex, isExpanded) {
            _setCurrentIndex(panelIndex, isExpanded);
          },
          children: _list.map((e) {
            return ExpansionPanel(
                headerBuilder: (context, isExpanded) {
                  return ListTile(
                    title: Text("No. $e"),
                  );
                },
                body: ListTile(
                  title: Text("expansion no. $e"),
                ),
                isExpanded: _models[e].isOpen);
          }).toList(),
        ),
      ),
    );
  }

  _setCurrentIndex(int index, bool isExpanded) {
    setState(() {
      _models.forEach((element) {
        if (element.index == index) {
          element.isOpen = !isExpanded;
        }
      });
    });
  }
}

class ExpansionStateModel {
  var isOpen;
  var index;

  ExpansionStateModel(this.index, this.isOpen);
}
image.gif

路径裁切和二次贝塞尔曲线

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: ClipPath(
        clipper: BottomClipper(),
        child: Container(
          color: Colors.redAccent,
          height: 150,
        ),
      )),
    );
  }
}
class BottomClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path();
    path.lineTo(0, 0);
    path.lineTo(0, size.height - 50);

    var beginPoint = Offset(size.width * 0.5, size.height);
    var endPoint = Offset(size.width, size.height - 50);
    path.quadraticBezierTo(beginPoint.dx, beginPoint.dy,
        endPoint.dx, endPoint.dy);

    path.lineTo(size.width, size.height - 50);
    path.lineTo(size.width, 0);
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return false;
  }
}
image.png

class BottomClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path();
    path.lineTo(0, 0);
    path.lineTo(0, size.height - 50);

    var beginPoint = Offset(size.width * 0.25, size.height);
    var endPoint = Offset(size.width * 0.5, size.height - 50);
    path.quadraticBezierTo(
        beginPoint.dx, beginPoint.dy, endPoint.dx, endPoint.dy);

    var beginPoint2 = Offset(size.width * 0.75, size.height - 100);
    var endPoint2 = Offset(size.width, size.height - 50);
    path.quadraticBezierTo(
        beginPoint2.dx, beginPoint2.dy, endPoint2.dx, endPoint2.dy);

    path.lineTo(size.width, size.height - 50);
    path.lineTo(size.width, 0);
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return false;
  }
}
image.png

APP闪屏动画

class GOAnimationPage extends StatefulWidget {
  const GOAnimationPage({Key? key}) : super(key: key);

  @override
  State<GOAnimationPage> createState() => _GOAnimationPageState();
}

class _GOAnimationPageState extends State<GOAnimationPage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this, duration: Duration(milliseconds: 3000));
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        Navigator.pushAndRemoveUntil(
            context,
            MaterialPageRoute(builder: (context) => HomePage()),
            (route) => route == null);
      }
    });
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _animation,
      child: Image.network(
        "https://picsum.photos/300/300",
        fit: BoxFit.fill,
        scale: 2.0,
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text("我是首页")),
    );
  }
}
image.gif

右滑返回 cupertino

import 'package:flutter/cupertino.dart';

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

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
        child: Center(
      child: Container(
        height: 100,
        width: 100,
        color: CupertinoColors.activeBlue,
        child: CupertinoButton(
          child: Icon(CupertinoIcons.add),
          onPressed: () {
            Navigator.push(context, CupertinoPageRoute(builder: (context) {
              return RightBackPage();
            }));
          },
        ),
      ),
    ));
  }
}
image.gif

Tooltip

当widget长按时显示一个提示标签

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Tooltip(
          child: Container(
            width: 100,
            height: 100,
            color: Colors.amber,
          ),
          message: "长按了 Container !!!",
          textStyle: TextStyle(fontSize: 22, color: Colors.redAccent),
          showDuration: Duration(seconds: 2),
        ),
      ),
    );
  }
}
image.gif

Draggable 拖拽控件

class GODraggable extends StatefulWidget {
  Offset? offset;
  Color? widgetColor;

  GODraggable({this.offset, this.widgetColor});

  @override
  State<GODraggable> createState() => _GODraggableState();
}

class _GODraggableState extends State<GODraggable> {
  @override
  Widget build(BuildContext context) {
    return Positioned(
        left: widget.offset?.dx,
        top: widget.offset?.dy,
        child: Draggable(
          data: widget.widgetColor,
          child: Container(
            width: 100,
            height: 100,
            color: widget.widgetColor,
          ),
          feedback: Container(
            width: 120,
            height: 120,
            color: widget.widgetColor?.withOpacity(0.5),
          ),
          onDraggableCanceled: (velocity, offset) {
            setState(() {
              widget.offset = offset;
            });
          },
        ));
  }
}
class GODraggablePage extends StatefulWidget {
  const GODraggablePage({Key? key}) : super(key: key);

  @override
  State<GODraggablePage> createState() => _GODraggablePageState();
}

class _GODraggablePageState extends State<GODraggablePage> {
  Color _color = Colors.black12;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          GODraggable(
            offset: Offset(80, 80),
            widgetColor: Colors.amber,
          ),
          GODraggable(
            offset: Offset(200, 80),
            widgetColor: Colors.redAccent,
          ),
          Center(
            child: DragTarget(
              onAccept: (Color color) {
                _color = color;
              },
              builder: (context, candidateData, rejectedData) {
                return Container(
                  width: 300,
                  height: 300,
                  color: _color,
                );
              },
            ),
          )
        ],
      ),
    );
  }
}
image.gif

本篇文章内容学习自:20个Flutter实例视频教程 让你轻松上手工作

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容