Flutter基础

     Flutter页面-基础Widget

​        + 在Flutter中,几乎所有的对象都是一个`Widget`,与原生开发中的**控件**不同的是,Flutter中的`widget`的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 `GestureDetector` widget、用于应用主题数据传递的`Theme`等等。由于Flutter主要就是用于构建用户界面的,所以,在大多数时候,可以认为widget就是一个控件,不必纠结于概念。

​    Widget的功能是“描述一个UI元素的配置数据”,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据。实际上,Flutter中真正代表屏幕上显示元素的类是`Element`,也就是说Widget只是描述`Element`的一个配置。一个Widget可以对应多个`Element`,这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个节点都会对应一个`Element`对象。

​    

 Widget

`StatelessWidget`和`StatefulWidget`是`flutter`的基础组件,日常开发中自定义`Widget`都是选择继承这两者之一。也是在往后的开放中,我们最多接触的Widget:

- ​ `StatelessWidget`:无状态的,展示信息,面向那些始终不变的UI控件;

-  `StatefulWidget`:有状态的,可以通过改变状态使得 UI 发生变化,可以包含用户交互(比如弹出一个 dialog)。


在实际使用中,Stateless与Stateful的选择需要取决于这个 Widget 是有状态还是无状态,简单来说看界面是否需要更新。

Stateless Widget

​    StatelessWidget用于不需要维护状态的场景,它通常在`build`方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。

> `BuildContext`表示构建widget的上下文,它是操作widget在树中位置的一个句柄,它包含了一些查找、遍历当前Widget树的一些方法。每一个widget都有一个自己的context对象。
 

import 'package:flutter/material.dart';

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

class StatelessApp extends StatelessWidget {
  ///在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget
  @override
  Widget build(BuildContext context) {
    //嵌套 MaterialApp:封装了应用程序实现Material Design所需要的一些widget
    return MaterialApp(
        title: "Widget演示", //标题,显示在recent时候的标题
        //主页面
        //Scaffold : Material Design布局结构的基本实现。
        home: Scaffold(
          //ToolBar/ActionBar
          appBar: AppBar(title: Text("Widget")),
          body: Text("Hello,Flutter!"),
        )
    );
  }
}

Material Design:

> 一种设计语言,Material Design 于2014年的 Google I/O 首次亮相,是谷歌推出的全新的设计语言。说白了,就是一种设计风格。

Stateful Widget

​       StatefulWidget是动态的,添加了一个新的接口`createState()`用于创建和Stateful widget相关的状态`State`,它在Stateful widget的生命周期中可能会被多次调用。

​    当State被改变时,可以手动调用其`setState()`方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其`build`方法重新构建widget树,从而达到更新UI的目的。
 

class StatefulState extends State<StatefulApp> {
  int _i;

  ///当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调
  @override
  void initState() {
    super.initState();
    _i = 1;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: "Widget演示",
        theme: ThemeData(),
        home: Scaffold(
          appBar: AppBar(title: Text("Widget")),
          body: RaisedButton(
            onPressed: () {
              //修改状态,setState会重新调用build更新ui
              setState(() {
                _i++;
              });
            },
            child: Text("Hello,Flutter! $_i"),
          ),
        ));
  }
}

 State生命周期

​    State类除了`build`之外还提供了很多方法能够让我们重写,这些方法会在不同的状态下由Flutter调起执行,所以这些方法我们就称之为**生命周期**方法。
 

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool isShowChild;

  ///当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调
  @override
  void initState() {
    super.initState();
    isShowChild = true;
    debugPrint("parent initState......");
  }

  ///初始化时,在initState()之后立刻调用
  ///当依赖的InheritedWidget rebuild,会触发此接口被调用
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint("parent didChangeDependencies......");
  }

  ///绘制界面,当setState触发的时候会再次被调用
  @override
  Widget build(BuildContext context) {
    debugPrint("parent build......");
    return MaterialApp(
      home: Scaffold(
        body: Center(
            child: RaisedButton(
          onPressed: () {
            setState(() {
              isShowChild = !isShowChild;
            });
          },
          child: isShowChild ? Child() : Text("演示移除Child"),
        )),
      ),
    );
  }

  ///状态改变的时候会调用该方法,比如调用了setState
  @override
  void didUpdateWidget(MyApp oldWidget) {
    super.didUpdateWidget(oldWidget);
    debugPrint("parent didUpdateWidget......");
  }

  ///当State对象从树中被移除时,会调用此回调
  @override
  void deactivate() {
    super.deactivate();
    debugPrint('parent deactivate......');
  }

  ///当State对象从树中被永久移除时调用;通常在此回调中释放资源
  @override
  void dispose() {
    super.dispose();
    debugPrint('parent dispose......');
  }
}

class Child extends StatefulWidget {
  @override
  _ChildState createState() => _ChildState();
}

class _ChildState extends State<Child> {
  @override
  Widget build(BuildContext context) {
    debugPrint("child build......");
    return Text('lifeCycle');
  }

  @override
  void initState() {
    super.initState();
    debugPrint("child initState......");
  }

  ///初始化时,在initState()之后立刻调用
  ///当依赖的InheritedWidget rebuild,会触发此接口被调用
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint("child didChangeDependencies......");
  }

  ///父widget状态改变的时候会调用该方法,比如父节点调用了setState
  @override
  void didUpdateWidget(Child oldWidget) {
    super.didUpdateWidget(oldWidget);
    debugPrint("child didUpdateWidget......");
  }

  ///当State对象从树中被移除时,会调用此回调
  @override
  void deactivate() {
    super.deactivate();
    debugPrint('child deactivate......');
  }

  ///当State对象从树中被永久移除时调用;通常在此回调中释放资源
  @override
  void dispose() {
    super.dispose();
    debugPrint('child dispose......');
  }
}


 

执行的输出结果显示为:

- 运行到显示

```dart
I/flutter (22218): parent initState......
I/flutter (22218): parent didChangeDependencies......
I/flutter (22218): parent build......
I/flutter (22218): child initState......
I/flutter (22218): child didChangeDependencies......
I/flutter (22218): child build......
```

- 点击按钮会移除Child

```dart
I/flutter (22218): parent build......
I/flutter (22218): child deactivate......
I/flutter (22218): child dispose......
```

-  将MyApp的代码由` child: isShowChild ? Child() : Text("演示移除Child")`,改为` child: Child()`,点击按钮时

```dart
I/flutter (22765): parent build......
I/flutter (22765): child didUpdateWidget......
I/flutter (22765): child build......
```

从这些实验中能够得出State的生命周期

基础widget文本显示

Text

`Text`是展示单一格式的文本Widget(Android `TextView`)。

import 'package:flutter/material.dart';

///
/// main方法 调用runApp传递Widget,这个Widget成为widget树的根
void main() => runApp(TextApp());

///
/// 1、单一文本Text
///
//创建一个无状态的Widget
class TextApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //封装了应用程序实现Material Design所需要的一些widget
    return MaterialApp(
      title: "Text演示", //标题,显示在recent时候的标题
      //主页面
      //Scaffold : Material Design布局结构的基本实现。
      home: Scaffold(
        //ToolBar/ActionBar
        appBar: AppBar(title: Text("Text")),
        body: Text("Hello,Flutter"),
      ),
    );
  }
}

在使用`Text`显示文字时候,可能需要对文字设置各种不同的样式,类似Android的 `android:textColor/Size`等

在Flutter中也拥有类似的属性

Widget _TextBody() {
  return Text(
    "Hello,Flutter",
    style: TextStyle(
        //颜色
        color: Colors.red,
        //字号 默认14
        fontSize: 18,
        //粗细
        fontWeight: FontWeight.w800,
        //斜体
        fontStyle: FontStyle.italic,
        //underline:下划线,overline:上划线,lineThrough:删除线
        decoration: TextDecoration.lineThrough,
        decorationColor: Colors.black,
        //solid:实线,double:双线,dotted:点虚线,dashed:横虚线,wavy:波浪线
        decorationStyle: TextDecorationStyle.wavy),
  );
}

class TextApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Text演示",
      home: Scaffold(
        appBar: AppBar(title: Text("Text")),
        body: _TextBody(),
      ),
    );
  }
}

RichText

如果需要显示更为丰富样式的文本(比如一段文本中文字不同颜色),可以使用`RichText`或者`Text.rich`


Widget _RichTextBody() {
  var textSpan = TextSpan(
    text: "Hello",
    style: TextStyle(color: Colors.red),
    children: [
      TextSpan(text: "Flu", style: TextStyle(color: Colors.blue)),
      TextSpan(text: "uter", style: TextStyle(color: Colors.yellow)),
    ],
  );
  //Text.rich(textSpan);
  return RichText(text: textSpan);
}

DefaultTextStyle

​    在widget树中,文本的样式默认是可以被继承的,因此,如果在widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式。相当于在Android中定义 Theme

Widget _DefaultStyle(){
  DefaultTextStyle(
    //设置文本默认样式
    style: TextStyle(
      color:Colors.red,
      fontSize: 20.0,
    ),
    textAlign: TextAlign.start,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text("Hello Flutter!"),
        Text("Hello Flutter!"),
        Text("Hello Flutter!",
          style: TextStyle(
              inherit: false, //不继承默认样式
              color: Colors.grey
          ),
        ),
      ],
    ),
  );
}

图片显示

"图文":有文字显示Widget,又怎么少的了图片呢。

FlutterLogo

​    这个Widget用于显示Flutter的logo......

Widget flutterLogo() {
  return FlutterLogo(
    //大小
    size: 100,
    //logo颜色 默认为 Colors.blue
    colors: Colors.red,
    //markOnly:只显示logo,horizontal:logo右边显示flutter文字,stacked:logo下面显示文字
    style: FlutterLogoStyle.stacked,
    //logo上文字颜色
    textColor: Colors.blue,
  );
}

Icon    

主要用于显示内置图标的`Widget`

Widget icon() {
  return Icon(
      //使用预定义Material icons
      // https://docs.flutter.io/flutter/material/Icons-class.html
      Icons.add,
      size: 100,
      color: Colors.red);
}

Image

显示图片的`Widget`。图片常用的格式主要有bmp,jpg,png,gif,webp等,Android中并不是天生支持gif和webp动图,但是这一特性在flutter中被很好的支持了。

| 方式          | 解释                                                        |
| ------------- | ----------------------------------------------------------- |
| Image()       | 使用ImageProvider提供图片,如下方法本质上也是使用的这个方法 |
| Image.asset   | 加载资源图片                                                |
| Image.file    | 加载本地图片文件                                            |
| Image.network | 加载网络图片                                                |
| Image.memory  | 加载内存图片                                                |

Iamge.asset

在工程目录下创建目录,如:assets,将图片放入此目录。打开项目根目录:pubspec.yaml

return MaterialApp(
      title: "Image演示",
      home: Scaffold(
        appBar: AppBar(title: Text("Image")),
        body: Image.asset("assets/banner.jpeg"),
      ),
    );

Image.file

在sd卡中放入一张图片。然后利用**path_provider**库获取sd卡根目录(Dart库版本可以在:https://pub.dartlang.org/packages查询)。

**注意权限**

class ImageState extends State<ImageApp> {
  Image image;

  @override
  void initState() {
    super.initState();
    getExternalStorageDirectory().then((path) {
      setState(() {
        image = Image.file(File("${path.path}${Platform.pathSeparator}banner.jpeg"));
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Image演示",
      home: Scaffold(
        appBar: AppBar(title: Text("Image")),
        body: image,
      ),
    );
  }
}

Image.network

直接给网络地址即可。

> Flutter 1.0,加载https时候经常出现证书错误。必须断开AS打开app

Image.memory

Future<List<int>> _imageByte() async {
  String path = (await getExternalStorageDirectory()).path;
  return await File("$path${Platform.pathSeparator}banner.jpeg").readAsBytes();
}

class ImageState extends State<ImageApp> {
  Image image;

  @override
  void initState() {
    super.initState();
    _imageByte().then((bytes) {
      setState(() {
        image = Image.memory(bytes);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Image演示",
      home: Scaffold(
        appBar: AppBar(title: Text("Image")),
        body: image,
      ),
    );
  }
}

fit属性相当于android中的scaletype,定义如下:

| fit              | 说明                                                         | 效果                                                         |
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| BoxFit.fill      | 填充,忽略原有的宽高比,填满为止                             | ![基础Widget_Image_Fill](图片/基础Widget_Image_Fill.png)     |
| BoxFit.contain   | 包含,不改变原有比例让容器包含整个图片,容器多余部分填充背景 | ![基础Widget_Image_Fill](图片/基础Widget_Image_Contain.png)  |
| BoxFit.cover     | 覆盖,不改变原有比例,让图片充满整个容器,图片多余部分裁剪   | ![基础Widget_Image_Fill](图片/基础Widget_Image_Cover.png)    |
| BoxFit.fitWidth  | 横向图片填充                                                 | ![基础Widget_Image_Fill](图片/基础Widget_Image_FitWidth.png) |
| BoxFit.fitHeight | 纵向图片填充                                                 | ![基础Widget_Image_Fill](图片/基础Widget_Image_FitHeight.png) |
| BoxFit.none      | 原始大小居中                                                 | ![基础Widget_Image_Fill](图片/基础Widget_Image_none.png)     |
| BoxFit.scaleDown | 图片大小小于容器事相当于none,图片大小大于容器时缩小图片大小实现contain | ![基础Widget_Image_Fill](图片/基础Widget_Image_ScaleDown.png) |

CircleAvatar

主要用来显示用户的头像,任何图片都会被剪切为圆形。

CircleAvatar(
      //图片提供者 ImageProvider
      backgroundImage: AssetImage("assets/banner.jpeg"),
      //半径,控制大小
      radius: 50.0,
    );

 FadeInImage

当使用默认`Image` widget显示图片时,您可能会注意到它们在加载完成后会直接显示到屏幕上。这可能会让用户产生视觉突兀。如果最初显示一个占位符,然后在图像加载完显示时淡入,我们可以使用`FadeInImage`来达到这个目的!

 image =  FadeInImage.memoryNetwork(
      placeholder: kTransparentImage,
      image: 'https://flutter.io/images/homepage/header-illustration.png',
    );

按钮

Material widget库中提供了多种按钮Widget如RaisedButton、FlatButton、OutlineButton等,它们都是直接或间接对RawMaterialButton的包装定制,所以他们大多数属性都和`RawMaterialButton`一样。所有Material 库中的按钮都有如下相同点:

1. 按下时都会有“水波动画”。
2. 有一个`onPressed`属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。

RaisedButton

"漂浮"按钮,它默认带有阴影和灰色背景

RaisedButton(
          child: Text("normal"),
          onPressed: () => {},
        )

FlatButton

扁平按钮,默认背景透明并不带阴影

FlatButton(
  child: Text("normal"),
  onPressed: () => {},
)

OutlineButton

默认有一个边框,不带阴影且背景透明。

OutlineButton(
  child: Text("normal"),
  onPressed: () => {},
)

IconButton

可点击的Icon

IconButton(
  icon: Icon(Icons.thumb_up),
  onPressed: () => {},
)

按钮外观可以通过其属性来定义,不同按钮属性大同小异

const FlatButton({
  ...  
  @required this.onPressed, //按钮点击回调
  this.textColor, //按钮文字颜色
  this.disabledTextColor, //按钮禁用时的文字颜色
  this.color, //按钮背景颜色
  this.disabledColor,//按钮禁用时的背景颜色
  this.highlightColor, //按钮按下时的背景颜色
  this.splashColor, //点击时,水波动画中水波的颜色
  this.colorBrightness,//按钮主题,默认是浅色主题
  this.padding, //按钮的填充
  this.shape, //外形
  @required this.child, //按钮的内容
})
    
FlatButton(
  onPressed: () => {},
  child: Text("Raised"),
  //蓝色
  color: Colors.blue,
  //水波
  splashColor: Colors.yellow,
  //深色主题,这样文字颜色会变成白色
  colorBrightness: Brightness.dark,
   //圆角按钮
  shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(50)
  ),
)

而`RaisedButton`,默认配置有阴影,因此在配置`RaisedButton` 时,拥有一系列 elevation 属性的配置

const RaisedButton({
  ...
  this.elevation = 2.0, //正常状态下的阴影
  this.highlightElevation = 8.0,//按下时的阴影
  this.disabledElevation = 0.0,// 禁用时的阴影
  ...
}

输入框

import 'package:flutter/material.dart';

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

class Demo1 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Demo1",
      home: Scaffold(
        appBar: AppBar(
          title: Text("登录"),
        ),
        //线性布局,垂直方向
        body: Column(
          children: <Widget>[
            TextField(
              //自动获得焦点
              autofocus: true,
              decoration: InputDecoration(
                  labelText: "用户名",
                  hintText: "用户名或邮箱",
                  prefixIcon: Icon(Icons.person)),
            ),
            TextField(
              //隐藏正在编辑的文本
              obscureText: true,
              decoration: InputDecoration(
                  labelText: "密码",
                  hintText: "您的登录密码",
                  prefixIcon: Icon(Icons.lock)),
            ),
          ],
        ),
      ),
    );
  }
}

这个效果非常的“系统”,我们可能大多数情况下需要将下划线更换为矩形边框,这时候可能就需要组合widget来完成:

//容器 设置一个控件的尺寸、背景、margin
Container(
    margin: EdgeInsets.all(32),
    child: TextField(
        keyboardType: TextInputType.emailAddress,
        decoration: InputDecoration(
            labelText: "用户名",
            hintText: "用户名或邮箱",
            prefixIcon: Icon(Icons.person),
            border: InputBorder.none //隐藏下划线
            )),
    //装饰
    decoration: BoxDecoration(
      // 边框浅灰色,宽度1像素
      border: Border.all(color: Colors.red[200], width: 1.0),
      //圆角
      borderRadius: BorderRadius.circular(5.0),
    ),
  )

焦点控制

​    FocusNode: 与Widget绑定,代表了这个Widget的焦点

​    FocusScope: 焦点控制范围

​    FocusScopeNode:控制焦点

class _TextFocusState extends State<TextFocusWidget> {
  FocusNode focusNode1 = new FocusNode();
  FocusNode focusNode2 = new FocusNode();

  void _listener() {
    debugPrint("用户名输入框焦点:${focusNode1.hasFocus}");
  }

  @override
  void initState() {
    super.initState();
    //监听焦点状态改变事件
    focusNode1.addListener(_listener);
  }

  @override
  void dispose() {
    super.dispose();
    focusNode1.dispose();
    focusNode2.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        TextField(
          autofocus: true,
          //关联焦点
          focusNode: focusNode1,
          //设置键盘动作为: 下一步
          textInputAction: TextInputAction.next,
          //点击下一步执行回调
          onEditingComplete: () {
            //获得 context对应UI树的焦点范围 的焦点控制器
            FocusScopeNode focusScopeNode = FocusScope.of(context);
            //将焦点交给focusNode2
            focusScopeNode.requestFocus(focusNode2);
          },
          decoration: InputDecoration(
              labelText: "用户名",
              hintText: "用户名或邮箱",
              prefixIcon: Icon(Icons.person)),
        ),
        TextField(
          //隐藏正在编辑的文本
          obscureText: true,
          focusNode: focusNode2,
          decoration: InputDecoration(
              labelText: "密码",
              hintText: "您的登录密码",
              prefixIcon: Icon(Icons.lock)),
        ),
        custom(),
      ],
    );
  }
}

获取输入内容

获取输入内容有两种方式:

1. 定义两个变量,用于保存用户名和密码,然后在onChange触发时,各自保存一下输入内容。
2. 通过controller直接获取。

onChange获得输入内容:

TextField(
      onChanged: (s) => debugPrint("ssss:$s"),
    )

controller获取:

定义一个controller:

//定义一个controller
TextEditingController _unameController=new TextEditingController();
```

然后设置输入框controller:

```dart
TextField(
    controller: _unameController, //设置controller
    ...
)

通过controller获取输入框内容

debugPrint(_unameController.text)

TextFormField

`TextFormField`比`TextField`多了一些属性,其中 ***validator***用于设置验证回调。在单独使用时与`TextField`没有太大的区别。当结合`From`,利用`From`可以对输入框进行分组,然后进行一些统一操作(验证)

class _TextFocusState extends State<TextFocusWidget> {
  //全局key
  GlobalKey<FormState> _key = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
        //类似 id
        key: _key,
        child: Column(
          children: <Widget>[
            TextFormField(
                autofocus: true,
                decoration: InputDecoration(
                    labelText: "用户名",
                    hintText: "用户名或邮箱",
                    icon: Icon(Icons.person)),
                // 校验用户名
                validator: (v) {
                  return v.trim().length > 0 ? null : "用户名不能为空";
                }),
            TextFormField(
                decoration: InputDecoration(
                    labelText: "密码",
                    hintText: "您的登录密码",
                    icon: Icon(Icons.lock)),
                // 校验用户名
                validator: (v) {
                  return v.trim().length > 0 ? null : "密码不能为空";
                }),
            RaisedButton(
              onPressed: () {
                //Form所有TextFormField成功 返回true
                if (_key.currentState.validate()) {
                  
                }
              },
              child: Text("提交"),
            )
          ],
        ));
  }
}


 第一个页面

入口函数

一个Flutter工程的入口函数与Dart命令行工程一样是`main`,不同的是在Flutter中执行`runApp(ArticleApp())` 就能够在手机屏幕上展示这个Widget。

import 'package:flutter/material.dart';
void main() => runApp(new ArticleApp());

ArticleApp

我们要实现的文章列表页面UI就在`ArticleApp`中定义:

class ArticleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(
            '文章',
            style: const TextStyle(color: Colors.white),
          ),
        ),
        body: new ArticlePage(),
      ),
    );
  }
}

 

`build`方法中返回的就是我们需要显示在屏幕上的widget。`MaterialApp`代表使用Material Design风格,这是一个封装了很多Android MD设计所必须要的组件的小部件。假设我们需要显示一个`Text`,而没有包裹在`MaterialApp`内:

class ArticleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //Center:摆放在中间
    return Center(
      child: Text("你好!"),
    );
  }
}

如果直接运行则会出现异常,因为Flutter不知道以什么顺序摆放文字

因此我们不得不给`Text`指名`textDirection`属性:

class ArticleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text("你好!",
          style: const TextStyle(color: Colors.white),
          textDirection: TextDirection.ltr),
    );
  }
}

但是如果包含在`MaterialApp`当中我们就不需要指名类似`textDirection`这样的属性了,因为内部已经内置了一套风格,指明了这些必须属性。而`Scaffold`则实现了基本的 Material Design布局结构,在 Material 设计中定义的单个界面上的各种布局元素,在 Scaffold 中都支持。比如:AppBar、抽屉菜单、BottomNavigationBar等等。

ArticlePage

​    在我们的布局中指定了`Scaffold`的body(主体)为`ArticlePage`,这是一个我们自定义的组合Widget。

class ArticlePage extends StatefulWidget {
  @override
  _ArticlePageState createState() => _ArticlePageState();
}

class _ArticlePageState extends State<ArticlePage> {
  ///滑动控制器
  ScrollController _controller = new ScrollController();

  ///控制小菊花的显示
  bool _isLoading = true;

  ///请求到的文章数据
  List articles = [];

  ///banner图
  List banners = [];

  ///总文章数有多少
  var listTotalSize = 0;

  ///分页加载,当前页码
  var curPage = 0;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      ///获得 SrollController 监听控件可以滚动的最大范围
      var maxScroll = _controller.position.maxScrollExtent;

      ///获得当前位置的像素值
      var pixels = _controller.position.pixels;

      ///当前滑动位置到达底部,同时还有更多数据
      if (maxScroll == pixels && articles.length < listTotalSize) {
        ///加载更多
        _getArticlelist();
      }
    });
    _pullToRefresh();
  }

  _getArticlelist([bool update = true]) async {
    /// 请求成功是map,失败是null
    var data = await Api.getArticleList(curPage);
    if (data != null) {
      var map = data['data'];
      var datas = map['datas'];

      ///文章总数
      listTotalSize = map["total"];

      if (curPage == 0) {
        articles.clear();
      }
      curPage++;
      articles.addAll(datas);

      ///更新ui
      if (update) {
        setState(() {});
      }
    }
  }

  _getBanner([bool update = true]) async {
    var data = await Api.getBanner();
    if (data != null) {
      banners.clear();
      banners.addAll(data['data']);
      if (update) {
        setState(() {});
      }
    }
  }

  ///下拉刷新
  Future<void> _pullToRefresh() async {
    curPage = 0;
    Iterable<Future> futures = [_getArticlelist(), _getBanner()];
    await Future.wait(futures);
    _isLoading = false;
    setState(() {});
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        ///小菊花
        Offstage(
          offstage: !_isLoading, //是否隐藏
          child: new Center(child: CircularProgressIndicator()),
        ),

        ///内容
        Offstage(
          offstage: _isLoading,
          child: new RefreshIndicator(
              child: ListView.builder(
                itemCount: articles.length + 1,
                itemBuilder: (context, i) => _buildItem(i),
                controller: _controller,
              ),
              onRefresh: _pullToRefresh),
        )
      ],
    );
  }

  Widget _buildItem(int i) {
    if (i == 0) {
      return new Container(
        height: 180.0,
        child: _bannerView(),
      );
    }
    var itemData = articles[i - 1];
    return new ArticleItem(itemData);
  }

  Widget _bannerView() {
    var list = banners.map((item) {
      return Image.network(item['imagePath'], fit: BoxFit.cover);
    }).toList();
    return list.isNotEmpty
        ? BannerView(
            list,
            intervalDuration: const Duration(seconds: 3),
          )
        : null;
  }
}

这个Widget的代码比较多,它配置了我们见到的banner、与文章列表。代码中重写了State的生命周期方法`initState`与`build`。我们首先来观察`build`方法:

@override
  Widget build(BuildContext context) {
    //Stack:帧布局  
    return Stack(
      children: <Widget>[
        ///正在加载
        Offstage( //可以控制是否隐藏
          offstage: !_isLoading, //是否隐藏
          child: new Center(child: CircularProgressIndicator()),//圆形进度指示器(小菊花)
        ),

        ///内容
        Offstage(
          offstage: _isLoading,
          child: new RefreshIndicator( //下拉刷新
              child: ListView.builder(
                itemCount: articles.length + 1,    //列表视图的个数
                itemBuilder: (context, i) => _buildItem(i),//类似adapter,item显示什么?返回widget
                controller: _controller,//滑动控制器
              ),
              onRefresh: _pullToRefresh),//刷新回调方法
        )
      ],
    );
  }

这段代码中各个部分都给到了注释,`_buildItem`与`_pullToRefresh`方法分别用于条目视图的生成与新数据的获取。

_pullToRefresh

`_pullToRefresh`是传递给下拉刷新组件:`RefreshIndicator`的刷新回调方法参数,它需要返回一个`Future<void>`,同时我们初次进入页面也需要自动的去获取一次数据,所以我们还会在`initState`方法中主动的调用一次该方法。

 Future<void> _pullToRefresh() async {
    curPage = 0;
    Iterable<Future> futures = [_getArticlelist(), _getBanner()];
    await Future.wait(futures);
    _isLoading = false;
    setState(() {});
    return null;
  }

在这个方法中,我们需要重新请求文章列表与banner图,因此借助`Future.wait`组合两个任务,在两个任务都完成后,再利用`setState`更新UI完成重绘。

 _buildItem

获取到数据之后,接下来我们需要对这些数据进行展示

Widget _buildItem(int i) {
    if (i == 0) {
      return new Container(
        height: 180.0,
        child: _bannerView(),
      );
    }
    var itemData = articles[i - 1];
    return new ArticleItem(itemData);
  }

  Widget _bannerView() {
    ///banners是请求到的banner信息组,其中imagePath代表了图片地址
    ///map意为映射,对banners中的数据进行遍历并返回Iterable<?>迭代器,
    ///?则是在map的参数:一个匿名方法中返回的类型
    var list = banners.map((item) {
      return Image.network(item['imagePath'], fit: BoxFit.cover);
    }).toList();
    ///BannerView的条目不能为空
    return list.isNotEmpty
        ? BannerView(
            list,
            ///切换时间
            intervalDuration: const Duration(seconds: 3),
          )
        : null;
  }

`_buildItem`用于生成ListView当中的条目。注意在配置ListView时,我们给的`itemCount`为:`articles.length + 1`。articles就是请求到的文章信息数量,而**+1**则是为了显示banner。因此当`i=0`,显示第一个条目时候,我们返回了一个`BannerView`。这个`BannerView`其实是一个库(关于如何导入第三方库在最后)。而`ArticleItem`则又是我们自己定义的用于显示文章信息item的组合Widget。

ArticleItem

class ArticleItem extends StatelessWidget {
  final itemData;

  const ArticleItem(this.itemData);

  @override
  Widget build(BuildContext context) {
    ///时间与作者
    Row author = new Row( //水平线性布局
      children: <Widget>[
        //expanded 最后摆我,相当于linearlayout的weight权重
        new Expanded(
            child: Text.rich(TextSpan(children: [
          TextSpan(text: "作者: "),
          TextSpan(
              text: itemData['author'],
              style: new TextStyle(color: Theme.of(context).primaryColor))
        ]))),
        new Text(itemData['niceDate'])//时间
      ],
    );

    ///标题
    Text title = new Text(
      itemData['title'],
      style: new TextStyle(fontSize: 16.0, color: Colors.black),
      textAlign: TextAlign.left,
    );

    ///章节名
    Text chapterName = new Text(itemData['chapterName'],
        style: new TextStyle(color: Theme.of(context).primaryColor));
    
    Column column = new Column( //垂直线性布局
      crossAxisAlignment: CrossAxisAlignment.start, //子控件左对齐
      children: <Widget>[
        new Padding(
          padding: EdgeInsets.all(10.0),
          child: author,
        ),
        new Padding(
          padding: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 5.0),
          child: title,
        ),
        new Padding(
          padding: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 10.0),
          child: chapterName,
        ),
      ],
    );

    return new Card(
      ///阴影效果
      elevation: 4.0,
      child: column,
    );
  }
}

Expanded

可以按比例“扩伸”Row、Column所占用的空间。

const Expanded({
  int flex = 1,
  @required Widget child,
})

flex为弹性系数,和Android中的`LinearLayout`的`weight`比重效果一致。

class _LayoutWidgetState extends State<LayoutWidget> {
  @override
  Widget build(BuildContext context) {
    return Row(
      //将Row 分成 2+3+1分,
      children: <Widget>[
        Expanded(flex:2,child: Container(child: Text('1'), color: Colors.red)),
        Expanded(flex:3,child: Container(child: Text('1'), color: Colors.blue)),
        Expanded(flex:1,child: Container(child: Text('1'), color: Colors.yellow)),
      ],
    );
  }
}

 网络请求

​    一个app中,网络请求是最基本的功能,我们需要使用网络请求数据用于显示或者进行不同的逻辑处理。在我们的案例中,同样需要请求文章数据与banner数据。在Dart SDK中的io库其实提供了`HttpClient` 进行网络请求。大家都知道,Java中也提供了HttpConnection,但是我们更喜欢使用更加方便的OkHttp,所以一般开发中,我们可能使用一些更加方便的网络库,比如**http、dio**等等。进入https://pub.dartlang.org/ 输入库名就能够搜索到相关的库。这次我们使用dio来完成网络的请求:

class HttpManager {
  Dio _dio;
  static HttpManager _instance;

  factory HttpManager.getInstance() {
    if (null == _instance) {
      _instance = new HttpManager._internal();
    }
    return _instance;
  }

  //以 _ 开头的函数、变量无法在库外使用
  HttpManager._internal() {
      ///基础配置
    BaseOptions options = new BaseOptions(
      baseUrl: Api.baseUrl, //基础地址
      connectTimeout: 5000, //连接服务器超时时间,单位是毫秒
      receiveTimeout: 3000, //读取超时
    );
    _dio = new Dio(options);
  }

  request(url, {String method = "get"}) async {
    try {
      ///默认使用get请求
      Options option = new Options(method: method);
      Response response = await _dio.request(url, options: option);
      ///一般来说,提供的是json字符串,response.data得到的就是这个json对应的map
      return response.data;
    } catch (e) {
      return null;
    }
  }
}

导入库

​    在Flutter工程中存在一个`pubspec.yaml`文件。此文件类似`build.gradle`,在这个文件中进行我们整个工程的一些配置,其中就包括了库的导入。配置完成之后,点击右上角的`Packages get`就能自动下载依赖。

库的最新版本可以进入https://pub.dartlang.org/ 搜索。


添加文章详情——路由与导航

路由管理

​    路由(Route)在移动开发中通常指页面(Page),在Android中通常指一个Activity。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。这和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //导航到新路由
              Navigator.push(context, MaterialPageRoute(builder: (context) {
                return SecondRoute();
              }));
            },
            child: Text("进入第二页"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //路由pop弹出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

命名路由

​    命名路由(Named Route)即给路由起一个名字,然后可以通过路由名字直接打开新的路由。这为路由管理带来了一种直观、简单的方式。
 

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      //home: MainRoute(),
      //注册路由表
      routes: {
          /// '/'是特殊地址,第一个页面
        "/" :(context) => MainRoute(),
        "new_page": (context) => SecondRoute(),
      },
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () async {
              //导航到新路由
              var result = await Navigator.pushNamed(context, "new_page");
              debugPrint("返回:$result");
            },
            child: Text("进入第二页"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //路由pop弹出
              Navigator.pop(context, "结束");
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

命名路由的最大优点是直观,我们可以通过语义化的字符串来管理路由。但其有一个明显的缺点:不能直接传递路由参数。假设SecondRoute,需要接受一个字符串参数`tip`,然后再在屏幕中心将`tip`的内容显示出来。因为命名路由需要提前注册到路由表中,所以就无法动态修改`tip`参数。

自定义路由切换动画

​    Material库中提供了MaterialPageRoute,它在Android上会上下滑动切换。如果想自定义路由切换动画,可以使用PageRouteBuilder。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () async {
              //导航到新路由
              var result = await Navigator.push(
                context,
                PageRouteBuilder(
                  ///动画时间
                  transitionDuration: Duration(milliseconds: 500),
                  pageBuilder: (BuildContext context, Animation animation,
                      Animation secondaryAnimation) {
                    ///平移
                    return SlideTransition(
                      ///Tween:在补间动画中,定义开始点结束点
                      position: new Tween<Offset>(
                        begin: const Offset(1.0, 0.0),
                        end: const Offset(0.0, 0.0),
                      ).animate(animation),
                      child: SecondRoute(),
                    );
                  },
                ),
              );
              debugPrint("返回:$result");
            },
            child: Text("进入第二页"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //路由pop弹出
              Navigator.pop(context, "结束");
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

同时我们也可以对动画进行组合

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () async {
              //导航到新路由
              var result = await Navigator.push(
                context,
                PageRouteBuilder(
                  ///动画时间
                    transitionDuration: Duration(milliseconds: 500),
                    pageBuilder: (BuildContext context, Animation animation,
                        Animation secondaryAnimation) {
                      ///透明渐变与旋转
                      return new FadeTransition(
                        opacity: animation,
                        child: new RotationTransition(
                          turns: new Tween<double>(begin: 0.5, end: 1.0)
                              .animate(animation),
                          child: SecondRoute(),
                        ),
                      );
                    },),
              );
              debugPrint("返回:$result");
            },
            child: Text("进入第二页"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //路由pop弹出
              Navigator.pop(context, "结束");
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

注意点

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主页"),
        ),
        body: Column(
          children: <Widget>[
            Text("第一个页面"),
            RaisedButton(
              onPressed: ()  {
                  ///Navigator.push内部其实就是 Navigator.of(context).push
                  Navigator.of(context).push(MaterialPageRoute(builder: (_){
                    return new SecondRoute();
                  }));
              },
              child: Text("进入第二页"),
            )
          ],
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //路由pop弹出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

 

这段代码运行会出现错误:!路由异常

​    问题关键点在于**Navigator operation requested with a context that does not include a Navigator.**(导航操作请求使用了不包含Navigator的上下文context)

​    `Navigator`实际上也是一个Widget,这个异常出现在`Navigator.of(context)`路由器的获取上,而这句代码会**从当前的context的父级一层层向上去查找一个`Navigator`**,我们当前传递的context就是MyApp,它的父级是root——UI根节点。`Navigator`这个widget的并不是由root创建的,因此在root下一级的上下文中无法获得`Navigator`。

> 在之前所有的路由案例中,我们的上下文是MainRoute,它的父级是MaterialApp。MaterialApp内部就会创建一个Navigator。
>
> MaterialApp->\_MaterialAppState->WidgetsApp->\_WidgetsAppState
>
> ![Navigator的创建](图片\Navigator的创建.png)

​    所以问题就在于,`Navigator`需要通过MaterialApp或者它孩子的上下文。

解决一

按照此笔记最开始的正常路由演示案例来进行修改。

解决二

使用`Builder`

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主页"),
        ),
        body: Column(
          children: <Widget>[
            Text("第一个页面"),
            ///
            Builder(builder: (context){
              return RaisedButton(
                onPressed: ()  {
                  Navigator.of(context).push(MaterialPageRoute(builder: (_){
                    return new SecondRoute();
                  }));
                },
                child: Text("进入第二页"),
              );
            })
          ],
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //路由pop弹出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

     使用Builder嵌套,Builder的参数可以看成一个回调,接收自身的context并返回布局配置。现在路由是从Builder的父亲开始查找啦,自然能找到Navigator。

![Builder源码](图片\Builder源码.png)

解决三

使用`navigatorKey`

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        ///指定路由器widget的key
      navigatorKey: navigatorKey,
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主页"),
        ),
        body: Column(
          children: <Widget>[
            Text("第一个页面"),
            RaisedButton(
              onPressed: ()  {
                ///输出Navigator
                debugPrint(navigatorKey.currentWidget.runtimeType.toString());
                navigatorKey.currentState.push(MaterialPageRoute(builder: (_){
                  return new SecondRoute();
                }));
              },
              child: Text("进入第二页"),
            )
          ],
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二页"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一个页面"),
          RaisedButton(
            onPressed: () {
              //路由pop弹出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

​    

![Navigator的创建](图片\Navigator的创建.png)

​    在创建`Navigator`的时候,会给一个**key**,这个key可以看成一个Widget的id。这里的**_navigator**就是我们指定的**navigatorKey**(如果我们没指定,会给默认值的,所以不要疑惑不指定是不是就不创建`Navigator`了)。而通过这个key,就能够获得这个`Navigator`。直接获得了路由自然不需要再去查找了!

完善第一个页面

​    之前我们编写了第一个页面显示了banner与文章列表,一般的,在点击banner与文章条目时需要进入新界面显示文章详情,而wanandroid的文章详情是一个web页面,因此我们需要增加一个新界面用于展示web

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

class WebViewPage extends StatefulWidget {
  final data;

  WebViewPage(this.data);

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

class _WebViewPageState extends State<WebViewPage> {
  bool isLoad = true;
  FlutterWebviewPlugin flutterWebViewPlugin;

  @override
  void initState() {
    super.initState();
    flutterWebViewPlugin = new FlutterWebviewPlugin();
    flutterWebViewPlugin.onStateChanged.listen((state) {
      if (state.type == WebViewState.finishLoad) {
        // 加载完成
        setState(() {
          isLoad = false;
        });
      } else if (state.type == WebViewState.startLoad) {
        setState(() {
          isLoad = true;
        });
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    ///WebView插件
    return WebviewScaffold(
        appBar: AppBar(
          title: Text(widget.data['title']),
          ///appbar下边摆放一个进度条
          bottom: PreferredSize(
              preferredSize: const Size.fromHeight(1.0),
              child: const LinearProgressIndicator()),
          ///透明度
          bottomOpacity: isLoad ? 1.0 : 0.0,
        ),
        withLocalStorage: true, //缓存,数据存储
        url: widget.data['url'],
        withJavascript: true);
  }
}

> flutter_webview_plugin是一个插件,需要引入:flutter_webview_plugin: ^0.3.0+2

首先我们加入banner的点击事件,在`page_article.dart`中修改`_bannerView`方法:

Widget _bannerView() {
    var list = banners.map((item) {
      ///自动增加点击水波纹的widget
      return InkWell(
        child: Image.network(item['imagePath'], fit: BoxFit.cover), //fit 图片充满容器
        ///点击事件
        onTap: () {
          ///跳转页面
          Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
            return WebViewPage(item);
          }));
        },
      );
    }).toList();
    return list.isNotEmpty
        ? BannerView(
            list,
            intervalDuration: const Duration(seconds: 3),
          )
        : null;
  }

然后还有文章条目的点击,在`article_item.dart`文件中修改`build`方法最后return的内容

 return Card(
      ///阴影效果
      elevation: 4.0,
      child: InkWell(
        child: column,
        onTap: () {
          Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
              ///因为webview获取的是map的url地址,而文章数据中文章详情的key是link
            itemData['url'] = itemData['link'];
            return WebViewPage(itemData);
          }));
        },
      ),
    );


 动画与打包

 

 动画

​        Flutter中的动画系统基于`Animation`对象的,和之前的手势不同,它不是一个Widget,这是因为`Animation`对象本身和UI渲染没有任何关系。Animation是一个抽象类,就相当于一个定时器,它用于保存动画的插值和状态,并执行数值的变化。widget可以在`build`函数中读取`Animation`对象的当前值, 并且可以监听动画的状态改变。

AnimationController

​        AnimationController用于控制动画,它包含动画的启动`forward()`、停止`stop()` 、反向播放 `reverse()`等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。

AnimationController controller = AnimationController(
 duration: const Duration(milliseconds: 2000), //动画时间
 lowerBound: 10.0,    //生成数字的区间
 upperBound: 20.0,    //10.0 - 20.0
 vsync: this  //TickerProvider 动画驱动器提供者
);

Ticker

​    Ticker的作用是添加屏幕刷新回调,每次屏幕刷新都会调用`TickerCallback`。使用Ticker来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源。因为Flutter中屏幕刷新时会通知Ticker,锁屏后屏幕会停止刷新,所以Ticker就不会再触发。最简单的做法为将`SingleTickerProviderStateMixin`添加到State的定义中。

import 'package:flutter/material.dart';

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

class AnimationApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "animation",
      home: Scaffold(
        appBar: AppBar(
          title: Text('animation'),
        ),
        body: AnimWidget(),
      ),
    );
  }
}

// 动画是有状态的
class AnimWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AnimWidgetState();
  }
}

class _AnimWidgetState extends State<AnimWidget>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  bool forward = true;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      // 动画的时长
      duration: Duration(milliseconds: 2000),
      lowerBound: 10.0,
      upperBound: 100.0,
      // 提供 vsync 最简单的方式,就是直接混入 SingleTickerProviderStateMixin
      // 如果有多个AnimationController,则使用TickerProviderStateMixin。
      vsync: this,
    );
       //状态修改监听
    controller
      ..addStatusListener((AnimationStatus status) {
        debugPrint("状态:$status");
      })
      ..addListener(() {
        setState(() => {});
      });

    debugPrint("controller.value:${controller.value}");
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: controller.value,
          height: controller.value,
          color: Colors.blue,
        ),
        RaisedButton(
          child: Text("播放"),
          onPressed: () {
            if (forward) {
              controller.forward();
            } else {
              controller.reverse();
            }
            forward = !forward;
          },
        ),
        RaisedButton(
          child: Text("停止"),
          onPressed: () {
            controller.stop();
          },
        )
      ],
    );
  }
}

> 动画状态监听:在forword结束之后状态为completed。在reverse结束之后状态为dismissed

Tween

​    默认情况下,`AnimationController`对象值为:double类型,范围是0.0到1.0 。如果我们需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。要使用Tween对象,需要调用其`animate()`方法,然后传入一个控制器对象,同时动画过程中产生的数值由`Tween`的`lerp`方法决定。

import 'package:flutter/material.dart';

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

class AnimationApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "animation",
      home: Scaffold(
        appBar: AppBar(
          title: Text('animation'),
        ),
        body: AnimWidget(),
      ),
    );
  }
}

// 动画是有状态的
class AnimWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AnimWidgetState();
  }
}

class _AnimWidgetState extends State<AnimWidget>
    with SingleTickerProviderStateMixin {
  AnimationController controller;

  bool forward = true;

  Tween<Color> tween;

  @override
  void initState() {
    super.initState();

    controller = AnimationController(
      // 动画的时长
      duration: Duration(milliseconds: 2000),
      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
      vsync: this,
    );
    //使用Color
    tween = ColorTween(begin: Colors.blue, end: Colors.yellow);
    //添加动画值修改监听
    tween.animate(controller)..addListener(() => setState(() {}));
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: 100,
          height: 100,
          //获取动画当前值
          color: tween.evaluate(controller),
        ),
        RaisedButton(
          child: Text("播放"),
          onPressed: () {
            if (forward) {
              controller.forward();
            } else {
              controller.reverse();
            }
            forward = !forward;
          },
        ),
        RaisedButton(
          child: Text("停止"),
          onPressed: () {
            controller.stop();
          },
        )
      ],
    );
  }
}

Curve

​    动画过程默认是线性的(匀速),如果需要非线形的,比如:加速的或者先加速后减速等。Flutter中可以通过Curve(曲线)来描述动画过程。

import 'package:flutter/material.dart';

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

class AnimationApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "animation",
      home: Scaffold(
        appBar: AppBar(
          title: Text('animation'),
        ),
        body: AnimWidget(),
      ),
    );
  }
}

// 动画是有状态的
class AnimWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AnimWidgetState();
  }
}

class _AnimWidgetState extends State<AnimWidget>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;
  bool forward = true;

  @override
  void initState() {
    super.initState();

    controller = AnimationController(
      // 动画的时长
      duration: Duration(milliseconds: 2000),
      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
      vsync: this,
    );

    //弹性
    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    //使用Color
    animation = Tween(begin: 10.0, end: 100.0).animate(animation)
      ..addListener(() {
        setState(() => {});
      });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          //不需要转换
          width: animation.value,
          height: animation.value,
          //获取动画当前值
          color: Colors.blue,
        ),
        RaisedButton(
          child: Text("播放"),
          onPressed: () {
            if (forward) {
              controller.forward();
            } else {
              controller.reverse();
            }
            forward = !forward;
          },
        ),
        RaisedButton(
          child: Text("停止"),
          onPressed: () {
            controller.stop();
          },
        )
      ],
    );
  }
}

AnimatedWidget

​        通过上面的学习我们能够感受到`Animation`对象本身和UI渲染没有任何关系。而通过`addListener()`和`setState()` 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用`setState()`的细节,简单来说就是自动调用`setState()`。

​    Flutter中已经封装了很多动画,比如对widget进行缩放,可以直接使用`ScaleTransition

import 'package:flutter/material.dart';

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

class AnimationApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "animation",
      home: Scaffold(
        appBar: AppBar(
          title: Text('animation'),
        ),
        body: AnimWidget(),
      ),
    );
  }
}

// 动画是有状态的
class AnimWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AnimWidgetState();
  }
}

class _AnimWidgetState extends State<AnimWidget>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;
  bool forward = true;

  @override
  void initState() {
    super.initState();

    controller = AnimationController(
      // 动画的时长
      duration: Duration(milliseconds: 2000),
      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
      vsync: this,
    );

    //弹性
    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    //使用Color
    animation = Tween(begin: 10.0, end: 100.0).animate(animation);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ScaleTransition(
          child:  Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
          scale: controller,
        ),
        RaisedButton(
          child: Text("播放"),
          onPressed: () {
            if (forward) {
              controller.forward();
            } else {
              controller.reverse();
            }
            forward = !forward;
          },
        ),
        RaisedButton(
          child: Text("停止"),
          onPressed: () {
            controller.stop();
          },
        )
      ],
    );
  }
}

Hero动画

​    Hero动画就是在路由切换时,有一个共享的Widget可以在新旧路由间切换,由于共享的Widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就会产生一个Hero动画。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
          appBar: AppBar(
            title: Text("主页"),
          ),
          body: Route1()),
    );
  }
}

// 路由A
class Route1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.topCenter,
      child: InkWell(
        child: Hero(
          tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
          child: CircleAvatar(
            backgroundImage: AssetImage(
              "assets/banner.jpeg",
            ),
          ),
        ),
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return Route2();
          }));
        },
      ),
    );
  }
}

class Route2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Hero(
          tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
          child: Image.asset("assets/banner.jpeg")),
    );
  }
}

 组合动画

有些时候我们可能会需要执行一个动画序列执行一些复杂的动画。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Route(),
    );
  }
}

class Route extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return RouteState();
  }
}

class RouteState extends State<Route> with SingleTickerProviderStateMixin {
  Animation<Color> color;
  Animation<double> width;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      // 动画的时长
      duration: Duration(milliseconds: 2000),
      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
      vsync: this,
    );

    //高度动画
    width = Tween<double>(
      begin: 100.0,
      end: 300.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          //间隔,前60%的动画时间 1200ms执行高度变化
          0.0, 0.6,
        ),
      ),
    );

    color = ColorTween(
      begin: Colors.green,
      end: Colors.red,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.6, 1.0, //高度变化完成后 800ms 执行颜色编码
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主页"),
      ),
      body: InkWell(
        ///1、不用显式的去添加帧监听器,再调用setState()
        ///2、缩小动画构建的范围,如果没有builder,setState()将会在父widget上下文调用,导致父widget的build方法重新调用,现在只会导致动画widget的build重新调用
        child: AnimatedBuilder(
            animation: controller,
            builder: (context, child) {
              return Container(
                color: color.value,
                width: width.value,
                height: 100.0,
              );
            }),
        onTap: () {
          controller.forward().whenCompleteOrCancel(() => controller.reverse());
        },
      ),
    );
  }
}

 打包

​    Flutter在打Release包时候回使用AOT,因此在对一个Flutter测试时候务必使用Release来进行测试。打包命令:`flutter build apk` 。当然我们需要打包时,还需要配置一些比如签名的内容。配置这些内容和普通Android工程没有区别,都是在`build.gradle`中进行,只是Flutter工程AS没有提供GUI。

​    在Flutter工程的`android/app`下面的`build.gradle`可以修改包名、版本等信息,这就不用多说了。获得签名文件之后,将它复制到flutter的android目录:

![签名文件](图片\签名文件.png)

​    然后在app的`build.gradle`中配置:

groovy

signingConfigs {
        release {
            keyAlias 'enjoy'
            keyPassword '123456'
            // 因为是放到父级的根目录,使用rootProject
            // 如果放在这个build.gradle的同级,直接使用file
            storeFile rootProject.file('enjoy.jks')
            storePassword '123456'
        }
    }
    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.release
        }
    }

饼图

<https://github.com/google/charts>

> Stack布局中的fit属性与Image的fit类似,表示内容的扩充情况。默认为`StackFit.loose`表示`Stack`与内容一样大。如果设置为`StackFit.passthrough`则表示`Stack`父Widget的约束会传给Stack内部非Positioned的子Widget。效果如代码中的`StackFit.dart`

摘自:享学课堂Lance老师笔记

Flutter开发的WanAndroid

Flutter开发的WanAndroid-玩Android - wanandroid.com

推荐7个Flutter重磅开源项目!
推荐7个Flutter重磅开源项目! - 哔哩哔哩    


    

​   


版权声明:本文为liqingmiao123原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。