Flutter进阶

底部导航栏制作

效果图:

主入口文件的编写

首先我们先写一个主入口文件,这个文件只是简单的APP通用结构,最主要的是要引入自定义的BottomNavigationWidget组件。

main.dart代码如下

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData.light(),
      home: BottomNavigationWidget(),
    );
  }
}

[!DANGER|style:flat] 注意的是BottomNaivgationWidget这个组件还没有编写,所以现在会报错。

StatefulWidget 讲解

在编写BottomNaivgationWidget组件前,我们需要简单了解一下什么是StatefulWidget.

StatefulWidget具有可变状态(state)的窗口组件(widget)。使用这个要根据变化状态,调整State值。

在lib目录下,新建一个bottom_navigation_widget.dart文件。

它的初始化和以前使用的StatelessWidget不同,我们在VSCode中直接使用快捷方式生成代码(直接在VSCode中输入stful):

class name extends StatefulWidget {
  _nameState createState() => _nameState();
}

class _nameState extends State<name> {
  @override
  Widget build(BuildContext context) {
    return Container(
       child: child,
    );
  }
}

上面的代码可以清楚的看到,使用StatefulWidget分为两个部分,第一个部分是继承与StatefullWidget,第二个部分是继承于State.其实State部分才是我们的重点,主要的代码都会写在State中。

BottomNaivgationWidget自定义

接下来我们就要创建BottomNaivgationWidget这个Widget了,只是建立一个底部导航。

import 'package:flutter/material.dart';
import 'package:studyrow/pages/airplay_screen.dart';
import 'package:studyrow/pages/email_screen.dart';
import 'package:studyrow/pages/home_screen.dart';
import 'package:studyrow/pages/pages_screen.dart';

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

  @override
  _BottomNavigationWidgetState createState() => _BottomNavigationWidgetState();
}

class _BottomNavigationWidgetState extends State<BottomNavigationWidget> {
  final _BottomNavigationColor = Colors.blue;
  final _BottomNavigationColor2 = Colors.grey;
  int _currentIndex = 0;
  List<Widget> list = [];

  @override
  void initState() {
    list
      ..add(HomeScreen())
      ..add(EmailScreen())
      ..add(PagesScreen())
      ..add(AirplayScreen());
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: list[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
            icon: Icon(
              Icons.home,
              color: _BottomNavigationColor,
            ),
            label: "首页",
          ),
          BottomNavigationBarItem(
            icon: Icon(
              Icons.email,
              color: _BottomNavigationColor,
            ),
            label: "邮箱",
          ),
          BottomNavigationBarItem(
            icon: Icon(
              Icons.pages,
              color: _BottomNavigationColor,
            ),
            label: "pages",
          ),
          BottomNavigationBarItem(
            icon: Icon(
              Icons.airplay,
              color: _BottomNavigationColor,
            ),
            label: "airplay",
          ),
        ],
        currentIndex: _currentIndex,
        onTap: (int index) {
          setState(() {
            _currentIndex = index;
          });
        },
        selectedItemColor: _BottomNavigationColor,
        type: BottomNavigationBarType.fixed,
      ),
    );
  }
}

子页面的编写

子页面我们就采用最简单的编写了,只放入一个AppBar和一个Center,然后用Text Widget表明即可。

先来写一个HomeScreen组件,新建一个pages目录,然后在目录下面新建home_screen.dart文件。

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("首页"),
      ),
      body: const Center(
        child: Text("首页"),
      ),
    );
  }
}

有了这个文件剩下的文件就可以复制粘贴,然后改少量的代码来完成了。

分别建立:

  • email_screen.dart
  • pages_screen.dart
  • airplay_screen.dart

这些都是导航要用的子页面,有了这些页面,我们才能继续编写代码。

重写initState()方法

我们要重新initState()方法,把刚才做好的页面进行初始化到一个Widget数组中。有了数组就可以根据数组的索引来切换不同的页面了。这是现在几乎所有的APP采用的方式。

代码如下:

 List<Widget> list = List();
 @override
 void initState(){
    list
      ..add(HomeScreen())
      ..add(EmailScreen())
      ..add(PagesScreen())
      ..add(AirplayScreen());
    super.initState();
  }

这里的..add()是Dart语言的..语法,如果你学过编程模式,你一定听说过建造者模式,简单来说就是返回调用者本身。这里list后用了..add(),还会返回list,然后就一直使用..语法,能一直想list里增加widget元素。 最后我们调用了一些父类的initState()方法。

BottomNavigationBar里的响应事件

BottomNavigationBar组件里提供了一个相应事件onTap,这个事件自带一个索引值index,通过索引值我们就可以和我们list里的索引值相对应了。

        onTap:(int index){
           setState((){
             _currentIndex= index;
           });
         },

不规则底部工具栏制作

效果图:

自定义主题样本

Flutter支持自定义主题,如果使用自定义主题,设置的内容项是非常多的,这可能让初学者头疼,Flutter贴心的为给我们准备了主题样本。

primarySwatch :现在支持18种主题样本了。

具体代码如下:

theme: ThemeData(
  primarySwatch: Colors.deepOrange,
),

替换primarySwatch颜色

[!Tip|style:flat] primarySwatch 是 MaterialColor 类型我们得把 Color 转换成 MaterialColor

转换代码如下:

import 'dart:ui';
import 'package:flutter/material.dart'; //#223344 needs change to 0xFF223344
//即把#换成0xFF即可

MaterialColor createMaterialColor(Color color) {
  List strengths = <double>[.05];
  Map<int, Color> swatch = <int, Color>{};
  final int r = color.red, g = color.green, b = color.blue;

  for (int i = 1; i < 10; i++) {
    strengths.add(0.1 * i);
  }
  for (var strength in strengths) {
    final double ds = 0.5 - strength;
    swatch[(strength * 1000).round()] = Color.fromRGBO(
      r + ((ds < 0 ? r : (255 - r)) * ds).round(),
      g + ((ds < 0 ? g : (255 - g)) * ds).round(),
      b + ((ds < 0 ? b : (255 - b)) * ds).round(),
      1,
    );
  }
  return MaterialColor(color.value, swatch);
}

会了这个知识后,我们就可以先把我们的主入口文件编写一下了,具体代码如下:

import 'package:flutter/material.dart';
import 'bottom_appBar_demo.dart';
import 'color.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData(
        primarySwatch: createMaterialColor(Color.fromARGB(255, 207, 95, 21)),
      ),
      home: const BottomAppBarDemo(),
    );
  }
}

这时候bottom_appBar_demo.dart这个文件还没有,也找不到,这个文件是我们的主要文件,我们的主要业务逻辑会写在这个文件里。

因为没有所以你写完之后会报错,那接下来我们就来编写这个文件。

floatingActionButton Widget

floatingActionButton从字面理解可以看出,它是“可交互的浮动按钮”,其实在Flutter默认生成的代码中就有这家伙,只是我们没有正式的接触。

一般来说,它是一个圆形,中间放着图标,会优先显示在其他Widget的前面。

下面我们来看看它的常用属性:

  • onPressed :点击相应事件,最常用的一个属性。

  • tooltip:长按显示的提示文字,因为一般只放一个图标在上面,防止用户不知道,当我们点击长按时就会出现一段文字性解释。非常友好,不妨碍整体布局。

  • child :放置子元素,一般放置Icon Widget。

我们来看一下floatingActionButton的主要代码:

floatingActionButton: FloatingActionButton(
    onPressed: (){
      Navigator.of(context).push(MaterialPageRoute(builder:(BuildContext context){
        return EachView('New Page');
      }));
    },
    tooltip: 'Increment',
    child: Icon(
      Icons.add,
      color: Colors.white,
    ),
  ),

写完这些代码已经有了一个悬浮的按钮,但这个悬浮按钮还没有和低栏进行融合,这时候需要一个属性。

floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

这时候就可以和底栏进行融合了。

BottomAppBar Widget

BottomAppBar 是 底部工具栏的意思,这个要比BottomNavigationBar widget灵活很多,可以放置文字和图标,当然也可以放置容器。

BottomAppBar的常用属性:

  • color:这个不用多说,底部工具栏的颜色。
  • shape:设置底栏的形状,一般使用这个都是为了和floatingActionButton融合,所以使用的值都是CircularNotchedRectangle(),有缺口的圆形矩形。
  • child : 里边可以放置大部分Widget,让我们随心所欲的设计底栏。

代码如下:

import 'package:flutter/material.dart';

class BottomAppBarDemo extends StatefulWidget {
  _BottomAppBarDemoState createState() => _BottomAppBarDemoState();
}

class _BottomAppBarDemoState extends State<BottomAppBarDemo> {

  @override
  Widget build(BuildContext context) {
     return Scaffold(
        floatingActionButton: FloatingActionButton(
          onPressed: (){

          },
          tooltip: 'Increment',
          child: Icon(
            Icons.add,
            color: Colors.white,
          ),
        ),
       floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
       bottomNavigationBar: BottomAppBar(
         color:Colors.lightBlue,
         shape:CircularNotchedRectangle(),
         child: Row(
           mainAxisSize: MainAxisSize.max,
           mainAxisAlignment: MainAxisAlignment.spaceAround,
           children: <Widget>[
             IconButton(
               icon:Icon(Icons.home),
               color:Colors.white,
               onPressed:(){

               }
             ),
             IconButton(
               icon:Icon(Icons.airport_shuttle),
               color:Colors.white,
               onPressed:(){

               }
             ),
           ],
         ),
       )
        ,
     );
  }
}

StatefulWidget子页面的制作

在之前实例中我们使用了子页面,但子页面继承与StatelessWidget(不可变控件),所以很麻烦的写了4个页面,其实完全可以写一个继承于StatefulWidget的控件,进行搞定。

新建一个each_view.dart文件,然后输入如下代码:

import 'package:flutter/material.dart';

class EachView extends StatefulWidget {
  String _title;
  EachView(this._title);
  @override
  _EachViewState createState() => _EachViewState();
}

class _EachViewState extends State<EachView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget._title)),
      body: Center(child: Text(widget._title)),
    );
  }
}

代码中设置了一个内部的_title变量,这个变量是从主页面传递过来的,然后根据传递过来的具体值显示在APP的标题栏和屏幕中间。

按钮交互效果的制作

这些效果都是在bottom_appBar_demo.dart页面完成的。首先我们需要引入新作的子页面each_view.dart。

import 'each_view.dart';

新建两个变量,主要作用是控制body中的试图,也就是显示不同的子页面。

  List<Widget> _eachView;  //创建视图数组
  int _index = 0;          //数组索引,通过改变索引值改变视图

下一步是为_eachView进行初始化赋值,我们可以直接重写初始化方法,具体代码如下:

@override
void initState() {
  _eachView = [];
  _eachView
    ..add(EachView('Home'))
    ..add(EachView('Me'));
  super.initState();
}

剩下的就是写个个按钮的交互事件,交互的动作分两种:

直接打开子导航,比如我们点击了中间的”+“按钮,我们直接开启子页面。

onPressed: (){
 Navigator.of(context).push(MaterialPageRoute(builder:(BuildContext context){
   return EachView('New Page');
 }));
},

改变状态,通过改变状态,来切换页面,这样我们整体页面并没有被刷新。

onPressed:(){
setState(() {
 _index=0;             
});
}

为了方便小伙伴们学习,给出bottom_appBar_demo.dart所有代码,代码如下:

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

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

  @override
  _BottomAppBarDemoState createState() => _BottomAppBarDemoState();
}

class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
  late List<Widget> _eachView; //创建视图数组
  int _index = 0; //数组索引,通过改变索引值改变视图
  @override
  void initState() {
    _eachView = [];
    _eachView
      ..add(EachView('Home'))
      ..add(EachView('Me'));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _eachView[_index],
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context)
              .push(MaterialPageRoute(builder: (BuildContext context) {
            return EachView('New Page');
          }));
        },
        tooltip: "新增",
        child: const Icon(
          Icons.add,
          color: Colors.white,
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      bottomNavigationBar: BottomAppBar(
        color: Colors.deepOrange,
        shape: const CircularNotchedRectangle(),
        child: Row(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            IconButton(
              onPressed: () {
                setState(() {
                  _index = 0;
                });
              },
              icon: const Icon(Icons.home),
              color: Colors.white,
            ),
            IconButton(
              onPressed: () {
                setState(() {
                  _index = 1;
                });
              },
              icon: const Icon(Icons.airport_shuttle),
              color: Colors.white,
            )
          ],
        ),
      ),
    );
  }
}

酷炫的路由动画

其实路由动画的原理很简单,就是重写并继承PageRouterBuilder这个类里的transitionsBuilder方法。

不过这个方法还是有很多写法的,通过写法的不同,产生的动画效果也有所不同。

主入口方法

先编写一个主入口方法,还是最简单的格式,只不过home属性,使用的FirstPage的组件是我们自定义的,需要我们再次编写。入口文件的代码如下:

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData(
        primarySwatch: Colors.lightGreen,
      ),
      home: FirstPage(),
    );
  }
}

pages.dart页面的编写

主入口文件用import引入了pages.dart文件,这个文件就是生成了两个页面(Flutter里的页面也是Widget,这个你要跟网页区分开)。有了两个页面就可以实现路由跳转了。

pages.dart文件的代码如下,这里我们先用普通路由代替,看一看效果。

import 'package:flutter/material.dart';

import 'custome_router.dart';

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.lightGreen,
      appBar: AppBar(
        title: const Text(
          "First Page",
          style: TextStyle(fontSize: 36.0),
        ),
        elevation: 0.0,
      ),
      body: Center(
        child: MaterialButton(
          child: const Icon(
            Icons.navigate_next,
            color: Colors.white,
            size: 46.0,
          ),
          onPressed: () {
            Navigator.of(context)
                .push(MaterialPageRoute(builder: (BuildContext context) {
              return SecondPage();
            }));
            // Navigator.of(context).push(CustomRoute(SecondPage()));
          },
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.pinkAccent,
      appBar: AppBar(
        title: const Text(
          "Second Page",
          style: TextStyle(fontSize: 36.0),
        ),
        backgroundColor: Colors.pinkAccent,
        leading: Container(),
        elevation: 0.0,
      ),
      body: Center(
        child: MaterialButton(
          child: const Icon(
            Icons.navigate_before,
            color: Colors.white,
            size: 46.0,
          ),
          onPressed: () {
            Navigator.of(context).pop();
          },
        ),
      ),
    );
  }
}

上面代码中有一个新知识点,需要学习一下:

AppBar Widger的 elevation 属性:这个值是AppBar 滚动时的融合程度,一般有滚动时默认是4.0,现在我们设置成0.0,就是和也main完全融合了。 写完这个页面代码后,已经可以进行最基本的导航了,但是并没有什么酷炫的动画。

自定义CustomRoute Widget

新建一个custome_router.dart文件,这个就是要自定义的路由方法,自定义首先要继承于通用的路由的构造器类PageRouterBuilder。继承之后重写父类的CustomRoute构造方法。

构造方法可以简单理解为:只要以调用这个类或者说是Widget,构造方法里的所有代码就执行了。

custome_router.dart代码如下

import 'package:flutter/material.dart';

class CustomRoute extends PageRouteBuilder{
  final Widget widget;
  CustomRoute(this.widget)
    :super(
      transitionDuration:const Duration(seconds:1),
      pageBuilder: (BuildContext context, Animation<double> animation1,
          Animation<double> animation2) {
        return widget;
      },
      transitionsBuilder:(
          BuildContext context,
          Animation<double> animation1,
          Animation<double> animation2,
          Widget child){
            return FadeTransition(
              opacity: Tween(begin:0.0,end :1.0).animate(CurvedAnimation(
                  parent:animation1,
                  curve:Curves.fastOutSlowIn
              )),
              child: child,
            );
        }  

    ); 
}
  • FadeTransition:渐隐渐现过渡效果,主要设置opactiy(透明度)属性,值是0.0-1.0。
  • animate :动画的样式,一般使用动画曲线组件(CurvedAnimation)。
  • curve: 设置动画的节奏,也就是常说的曲线,Flutter准备了很多节奏,通过改变动画取消可以做出很多不同的效果。
  • transitionDuration:设置动画持续的时间,建议再1和2之间。

写完代码,我们已经可以看到在切换路由时有了动画效果

缩放路由动画

return ScaleTransition(
  scale:Tween(begin:0.0,end:1.0).animate(CurvedAnimation(
    parent:animation1,
    curve: Curves.fastOutSlowIn
    )),
    child:child
);

旋转+缩放路由动画

旋转+缩放的思路是在一个路由动画里的child属性里再加入一个动画,让两个动画同时执行。来看详细代码:

 return RotationTransition(
  turns:Tween(begin:0.0,end:1.0)
  .animate(CurvedAnimation(
    parent: animation1,
    curve: Curves.fastOutSlowIn
  )),
  child:ScaleTransition(
    scale:Tween(begin: 0.0,end:1.0)
    .animate(CurvedAnimation(
        parent: animation1,
        curve:Curves.fastOutSlowIn
    )),
    child: child,
  )
);

左右滑动路由动画

其实用的做多的还是左右滑动路由动画,其实实现起来也是非常简单,直接使用SlideTransition就可以了。

// 幻灯片路由动画
return SlideTransition(
  position: Tween<Offset>(
    begin: Offset(-1.0, 0.0),
    end:Offset(0.0, 0.0)
  )
  .animate(CurvedAnimation(
    parent: animation1,
    curve: Curves.fastOutSlowIn
  )),
  child: child,
);

总结:动画的使用会让我们的APP更加酷炫,也会让别人觉的你不是一个新手,再Flutter里使用动画是非常方便的,所以你可以把这些动画效果事先写好,在工作中直接使用。

毛玻璃效果制作

main.dart文件编写

这个和以前的写法都一样,所以就直接贴代码了

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Scaffold(
        body: FrostedGlassEmo(),
      ),
    );
  }
}

BackdropFilter Widget

BackdropFilter就是背景滤镜组件,使用它可以给父元素增加滤镜效果,它里边最重要的一个属性是filterfilter属性中要添加一个滤镜组件,实例中我们添加了图片滤镜组件,并给了模糊效果。

我们新建一个frosted_glass_demo.dart的文件,然后写入下面的代码,具体的解释已经写到了代码的注释中。

import 'package:flutter/material.dart';
import 'dart:ui';   //引入ui库,因为ImageFilter Widget在这个里边。

class FrostedGlassDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:Stack(   //重叠的Stack Widget,实现重贴
        children: <Widget>[
          ConstrainedBox( //约束盒子组件,添加额外的限制条件到 child上。
            constraints: const BoxConstraints.expand(), //限制条件,可扩展的。
            child:Image.network('https://pic.tianqinote.com/20220530165628.png')
          ),
          Center(
            child: ClipRect(  //裁切长方形
              child: BackdropFilter(   //背景滤镜器
                filter: ImageFilter.blur(sigmaX: 5.0,sigmaY: 5.0), //图片模糊过滤,横向竖向都设置5.0
                child: Opacity( //透明控件
                  opacity: 0.5,
                  child: Container(// 容器组件
                    width: 500.0,
                    height: 700.0,
                    decoration: BoxDecoration(color:Colors.grey.shade200), //盒子装饰器,进行装饰,设置颜色为灰色
                    child: Center(
                      child: Text(
                        'JSPang',
                        style: Theme.of(context).textTheme.display3, //设置比较酷炫的字体
                      ),
                    ),
                  ),
                ),
              ),
            ),
          )
        ],
      )
    );
  }
}

这个代码嵌套很多,所以一定要注意你的代码层次,容易出错的地方就是嵌套错误。这个效果尽量少用,因为我测试了一下,它对系统的消耗还是比较大的。

保持页面状态

效果图:

With 关键字的使用

with是dart的关键字,意思是混入的意思,就是说可以将一个或者多个类的功能添加到自己的类无需继承这些类, 避免多重继承导致的问题。

比如下面的代码:

class _KeepAliveDemoState extends State<KeepAliveDemo> with SingleTickerProviderStateMixin {

}

需要注意的是with后边是Mixin,而不是普通的Widget,这个初学者比较爱犯错误。需要强调一下。

TabBar Widget的使用

TabBar是切换组件,它需要设置两个属性。

  • controller: 控制器,后边跟的多是TabController组件。
  • tabs:具体切换项,是一个数组,里边放的也是Tab Widget。
bottom:TabBar(
  controller: _controller,
  tabs:[
    Tab(icon:Icon(Icons.directions_car)),
    Tab(icon:Icon(Icons.directions_transit)),
    Tab(icon:Icon(Icons.directions_bike)),
  ],
)

基本页面布局

我们先把入口页面布局好,下节课我们再让他保持状态。学了上面两个知识,你其实可以做出来布局了。全部代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Scaffold(
        body: KeepAliveDemo(),
      ),
    );
  }
}

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

  @override
  _KeepAliveDemoState createState() => _KeepAliveDemoState();
}

/*
with是dart的关键字,意思是混入的意思,
就是说可以将一个或者多个类的功能添加到自己的类无需继承这些类,
避免多重继承导致的问题。
SingleTickerProviderStateMixin 主要是我们初始化TabController时,
需要用到vsync ,垂直属性,然后传递this
*/
class _KeepAliveDemoState extends State<KeepAliveDemo>
    with SingleTickerProviderStateMixin {
  late TabController _controller;

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Keep Alive Demo"),
        bottom: TabBar(
          controller: _controller,
          tabs: const [
            Tab(
              icon: Icon(Icons.directions_car),
            ),
            Tab(
              icon: Icon(Icons.directions_transit),
            ),
            Tab(
              icon: Icon(Icons.directions_bike),
            )
          ],
        ),
      ),
      body: TabBarView(
        controller: _controller,
        children: const [
          Text("1111"),
          Text("2222"),
          Text("3333"),
        ],
      ),
    );
  }
}

保持页面状态需要实现AutomaticKeepAliveClientMixin

新建my_home_page.dart页面,代码如下:

import 'package:flutter/material.dart';

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

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

//混入AutomaticKeepAliveClientMixin,这是保持状态的关键
//然后重写wantKeppAlive 的值为true。
class _MyHomePageState extends State<MyHomePage>
    with AutomaticKeepAliveClientMixin {
  int _counter = 0;
  //重写keepAlive 为ture ,就是可以有记忆功能了。
  @override
  bool get wantKeepAlive => true;
  //声明一个内部方法,用来点击按钮后增加数量
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('点一下加1,点一下加1:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.displayMedium,
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: "Increment",
        child: const Icon(Icons.add),
      ),
    );
  }
}

然后进入主入口文件替换body里的children里边的内容

body: TabBarView(
  controller: _controller,
  children: const [
    MyHomePage(),
    MyHomePage(),
    MyHomePage(),
  ],
),

好用的搜索框

效果图:

主入口文件

这个还是继承StatelessWidget,然后在home属性中加入SearchBarDemo,这是一个自定义的Widget,主要代码都在这个文件中。

main.dart 文件的代码如下:

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Scaffold(
        body: SearchBarDemo(),
      ),
    );
  }
}

数据文件 asset.dart

asset.dart相当于数据文件,工作中这些数据是后台传递给我们,或者写成配置文件的,这里我们就以List的方式代替了。我们在这个文件中定义了两个List:

  • searchList : 这个相当于数据库中的数据,我们要在这里进行搜索。

  • recentSuggest : 目前的推荐数据,就是搜索时,自动为我们进行推荐。

整体代码如下 :

const searchList = ["jiejie-大长腿", "jiejie-水蛇腰", "gege1-帅气欧巴", "gege2-小鲜肉"];

const recentSuggest = ["推荐-1", "推荐-2"];

AppBar的样式制作

这节课我们先把第一个搜索界面布好,下节课我们主要作搜索的交互效果。看下面的代码:

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


class SearchBarDemo extends StatefulWidget {
  _SearchBarDemoState createState() => _SearchBarDemoState();
}

class _SearchBarDemoState extends State<SearchBarDemo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar:AppBar(
        title:Text('SearchBarDemo'),
        actions:<Widget>[
          IconButton(
            icon:Icon(Icons.search),
            onPressed: (){
               print('开始搜索');
            }
          ),
        ]
      )
    );

  }
}

上节课已经作好了搜索条的基本布局,现在就是要在点击搜索图标后,变成搜索条的样式,并且有一定都交互效果。

在点击图标时执行searchBarDelegate 类,这个类继承与SearchDelegate类,继承后要重写里边的四个方法。

重写buildActions方法:

buildActions方法时搜索条右侧的按钮执行方法,我们在这里方法里放入一个clear图标。 当点击图片时,清空搜索的内容。

代码如下:

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

buildLeading 方法重写

这个时搜索栏左侧的图标和功能的编写,这里我们才用AnimatedIcon,然后在点击时关闭整个搜索页面,代码如下。

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

buildResults方法重写

buildResults方法,是搜到到内容后的展现,因为我们的数据都是模拟的,所以我这里就使用最简单的Container+Card组件进行演示了,不做过多的花式修饰了。

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

buildSuggestions方法重写

这个方法主要的作用就是设置推荐,就是我们输入一个字,然后自动为我们推送相关的搜索结果,这样的体验是非常好的。(具体代码的解释,请观看视频)

具体代码如下:

 @override
  Widget buildSuggestions(BuildContext context) {
    final suggestionList = query.isEmpty
        ? recentSuggest
        : searchList.where((input) => input.startsWith(query)).toList();
    return ListView.builder(
        itemCount: suggestionList.length,
        itemBuilder: (context, index) => ListTile(
              title: RichText(
                  text: TextSpan(
                      text: suggestionList[index].substring(0, query.length),
                      style: TextStyle(
                          color: Colors.black, fontWeight: FontWeight.bold),
                      children: [
                    TextSpan(
                        text: suggestionList[index].substring(query.length),
                        style: TextStyle(color: Colors.grey))
                  ])),
            ));
  }


}

所有search_bar_demo.dart文件的代码:

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

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

  @override
  _SearchBarDemoState createState() => _SearchBarDemoState();
}

class _SearchBarDemoState extends State<SearchBarDemo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SearchBarDemo"),
        actions: [
          IconButton(
            onPressed: () {
              showSearch(context: context, delegate: SearchBarDelegate());
            },
            icon: const Icon(Icons.search),
          )
        ],
      ),
    );
  }
}

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

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

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

  @override
  Widget buildSuggestions(BuildContext context) {
    final suggestionList = query.isEmpty
        ? recentSuggest
        : searchList.where((input) => input.startsWith(query)).toList();
    return ListView.builder(
      itemCount: suggestionList.length,
      itemBuilder: (context, index) => ListTile(
        title: RichText(
          text: TextSpan(
              text: suggestionList[index].substring(0, query.length),
              style: const TextStyle(
                color: Colors.black,
                fontWeight: FontWeight.bold,
              ),
              children: [
                TextSpan(
                    text: suggestionList[index].substring(query.length),
                    style: const TextStyle(color: Colors.grey))
              ]),
        ),
      ),
    );
  }
}

流式布局 模拟添加照片效果

效果图:

mediaQuery 媒体查询

使用meidaQuery可以很容易的得到屏幕的宽和高,得到宽和高的代码如下:

final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;

Wrap流式布局

Flutter中流式布局大概有三种常用方法,这节课先学一下Wrap的流式布局。有的小伙伴会说Wrap中的流式布局可以用Flow很轻松的实现出来,但是Wrap更多的式在使用了Flex中的一些概念,某种意义上说式跟RowColumn更加相似的。

单行的Wrap跟Row表现几乎一致,单列的Wrap则跟Column表现几乎一致。但Row与Column都是单行单列的,Wrap则突破了这个限制,mainAxis上空间不足时,则向crossAxis上去扩展显示。

从效率上讲,Flow肯定会比Wrap高,但Wrap使用起来会更方便一些。

这个会在实例中用到,所以,我在实例中会讲解这个代码。

GestureDetector 手势操作

GestureDetector它式一个Widget,但没有任何的显示功能,而只是一个手势操作,用来触发事件的。虽然很多Button组件是有触发事件的,比如点击,但是也有一些组件是没有触发事件的,比如:Padding、Container、Center这时候我们想让它有触发事件就需要再它们的外层增加一个GestureDetector,比如我们让Padding有触发事件,代码如下:

Widget buildAddButton(){
    return  GestureDetector(
      onTap:(){
        if(list.length<9){
          setState(() {
                list.insert(list.length-1,buildPhoto());
          });
        }
      },
      child: Padding(
        padding:const EdgeInsets.all(8.0),
        child: Container(
          width: 80.0,
          height: 80.0,
          color: Colors.black54,
          child: Icon(Icons.add),
        ),
      ),
    );
  }

入口文件

入口文件很简单,就是引用了warp_demo.dart文件,然后再home属性中使用了WarpDemo,代码如下:

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Scaffold(
        body: WrapDemo(),
      ),
    );
  }
}

wrap_demo.dart

主要的文件代码我就列在下面了

import 'package:flutter/material.dart';

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

  @override
  _WrapDemoState createState() => _WrapDemoState();
}

class _WrapDemoState extends State<WrapDemo> {
  List<Widget> list = [];

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

  @override
  Widget build(BuildContext context) {
    //得到屏幕的高度和宽度,用来设置Container的宽和高
    final width = MediaQuery.of(context).size.width;
    final height = MediaQuery.of(context).size.height;
    return Scaffold(
      appBar: AppBar(
        title: const Text("wrap流布局"),
      ),
      body: Center(
        child: Opacity(
          opacity: 0.5,
          child: Container(
            width: width,
            height: height / 2,
            color: Colors.grey,
            child: Wrap(
              spacing: 22.0,
              children: list,
            ),
          ),
        ),
      ),
    );
  }

  Widget buildAddButton() {
    return GestureDetector(
      onTap: () {
        if (list.length < 12) {
          setState(() {
            list.insert(list.length - 1, buildPhoto());
          });
        }
      },
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Container(
          width: 80.0,
          height: 80.0,
          color: Colors.black54,
          child: const Center(
            child: Icon(Icons.add),
          ),
        ),
      ),
    );
  }

  Widget buildPhoto() {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        width: 80.0,
        height: 80.0,
        color: Colors.amber,
        child: const Center(
          child: Text('照片'),
        ),
      ),
    );
  }
}

展开闭合案例

效果图:

ExpansionTile组件

ExpansionTile Widget就是一个可以展开闭合的组件,常用的属性有如下几个。

  • title:闭合时显示的标题,这个部分经常使用Text Widget。
  • leading:标题左侧图标,多是用来修饰,让界面显得美观。
  • backgroundColor: 展开时的背景颜色,当然也是有过度动画的,效果非常好。
  • children: 子元素,是一个数组,可以放入多个元素。
  • trailing : 右侧的箭头,你可以自行替换但是我觉的很少替换,因为谷歌已经表现的很完美了。
  • initiallyExpanded: 初始状态是否展开,为true时,是展开,默认为false,是不展开。

main.dart入口文件

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter bottomNavigationBar",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Scaffold(
        body: ExpansionTileDemo(),
      ),
    );
  }
}

expansion_tile.dart 扩展组件

这个文件是我们的主要学习文件,但是并不复杂,就是一个可展开组件。全部代码如下:

import 'package:flutter/material.dart';

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

  @override
  _ExpansionTileDemoState createState() => _ExpansionTileDemoState();
}

class _ExpansionTileDemoState extends State<ExpansionTileDemo> {
  @override
  Widget build(BuildContext context) {
    return const Center(
      child: ExpansionTile(
        title: Text(
          "Expansion Tile",
        ),
        leading: Icon(Icons.access_alarm),
        backgroundColor: Colors.white12,
        children: [
          ListTile(
            title: Text(
              "first line",
            ),
            subtitle: Text("first line subtitle"),
          ),
          ListTile(
            title: Text(
              "second line",
            ),
            subtitle: Text("second line subtitle"),
          )
        ],
      ),
    );
  }
}

展开闭合列表案例

效果图:

ExpansionPanelList 常用属性

  • expansionCallback:点击和交互的回掉事件,有两个参数,第一个是触发动作的索引,第二个是布尔类型的触发值。
  • children:列表的子元素,里边多是一个List数组。

ExpandStateBean 自定义类

为了方便管理制作了一个ExpandStateBean类,里边就是两个状态,一个是是否展开isOpen,另一个索引值。代码如下:

class ExpandStateBean{
  var isOpen;
  var index;
  ExpandStateBean(this.index,this.isOpen);
}

expansion_panel_list_demo.dart

这个文件我就直接上代码了,讲解我会在视频里说明,代码中我也进行了详细的注释。

import 'package:flutter/material.dart';

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

  @override
  _ExpansionPanelListDemoState createState() => _ExpansionPanelListDemoState();
}

class _ExpansionPanelListDemoState extends State<ExpansionPanelListDemo> {
  var currentPanelIndex = -1;
  late List<int> mList; //组成一个int类型数组,用来控制索引
  late List<ExpandStateBean> expandStateList; //开展开的状态列表, ExpandStateBean是自定义的类
  //构造方法,调用这个类的时候自动执行
  _ExpansionPanelListDemoState() {
    mList = [];
    expandStateList = [];
    //便利为两个List进行赋值
    for (int i = 0; i < 20; i++) {
      mList.add(i);
      expandStateList.add(ExpandStateBean(i, false));
    }
  }
  //修改展开与闭合的内部方法
  _setCurrentPanelIndex(int index, isExpand) {
    setState(() {
      expandStateList.forEach((item) {
        if (item.index == index) {
          item.isOpen = !isExpand;
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("expansion panel list"),
      ),
      body: SingleChildScrollView(
        child: ExpansionPanelList(
          expansionCallback: (panelIndex, isExpanded) {
            _setCurrentPanelIndex(panelIndex, isExpanded);
          },
          children: mList.map((index) {
            return ExpansionPanel(
              headerBuilder: (context, isExpanded) {
                return ListTile(
                  title: Text("This is NO $index"),
                );
              },
              body: ListTile(title: Text('expansion no.$index')),
              isExpanded: expandStateList[index].isOpen,
            );
          }).toList(),
        ),
      ),
    );
  }
}

class ExpandStateBean {
  var isOpen;
  var index;
  ExpandStateBean(this.index, this.isOpen);
}

贝塞尔曲线切割

效果图:

[!Note|style:flat] 现在人们对于网站的美感要求是越来越高了,所以很多布局需要优美的曲线设计。当然最简单的办法是作一个PNG的透明图片,然后外边放一个Container.但其内容如果本身就不是图片,只是容器,这种放入图片的做法会让包体变大。其实我们完全可以使用贝塞尔曲线进行切割。

去掉DeBug图标

在讲正式内容之前,先回答小伙伴们的一个问题,就是如何去掉DeBug图标。在我们进行编写代码预览时,有一Debug的图标一直在屏幕上,确实不太美观,其实只要语句代码就可以去掉的。

debugShowCheckedModeBanner: false,

这个代码要配置在主入口文件里,全部代码如下“

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Demo",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: const HomePage(),
    );
  }
}

ClipPath 路径裁切控件

clipPath控件可以把其内部的子控件切割,它有两个主要属性(参数):

  • child :要切割的元素,可以是容器,图片.....
  • clipper : 切割的路径,这个要和CustomClipper对象配合使用。
import 'package:flutter/material.dart';

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

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          ClipPath(
            clipper: BottomClipper(),
            child: Container(
              color: Colors.deepOrange,
              height: 200.0,
            ),
          )
        ],
      ),
    );
  }
}

Scaffold里放置了一个列容器,然后把ClipPath控件放到了里边,ClipPath的子元素是一个容器控件ContainerBootomClipper是我们自定义的一个对象,里边主要就是切割的路径。

CustomClipper 裁切路径

我们主要的贝塞尔曲线路径就写在getClip方法里,它返回一段路径。

一个二阶的贝塞尔曲线是需要控制点和终点的,控制点就像一块磁铁,把直线吸引过去,形成一个完美的弧度,这个弧度就是贝塞尔曲线了。

我们先来熟悉一下裁切路径和贝塞尔曲线,作一个最简单的贝塞尔曲线出来。

全部代码如下:

import 'package:flutter/material.dart';

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

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          ClipPath(
            clipper: BottomClipper(),
            child: Container(
              color: Colors.deepOrange,
              height: 200.0,
            ),
          )
        ],
      ),
    );
  }
}

class BottomClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path();
    path.lineTo(0, 0);
    path.lineTo(0, size.height - 50);
    var firstControlPoint = Offset(size.width / 2, size.height);
    var firstEndPoint = Offset(size.width, size.height - 50);
    path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy,
        firstEndPoint.dx, firstEndPoint.dy);
    path.lineTo(size.width, size.height - 50);
    path.lineTo(size.width, 0);
    return path;
  }

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

波浪形式的贝塞尔曲线

效果图:

这节课主要改造上节课的代码,作一个波浪形的贝塞尔裁切。波浪形式的只要把裁切变成两个对称的贝塞尔曲线就可以实现了。代码如下:

import 'package:flutter/material.dart';

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

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          ClipPath(
            clipper: BottomClipper(),
            child: Container(
              color: Colors.deepOrange,
              height: 200.0,
              width: 360.0,
              child: Image.network(
                "https://pic.tianqinote.com/classroom_1.png",
                fit: BoxFit.scaleDown,
              ),
            ),
          )
        ],
      ),
    );
  }
}

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

    var firstControlPoint = Offset(size.width / 4, size.height);
    var firstEndPoint = Offset(size.width / 2.25, size.height - 30);

    path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy,
        firstEndPoint.dx, firstEndPoint.dy);

    var secondControlPoint = Offset(size.width / 4 * 3, size.height - 80);
    var secondEndPoint = Offset(size.width, size.height - 40);

    path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy,
        secondEndPoint.dx, secondEndPoint.dy);

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

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

打开应用的闪屏动画案例

效果图:

[!Tip|style:flat] 打开一个APP,经常会看到精美的启动页,这种启动页也称为闪屏动画。它是从无到有有一个透明度的渐变动画的。图像展示完事后,才跳转到用户可操作的页面。这节课主要学习一下闪屏动画的制作。

AnimationController

AnimationControllerAnimation的一个子类,它可以控制Animation, 也就是说它是来控制动画的,比如说控制动画的执行时间。

我们这里有了两个参数 :

  • vsync:this :垂直同步设置,使用this就可以了。
  • duration : 动画持续时间,这个可以使用seconds秒,也可以使用milliseconds毫秒,工作中经常使用毫秒,因为秒还是太粗糙了。

来看一段代码,这段代码就是控制动画的一个典型应用。

 _controller = AnimationController(vsync:this,duration:Duration(milliseconds:3000));
_animation = Tween(begin: 0.0,end:1.0).animate(_controller);

这段代码的意思是,设置一个动画控制器,这个动画控制器控制动画执行时间是3000毫秒。然后我们设置了一段动画,动画使用了动画控制器的约定。

animation.addStatusListener

animation.addStatusListener动画事件监听器,它可以监听到动画的执行状态,我们这里只监听动画是否结束,如果结束则执行页面跳转动作。

_animation.addStatusListener((status){
  if(status == AnimationStatus.completed){
    Navigator.of(context).pushAndRemoveUntil(
      MaterialPageRoute(builder: (context)=>MyHomePage()), 
      (route)=> route==null);
  }
});
  • AnimationStatus.completed:表示动画已经执行完毕。
  • pushAndRemoveUntil:跳转页面,并销毁当前控件。

案例重要代码

我们会了上边的知识点,做出案例就没什么问题了。我把复杂的代码都作了注释,小伙伴们应该可以看懂。

main.dart文件

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Demo",
      theme: ThemeData(
        primarySwatch: Colors.deepOrange,
      ),
      debugShowCheckedModeBanner: false,
      home: const SplashScreen(),
    );
  }
}

splash_screen.dart文件

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

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

  @override
  _SplashScreenState createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 3000));
    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller);

    /*动画事件监听器,
    它可以监听到动画的执行状态,
    我们这里只监听动画是否结束,
    如果结束则执行页面跳转动作。 */
    _animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        Navigator.of(context).pushAndRemoveUntil(
            MaterialPageRoute(builder: (context) => const HomePage()),
            (route) => route == null);
      }
    });

    // 播放动画
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      //透明度动画组件
      opacity: _animation, //执行动画
      child: Image.network(
          //网络图片
          'https://pic.tianqinote.com/Activity生命周期.png',
          scale: 2.0, //进行缩放
          fit: BoxFit.cover // 充满父容器
          ),
    );
  }
}

home_page.dart文件

import 'package:flutter/material.dart';

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

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("HomePage"),
        elevation: 0.0,
      ),
      body: Column(
        children: [
          ClipPath(
            clipper: BottomClipper(),
            child: Container(
              color: Colors.deepOrange,
              height: 200.0,
              width: 360.0,
              child: Image.network(
                "https://pic.tianqinote.com/classroom_1.png",
                fit: BoxFit.scaleDown,
              ),
            ),
          )
        ],
      ),
    );
  }
}

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

    var firstControlPoint = Offset(size.width / 4, size.height);
    var firstEndPoint = Offset(size.width / 2.25, size.height - 30);

    path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy,
        firstEndPoint.dx, firstEndPoint.dy);

    var secondControlPoint = Offset(size.width / 4 * 3, size.height - 80);
    var secondEndPoint = Offset(size.width, size.height - 40);

    path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy,
        secondEndPoint.dx, secondEndPoint.dy);

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

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

右滑返回上一页案例

效果图:

Cupertino UI

其实早都知道Flutter有两套UI模板,一套是material,另一套就是Cupertino。Cupertino主要针对的的就是IOS系统的UI,所以用的右滑返回上一级就是在这个Cupertino里。

Cupertino的引入方法

直接使用import引入就可以了,代码如下:

import 'package:flutter/cupertino.dart';

引入了cupertino的包之后,就可以使用皮肤和交互效果的特性了。要用的右滑返回上一页也是皮肤的交互特性,直接使用就可以了。

CupertinoPageScaffold

这个和以前使用materialScaffold类似,不过他里边的参数是child,例如下面的代码.

return CupertinoPageScaffold(
    child: Center(
      child: Container(
        height: 100.0,
        width: 100.0,
        color: CupertinoColors.activeBlue,
        child: CupertinoButton(
          onPressed: () {
            Navigator.of(context)
                .push(CupertinoPageRoute(builder: (BuildContext context) {
              return const RightBackDemo();
            }));
          },
          child: const Icon(CupertinoIcons.add),
        ),
      ),
    ),
  );
}

Cupertino下也有很多Widget控件,他们都是以Cupertino开头的,这就让我们很好区分,当然两种皮肤是可以进行混用的。

整个案例代码

这个案例就两个主要文件,main.dartright_back_demo.dart,具体解释可以看视频,代码如下:

main.dart文件代码

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Demo",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const RightBackDemo(),
    );
  }
}

right_back_demo.dart文件

import 'package:flutter/cupertino.dart';

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

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

其实只要使用CupertinoPageRoute打开的子页面,就可以实现右滑返回上一级

ToolTip控件实例

效果图:

轻量级操作提示

其实Flutter中有很多提示控件,比如DialogSnackbarBottomSheet这些操作都是比较重量级的,存在屏幕上的时间较长或者会直接打断用户的操作。

当然我并不是说这些控件不好,根据需求的不同,要有多种选择,所以才会给大家讲一下轻量级操作提示Tooltip

Tooltip是继承于StatefulWidget的一个Widget,它并不需要调出方法,当用户长按被Tooltip包裹的Widget时,会自动弹出相应的操作提示。

main.dart文件

这节课就用最简单代码给大家做一个轻量级提示案例出来。入口文件代码如下:

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Demo",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ToolTipsDemo(),
    );
  }
}

tool_tips_demo.dart文件

这个文件主要的用途就是承载轻提示控件,代码如下:

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ToolTipsDemo"),
      ),
      body: Center(
        child: Tooltip(
            message: "不要骗我,我很爱打人!",
            child: Container(
              width: 200.0,
              height: 200.0,
              alignment: Alignment.center,
              child: Image.network("https://pic.tianqinote.com/classroom_1.png"),
            )),
      ),
    );
  }
}

Draggable控件实例

效果图:

Draggable Widget

Draggable控件负责就是拖拽,父层使用了Draggable,它的子元素就是可以拖动的,子元素可以实容器,可以是图片。用起来非常的灵活。

参数说明:

  • data: 是要传递的参数,在DragTarget里,会接受到这个参数。当然要在拖拽控件推拽到DragTarget的时候。
  • child:在这里放置你要推拽的元素,可以是容器,也可以是图片和文字。
  • feedback: 常用于设置推拽元素时的样子,在案例中当推拽的时候,我们把它的颜色透明度变成了50%。当然你还可以改变它的大小。
  • onDraggableCanceled:是当松开时的相应事件,经常用来改变推拽时到达的位置,改变时用setState来进行。

代码:

Draggable(
  data:widget.widgetColor,
  child: Container(
    width: 100,
    height: 100,
    color:widget.widgetColor,
  ),
  feedback:Container(
    width: 100.0,
    height: 100.0,
    color: widget.widgetColor.withOpacity(0.5),
  ),
  onDraggableCanceled: (Velocity velocity, Offset offset){
    setState(() {
      this.offset = offset;
    });
  },

DragTarget Widget

DragTarget是用来接收拖拽事件的控件,当把Draggable放到DragTarget里时,他会接收Draggable传递过来的值,然后用生成器改变组件状态。

  • onAccept:当推拽到控件里时触发,经常在这里得到传递过来的值。
  • builder: 构造器,里边进行修改child值。
DragTarget(onAccept: (Color color) {
  _draggableColor = color;
}, builder: (context, candidateData, rejectedData) {
  return Container(
    width: 200.0,
    height: 200.0,
    color: _draggableColor,
  );
}),

实例代码DEMO

main.dart 文件:

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

void main() => runApp(MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Demo",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const DraggableDemo(),
    );
  }
}

draggable_demo.dart 文件

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

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

  @override
  _DraggableDemoState createState() => _DraggableDemoState();
}

class _DraggableDemoState extends State<DraggableDemo> {
  Color _draggableColor = Colors.grey;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          const DraggableWidget(
            offset: Offset(80.0, 80.0),
            widgetColor: Colors.tealAccent,
          ),
          const DraggableWidget(
            offset: Offset(180.0, 80.0),
            widgetColor: Colors.redAccent,
          ),
          Center(
            child: DragTarget(
              onAccept: (Color color) {
                _draggableColor = color;
              },
              builder: (context, candidateData, rejectedData) {
                return Container(
                  width: 200.0,
                  height: 200.0,
                  color: _draggableColor,
                );
              },
            ),
          )
        ],
      ),
    );
  }
}

draggable_widget.dart 文件

import 'package:flutter/material.dart';

class DraggableWidget extends StatefulWidget {
  final Offset offset;
  final Color widgetColor;
  const DraggableWidget(
      {Key? key, required this.offset, required this.widgetColor})
      : super(key: key);

  @override
  _DraggableWidgetState createState() => _DraggableWidgetState();
}

class _DraggableWidgetState extends State<DraggableWidget> {
  Offset offset = Offset(0.0, 0.0);
  @override
  void initState() {
    super.initState();
    offset = widget.offset;
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: offset.dx,
      top: offset.dy,
      child: Draggable(
        data: widget.widgetColor,
        feedback: Container(
          width: 100.0,
          height: 100.0,
          color: widget.widgetColor.withOpacity(0.5),
        ),
        onDraggableCanceled: (Velocity velocity, Offset offset) {
          setState(() {
            this.offset = offset;
          });
        },
        child: Container(
          width: 100,
          height: 100,
          color: widget.widgetColor,
        ),
      ),
    );
  }
}

results matching ""

    No results matching ""