资讯详情

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> {   @ovrride
  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

标签: lance连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司