二、Flutter-Flutter初体验

第三部分 Flutter初体验

1-1 创建项目及项目目录

$ flutter create learn_flutter #终端创建项目
  
目录结构
  dart_tool  #记录某些信息
  .idea #对当前的项目进行配置, Android Studio是基于IDEA开发的
  android #安卓工程
  ios #iOS工程
  lib #⚠️放flutter源代码
        main.dart #启动文件
  test #做测试用
  .gitignore #提交git忽略
  pubspec.yaml #⚠️第三方配置

1-2 Hot Reload 与 Hot Restart

/*
Hot Reload: 热重载 (command + \)
Hot Restart: 热重启 (shift + command + \)

运行一个Flutter项目, 有三种启动方式:
    1.冷启动: 所有的代码从零开始启动.(过程非常慢)
    2.热重载: 最主要执行build方法
    3.热重启: 重新运行整个App
*/

1-3 从零编写项目入口文件

import 'package: flutter/material.dart'; //runApp函数在这个库中
main(){
  // 1.调用runApp函数, 需要传入一个Widget
  // Widget是一个抽象类, 抽象类不能初始化; 抽象类可以被子类继承, 所以用它的子类进行初始化.
  // 多态: 父类引用指向子类对象.
  runApp(
    Center(
        child: Text(
        "Hello World", 
        textDirection: TextDirection.ltr,
        style: TextStyle(
          fontSize: 30,
          color: Colors.orange
        ),
      ),
    )
  ); //报错: 要指明文字排版方向是 从左往右 还是 从右往左; 如果使用了Material设计风格就不用设置排版了, 因为它做了默认设置从左往右.
}
// Flutter缩进两个空格

1-4 创建一个Material风格App

import 'package: flutter/material.dart';
main(){
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false, //去掉"DUBUG"标识
      //一个Scaffold可以看成是一个iOS的UIViewController或安卓的activity.
        home: Scaffold(//Scaffold脚手架, 帮助我们快速搭建页面
        sppBar: AppBar(
            title: Text("第一个Flutter程序"),
        ),
        body: Center(
          child: Text(
            "Hello World", 
            textDirection: TextDirection.ltr,
            style: TextStyle(
              fontSize: 30,
              color: Colors.orange
            ),
          ),
        )
      )
    )
  );
}

1-5 重构代码: 抽取Widget组件

import 'package: flutter/material.dart';
//main(){
//  runApp(MyApp());
//}
//简写👆:
main() => runApp(MyApp());

/*Widget:
    StatefulWidget(有状态的Widget): 在运行过程中有一些状态(data)需要改变
    StatelessWidget(无状态的Widget): 内容是确定没有状态(data)的改变
*/
//Flutter 没有命名空间, 只有库的概念. 就是一个文件里名字不能重复
class MyApp extends StatelessWidget { 
  //子类必须实现父类的抽象方法
  @override
  //build 方法什么情况下被执行呢?
  //    1.当我们的StatelessWidget第一次被插入到Widget树中时(也就是第一次被创建时);
  //    2.当我们的父Widget发生改变时, 子Widget会被重新构建;
  //    3.如果我们的Widget依赖InheritedWidget的一些数据, InheritedWidget数据发生改变时;
  Widget build(BuildContext context){
    return MaterialApp(
      debugShowCheckedModeBanner: false, //去掉"DUBUG"标识
        home: HYHomePage()
    );
  }
}

class HYHomePage extends StatelessWidget {//自定义组件一般是: 前缀 + 组件名 , 为了区分自定义还是系统的
  @override
  Widget build(BuildContext context){
    return Scaffold(//Scaffold脚手架, 帮助我们快速搭建页面
      sppBar: AppBar(
        title: Text("第一个Flutter程序"),
      ),
      body: HYContentBody()
    );
  }
}

/// 最终案例:

//这个代码是有问题的❌
class HYContentBody extends StatelessWidget {
  //这里不能放变量, 因为StatelessWidget继承自Widget, Widget有@immutable注解, 被@immutable注解标明的类或者子类都必须是不可变的. 所以StatelessWidget和StatefulWidget都是不可变的
  var flag = true;//错误的代码❌
  
    @override
  Widget build(BuildContext context){
    return  Center(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Checkbox(
            value: flag,
            onChanged: (value) => flag = value//错误代码❌
          ),
          Text("同意协议", style: TextStyle(fontSize: 20),)
        ]
      )
    );
  }
}
// @required: 必须的, 注解告诉某些可选命名参数是必传的. 次注解只出现在Flutter中, dart中没有.
// @immutable: 不可变的, 被注解的类里面所有的成员变量都必须是final的, 不能保存状态.

1-6 修改: 上面状态改变的错误bug

// Android/iOS 是命令式编程, 没有状态的概念, 只说属性、数据;
// vue/react/angular 是声明式编程, 管好状态就行了.

//在Flutter开发中, 所有的Widget都不能定义状态, 因为所有的Widget都有@immutable修饰.
//StatefulWidget不能定义状态 -> 创建一个单独的类(State对象), 这个类负责维护状态
class HYContentBody extends StatefulWidget {
    @override
  State<StatefulWidget> createState(){//这是个抽象方法, 抽象方法必须实现
    return HYContentBodyState();
  }
}

class HYContentBodyState extends State<HYContentBody> {
  var flag = true;
    @override
  // 让State来构建(build)这个Widget, 这样就可以在类里创建状态
  Widget build(BuildContext context){//这是个抽象方法, 需要自己实现方法体
    //它有一个 this.widget;
    return  Center(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Checkbox(
            value: flag,
            onChanged: (value) {
              setState((){// 类似React的虚拟DOM操作
                flag = value;
              });
            },
          ),
          Text("同意协议", style: TextStyle(fontSize: 20),)
        ]
      )
    );
  }
}
// 调用setState() 会使所有的Widget重建. Widget耗性能比较低, 更像是一个配置文件.

第四部分 StatelessWidget

1-1 商品列表实现

// StatefulWidget分成两个类: 1.继承自StatefulWidget的类(用于创建state 和 可以接受父Widget传过来的数据); 2.State类(用于保存状态)

import 'package: flutter/material.dart';
main() => runApp(MyApp());
// AS代码块: stl 会自动帮我们生成statelesswidget代码
class MyApp extends StatelessWidget { 
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      debugShowCheckedModeBanner: false, //去掉"DUBUG"标识
        home: HYHomePage()
    );
  }
}

class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return Scaffold(
      sppBar: AppBar(
        title: Text("商品列表"),
      ),
      body: HYContentBody()
    );
  }
}

class HYContentBody extends StatelessWidget {
    @override
  Widget build(BuildContext context){
    //return Column(//不可滚动的
    return ListView(//可滚动的
        children: <Widget>[
          HYHomeProductItem("Apple1","Macbook1","图片地址1"),
          SizedBox(height: 8),
          HYHomeProductItem("Apple2","Macbook2","图片地址2"),
          SizedBox(height: 8),
          HYHomeProductItem("Apple3","Macbook3","图片地址3"),
        ]
    );
  }
}

class HYHomeProductItem extends StatelessWidget {
  //注意: 在Widget里定义的所有变量都必须是final的
  final String title; 
  final String desc;
  final String imageURL;
  
  final style1 = TextStyle(fontSize: 25, color: Colors.orange);
  final style2 = TextStyle(fontSize: 25, color: Colors.green);
  
  HYHomeProductItem(this.title, this.desc, this.imageURL);//自定义构造函数
  
    @override
  Widget build(BuildContext context){
    //final style1 = TextStyle(fontSize: 25, color: Colors.orange);//每次调用Build都会重新创建一遍这个代码
    return Container(
      padding: EdgeInsets.all(8),//设置内边距
      decoration: BoxDecoration(
        border: Border.all(
            width: 5, //设置边框的宽度
          color: Colors.black12,//设置边框的颜色: 透明度是12的黑色
        )
      ),//decoration装饰
      child: Column(
        //crossAxisAlignment: CrossAxisAlignment.start //设置交叉轴对齐方式
        children: <Widget>[
          Text(title, style: style1),
          SizedBox(height: 8),//设置尺寸间距
          Text(desc, style: style2),
          SizedBox(height: 8),//8 相当于8个像素, 相当于iOS的点像素
          Image.network(imageURL)// 图片加载有专门的IO线程
        ],
      )
    );
  }
}

/*注意⚠️: 显示黄条(relayoutBoundary), 表示你的内容超出了屏幕显示的区域(像素溢出), 也就是超出了布局范围.*/

/*快捷键: 在AS中快捷键 Alt + Enter, 用于给某一个组件包裹另外一个Widget*/
/*快捷键: 在AS中快捷键 command + alt + B, 可以查看它某个类的所有的实现类*/

/* 上面例子中, 文字内容为什么是居中显示?  因为column组件交叉轴默认是居中对齐显示的, 可以设置为crossAxisAlignment: CrossAxisAlignment.start */

1-2 statefulWidget 综合练习

import 'package: flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget { 
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      debugShowCheckedModeBanner: false,
        home: HYHomePage()
    );
  }
}

class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return Scaffold(
      sppBar: AppBar(
        title: Text("商品列表"),
      ),
      body: HYContentBody("传递信息")//sp1.
    );
  }
}

// Widget是不加_: 暴露给别人使用
class HYContentBody extends StatefulWidget {
  final String message;
  HYHomeContent(this.message);//sp2.
    @override
  State<StatefulWidget> createState(){
        return _HYHomeContentState();
  }
}

/*为什么Flutter在设计的时候把StatefulWidget的build方法放在State中, 而不是StatefulWidget 里面? 
    1.build出来的Widget是需要依赖State中的变量(也叫状态/数据)
    2.在Flutter的运行过程中:
        Widget是不断的销毁和创建的, 当我们自己的状态发生改变时, 并不希望重新创建一个新的State.
*/
//Widget(描述信息)->Element->Render Object

// State是加_: 状态这个类只是给自己Wideget使用
class _HYHomeContentState extends State<HYHomeContent> {
  int _counter = 0;
  @override
  Widget build(BuildContext context){
    return Center(
        child: Coluum(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          _getButtons(),
          Text("当前计数: $_counter", style: TextStyle(fontSize: 25),),
          Text("传递的信息:${this.widget.message}")//sp3. this.widget 指向 HYHomeContent对象
        ],
      ),
    );
  }
  
  widget _getButtons(){
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        RaisedButton(
          child: Text("+"),
          color: Color.pink,
          onPressed: (){
           setState((){
             _counter++;
           }); 
          },
        ),
        RaisedButton(
          child: Text("-"),
          color: Color.purple,
          onPressed: (){
           setState((){
             _counter--;
           }); 
          },
        ),
      ],
    );
  }
}

1-3 生命周期

//StatelessWidget生命周期非常简单: 1.构造函数被调用 2.调用build方法;
class HYContentBody extends StatelessWidget {
  final String message;
  HYHomeContent(this.message){
    print("构造函数被调用");
  }
    @override
  Widget build(BuildContext context){
    print("调用build方法");
        return Text(message);
  }
}

/*开发中用的最多的就是StatefulWidget的生命周期: */
class HYHomeContent extends StatefulWidget {
  HYHomeContent(){
    print("1.调用HYHomeContent的constructor方法");
  }
    @override
  _HYHomeContentState<StatefulWidget> createState(){
        print("2.调用HYHomeContent的createState方法");
    return _HYHomeContentState();
  }
}
class _HYHomeContentState extends State<HYHomeContent> {
  
  _HYHomeContentState(){
    print("3.调用_HYHomeContentState的constructor方法");
  }
  
  @override
  void initState(){//✅
    //注意: 这里是必须调用super的, 原因1、父类帮助我们初始化;原因2、不调会报警告, 因为内部实现有个注解@mustCallSuper, 这个注解的意思是必须调用父类方法
    super.initState();
    print("4.调用_HYHomeContentState的initState方法");
  }
  
  @override
  void didUpdateWidget(HYHomeContent oldWidget){ //这个方法用的不是很多: 当父Widget重新创建(build)时会调用这个方法
    super.didUpdateWidget(oldWidget);
    print("调用_HYHomeContentState的didUpdateWidget方法");
  } 
  
  @override
  void didChangeDependencies(){ //⚠️改变了我们的依赖: 如果当前的state依赖一些数据时, 数据发生改变就会调用这个方法.
    super.didChangeDependencies();
    print("调用_HYHomeContentState的didChangeDependencies方法");
  } 
  
  @override
  Widget build(BuildContext context){
    print("5.调用_HYHomeContentState的build方法");
    return Column(
        children: <Widget>[
        RaisedButton(
            child: Icon(Icons.add),
          onPressed:(){
            setState((){//setState本质是给组件做一个标记, 然后调用build, 重新显示
              _counter++;
            });
          },
        ),
        Text("数字:$_counter")
      ]
    );
  }
  
  @override
  void dispose(){ //✅一般不会去手动销毁
    print("6.调用_HYHomeContentState的dispose方法");
    super.dispose();
  }
}
/* AS快捷键: 
    StatelessWidget转StatefulWidget快捷键: Alt + 回车 然后选择"Convert to StatefulWidget"
    
    将我们build出来的Widget抽取到一个单独的Widget中快捷键: Alt + enter +w 然后输入组件名即可
    
    返回到上次编辑位置(向前向后跳转) opt + cmd + ← / → 
    全局搜索类(cmd + O)
*/

第五部分: 基础Widget(叶子组件)

1-1 Flutter的编程范式

/*编程范式: 
    面向对象编程, 与之对应或者结合开发包括: 面向过程编程、函数式编程、面向协议编程; 另外还有两个编程范式: 命令式编程 和 声明式编程.*/

1-2 文本Widget

// 不可以直接给Text设置宽度
return Text(
    "Hello world".toLowerCase(), //转小写
  textAlign: TextAlign.center,//文本排版
  maxLine: 2,//最大行数
  overflow: TextOverflow.ellipsis,//超出部分显示...
  textScaleFactor: 1.5,//缩放因子, 默认是1.(放大/缩小 字体)
  syle: TextStyle(//文本样式
    fontSize: 30,
    color: Colors.red,
    fontWeight: FontWeight.bold
  ),
);

//开发中: 富文本
return Text.rich(
    TextSpan(
//      text: "Hello World",
//    style: TextStyle(color: Colors.red)
    children: [
      TextSpan(text:"Hello World",style: TextStyle(color: Colors.red)),//插入文本
      WidgetSpan(child: Icons(Icons.favorite, color: Colors.red)),//插入图标/图片
      TextSpan(text:"Hello Flutter",style: TextStyle(color: Colors.green)),
      TextSpan(text:"Hello Dart",style: TextStyle(color: Colors.blue)),
    ]
  );
);
//AS快捷键: option + command + B 查看此类的所有实现类, 比如查看抽象类的子类, 来构建对象.

1-3 Button-按钮 Widget

//1.RaisedButton 凸起按钮
return RaisedButton(
    child: Text("RaisedButton"),
  textColor: Colors.red,
  onPressed: ()=> print("RaisedButton Click"),
);
//必传参数, 不传就报错(编译不通过);注解@required 编译可以通过, 但是会报警告.
  
//2.FlatButton 平铺按钮
return FlatButton(
    child: Text("FlatButton"),
  textColor: Colors.white,
  color: Colors.orange,
  onPressed: ()=> print("FlatButton Click"),
);
  
//3.OutlineButton 边框按钮
return FlatButton(
    child: Text("OutlineButton"),
  textColor: Colors.white,
  color: Colors.orange,
  onPressed: ()=> print("OutlineButton Click"),
);

//4.FloatingActionButton 浮动按钮
class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return Scaffold(
      sppBar: AppBar(
        title: Text("基础组件"),
      ),
      body: HYContentBody(),
      floatingActionButton: FloatingActionButton(//浮动按钮
        child: Icon(Icons.add),
        onPressed: ()=> print("FloatingActionButton Click"),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, //设置浮动按钮位置
    );
  }
}

//5.自定义button: 图标-文字-背景-圆角
return FlatButton(
  color.Colors.amberAccent,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(8) //设置圆角
  ),
    child: Row( //默认情况下, 会占一整行
    mainAxisSize: MainAxisSize.min, //默认值MainAxisSize.max: 剩余空间能占多大占多大;
    children: <Widget>[
      Icon(Icons.favorite, color: Colors.red),
      Text("喜欢作者"),
    ]
  ),
  onPressed: () {},
);

//6.补充
return ButtonTheme(//消除按钮最小宽高
  minWidth: 30,//设置最小宽度
  height: 10,//设置最小高度
  child: FlatButton(
    /* 有两个小问题: 
    1.默认情况下如果Button的宽/高小于48px, Button上下会存在一定间距使其范围达到48px 
    2.Button变小 (默认情况下Button存在最小尺寸:宽88、高36)---ButtonTheme
    3.Button的默认内间距
    */
    padding: EdgeInsets.all(0),//消除Button默认内间距
    child: Text("FlatButton"),
    textColor: Colors.white,
    color: Colors.orange,
    materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,//(紧缩包裹)消除上下默认间距
    onPressed: ()=> print("FlatButton Click"),
  )
);

1-4 图片-image

//抽象类也可以实例化, 就是它有工厂方法的时候, 用工厂方法实例化.
/// 1.网络图片
return Image(
  color: Colors.red,//设置图片混入颜色
  colorBlendMode: BlendMode.colorDodge,//设置颜色混入模式
    image: NetworkImage("图片网络地址"),
  width: 200,
  height: 200,
  fit: BoxFit.contain, //设置图片的填充属性. 其中 BoxFit.fitWidth: 宽度固定,高度自适应;同理BoxFit.fitHeight.
  repeat: ImageRepeat.repeatY,//设置图片在某一个方向上不断的重复
//  alignment: Alignment.bottomCenter,//设置图片在矩形框的位置
  alignment: Alignment(x, y), //范围 -1 到 1
);
//简单写法: return Image.network("图片网络地址");

/// 2.加载本地图片
  /*本地图片使用步骤:
  1.在Flutter项目中创建一个文件夹, 存储图片.比如在项目目录中创建asssets文件夹:
  assets
    fonts
    images
        2.0x
        3.0x
        xxx.jpeg
  2.在pubspec.yaml进行配置:
  assets:
    - assets/images/ #写个统配符, 这个文件夹下的所有图片都会配置好,可直接使用
    #- assets/images/xxx.jpeg #配置用到的图片
    #- assets/images/2.0x/xxx.jpeg #配置2倍图
    #- assets/images/3.0x/xxx.jpeg #配置3倍图
  3.使用图片*/*/
return Image(
    image: AssetImage("assets/images/xxx.jpeg"),//本地图片路径
);
//简写: return Image.asset("assets/images/xxx.jpeg");

///3.占位图
return FadeInImage(//设置占位图: 淡入淡出
  fadeOutDuration: Duration(milliseconds: 1),
  fadeInDuration: Duration(milliseconds: 1),
  placeholder: AssetImage("assets/images/xxx.jpeg"),
  image: NetworkImage("图片网络地址"),
);
//图片缓存: Flutter默认情况下会对图片做内存缓存的, 最多缓存1000张, 同时最大上限100M空间.超过缓存自动清理

///4.Icon
return Icon(Icons.pats, size: 100, color: Colors.orange); //Icon是字体图标
/* Icon字体图标 和 图片图标 区别:
    1.字体图标是矢量图, 放大的时候不会失真;
    2.字体图标可以设置颜色;
    3.图标很多时, 相对占用空间更小.
*/
return Icon(IconData(0xe478, fontFamily: 'MaterialIcons'), size: 100, color: Colors.orange);
//使用Text来显示字体图标: sp1. 把0xe478数字 转 Unicode编码 sp2.设置对应的字体fontFamily
return const Text('\ue478', style: TextStyle(fontSize: 100, color: Colors.red, fontFamily: 'MaterialIcons'),);

1-5 TextFile

class _MyHomePageState extends State<MyHomePage> {
  final usernameTextEditController = TextEditingController();//设置全局变量记录输入框内容
  final passwordTextEditController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: usernameTextEditController,
              decoration: const InputDecoration(
                labelText: "username",//提示文本
                icon: Icon(Icons.people),//设置图片
                hintText: "请输入用户名",//默认提示
                border: OutlineInputBorder(),//设置外边框
                filled: true,//开启填充色
                fillColor: Colors.orange,//填充颜色
              ),
              onChanged: (value) {//监听值改变
                print('onChanged:$value');
              },
              onSubmitted: (value) {//监听用户点击确定
                print('onSubmitted:$value');
              },
            ),
            const SizedBox(height: 10,),
            TextField(
              controller: passwordTextEditController,
              decoration: const InputDecoration(
                labelText: "password",//提示文本
                icon: Icon(Icons.lock),//设置图片
                hintText: "请输入密码",//默认提示
                border: InputBorder.none,//设置外边框(无边框)
                filled: true,//开启填充色
                fillColor: Colors.pink,//填充颜色
              ),
              onChanged: (value) {//监听值改变
                print('onChanged:$value');
              },
              onSubmitted: (value) {//监听用户点击确定
                print('onSubmitted:$value');
              },
            ),
            TextButton(
              onPressed: (){
                print("用户名:${usernameTextEditController.text}-----密码:${passwordTextEditController.text}");
                usernameTextEditController.text = "";//清空输入框内容
                passwordTextEditController.text = "";//清空输入框内容
              },
              child: const Text("登录", style: TextStyle(fontSize: 20, color: Colors.red, backgroundColor: Colors.orange),),
            ),
          ],
      ),
    );
  }
}

1-6 颜色color

//方式1:
// Color(0xff ff ff ff),  对应: 透明度 R G B
//方式2:
color: Color.fromRGBO(r, g, b, opacity),

第六部分 布局Widget

1-1 Align

return Container(//Container可以设置宽高, 套一个Container为了限制其范围
  width: 100,
  height: 100,
  color: Colors.red,
  child: const Align(
    alignment: Alignment(1, 1),//改变子组件位置
    widthFactor: 10,//设置宽度范围为子组件的多少倍
    heightFactor: 10,//设置高度范围为子组件的多少倍
    child: Icon(Icons.pets, size: 60,),
  ),
);

//Center继承自Align

1-2 Padding

return const Padding(
  // padding: EdgeInsets.all(80.0),//设置间距方法1
  padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),//设置间距方法2 ...
  child: Text(
    'You have pushed the button this many times:',
    style: TextStyle(backgroundColor: Colors.red),
  ),
); //有时可以用Sizebox代替

1-3 Container

return Container(
  width: 200,// width: double.infinity,//设置宽度无限大
  height: 200,
  // color: Colors.red,//设置背景色, 会与设置BoxDecoration冲突
  alignment: Alignment(-1, -1),//设置子组件排版位置
  padding: EdgeInsets.all(20.0)//设置内边距
  margin: EdgeInsets.all(10.0)//设置内边距
  child: child: Icon(Icons.pets, size: 60,),
  transform: Matrix4.rotationZ(100),//容器设置形变, 比如:物理旋转
  decoration: BoxDecoration{
    color: Colors.red,//设置背景色
    border: Border.all(//设置边框
        width: 5,
      color: Colors.purple
    ),
    borderRadius: BorderRadius.circular(100),//设置圆角
    boxShadow: [
      //Offset:偏移方向; spreadRadius: 延伸范围; blurRadius: 模糊范围;
      BoxShadow(color: Colors.orange, offset: Offset(10, 10), spreadRadius: 5, blurRadius: 10),
      BoxShadow(color: Colors.blue, offset: Offset(-10, 10), spreadRadius: 5, blurRadius: 10),
    ],
  },
);

// 如果没有设置alignment: , 就不会创建Alignment对象, Text作为子组件会被拉满, 从头开始; 设置alignment后, Text就不会拉满,而是变成它内容大小.

第七部分 Flex

1-1 flex组件

// 比较常见的多子布局组件是Row、Column、Stack
return Flex(
  direction: Axis.vertical, //Column
//  direction: Axis.horizontal, //Row
);
//Column和Row都继承自Flex
// 主轴 Main Axis ; 交叉轴 Cross Axis
// MediaQuery.of(context).size.width //屏幕宽

/*
Row特点:
    - 水平方向尽可能占据比较大的空间
        * 水平方向也是希望包裹内容, 那么设置mainAxisSize = min
    - 垂直方向包裹内容
    
mainAxisAlignment:
    - start: 主轴的开始位置挨个摆放元素(默认值)
    - end: 主轴的结束位置挨个摆放元素
    - center: 主轴的中心点对齐
    - spaceBetween: 左右两边间距为0, 其它元素之间平分间距
  - spaceAround: 左右两边的间距是其它元素之间的间距的一半
  - spaceEvenly: 所有的间距平分空间
CrossAxisAlignment:
    - start: 交叉轴的起始位置对齐
    - end: 交叉轴的结束位置对齐
    - center: 交叉轴中心点对齐(默认值)
    - baseline: 基线对齐(必须有文本的时候才有效果)
    - stretch: 将所有的子Widget交叉轴的高度, 拉伸到最大; 想要限制其最大高度,需要外面包裹一层Container,并设置Container的height.
*/
return Container(
  height: 300,//通过包一层Container, 来设置最大延伸高度
  child: Row(
    mainAxisAlignment: MainAxisAlignment.end,
    textDirection: TextDirection.rtl,//Row 特有(文字排版方向)
    mainAxisSize: MainAxisSize.min, // 默认: MainAxisSize.max
    //crossAxisAlignment: crossAxisAlignment.baseline,//基线对齐
    //textBaseline: TextBaseline.ideographic, //设置基线(基线对齐必须设置这个)
    crossAxisAlignment: crossAxisAlignment.stretch,
    children: <Widget>[
      Container(width:80, height: 60, color: Colors.red, child:Text("Hellox", style:TextStyle(fontSize:20),),),
      ...
    ],
  )
);

return Column(
    mainAxisAlignment: MainAxisAlignment.end,
    verticalDirection: VerticalDirection.down,//Column 特有(文字排版方向)
    mainAxisSize: MainAxisSize.min, 
    crossAxisAlignment: crossAxisAlignment.baseline,
    textBaseline: TextBaseline.ideographic, 
    children: <Widget>[
      Container(width:80, height: 60, color: Colors.red, child:Text("Hellox", style:TextStyle(fontSize:20),),),
      ...
    ],
  );

1-2 、Expanded

return Row(
  children: [
    /*
    * Flexible中的属性: 可延伸、可伸缩的
    * - flex
    *
    * Expanded 相当于 Flexible( fit: FlexFit.tight,)
    * Expanded 用的比较多
    * */
    Flexible(
      fit: FlexFit.tight,//按比例分配剩余空间
      flex: 1,//使子组件宽度无效, 分配剩余空间的比例
      child: Container(width: 80, height: 60, color: Colors.red, ),
    ),
    Flexible(
      fit: FlexFit.tight,//按比例分配剩余空间
      flex: 2,//使子组件宽度无效, 分配剩余空间的比例
      child: Container(width: 12000, height: 100, color: Colors.green, ),
    ),
    Expanded(
        flex: 1,//使子组件宽度无效, 分配剩余空间的比例
        child: Container(width: 90, height: 80, color: Colors.blue,)
    ),
    Container(width: 50, height: 120, color: Colors.orange, ),
  ],
),

1-3、Stack组件--重叠效果

/*
* Stack默认大小是包裹内容的
*  - alignment: 从什么位置开始排布所有子Widget. 默认topStart
*  - fit: 默认loose, 按照子元素原本的大小排布. 很少用
*  - overflow: 超出部分如何处理
*  - Positioned: 配合Stack组件,来调整某个子元素的位置
* */
return Stack(
  alignment: AlignmentDirectional.center,//设置组件排布方式
  // fit: StackFit.expand,//将子组件拉伸到尽可能大, 可以外层包一层Container,来设置宽高.
  // overflow: Overflow.visible,
  children: [
    Positioned(//配合Stack组件,来调整某个子元素的位置
        left: 20,
        bottom: -20,
        child: Container(width: 150, height: 300,)
    ),
    Image.asset("assets/images/juren.jepg"),
    Text('进击的巨人')
  ],
)
  
//使用示例:
Stack(
  children: [
    Image.asset("assets/images/juren.jepg"),
    Positioned(//配合Stack组件,来调整某个子元素的位置
        left: 0,
        right: 0,
        // width: double.infinity,//宽度无限大
        bottom: 0,
        child: Container(
          padding: EdgeInsets.all(8),
          color: Color.fromARGB(150, 0, 0, 0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text('进击的巨人', style: TextStyle(fontSize: 20, color: Colors.white),),
              // GestureDetector(child: Icon(Icons.favorite, color: Colors.white,),),
              IconButton(onPressed: ()=> print('点击了收藏'), icon: Icon(Icons.favorite, color: Colors.white,))
            ],
          ),
        )
    ),
  ],
)

第八部分 滚动Widget-listview、gridview、slivers

1-1、ListView--创建方式一

//立即创建
return ListView(
  scrollDirection: Axis.vertical,//滚动方向
  reverse: true,//列表内容反转(倒序<->正序)
  itemExtent: 100,//item主轴固定高度
  children: List.generate(100, (index) {
    // return Text("Hello World: $index", style: TextStyle(fontSize: 30),);
    return ListTile(
      leading: Icon(Icons.people),//前面内容
      trailing: Icon(Icons.delete),//后面内容
      title: Text("联系人${index + 1}"),
      subtitle: Text("联系电话: 1234567"),
    );
  }),
)

1-2、ListView--创建方式二

ListView.builder(//相当于懒加载创建item
  itemCount: 100,//准备展示多少个item, 不会立刻创建
  itemBuilder: (BuildContext ctx, int index) {
    //itemBuilder 是回调函数,当有item被展示到屏幕时才会被调用
    return Text("Hello World: $index");//返回每个Item的UI组件
  }
)

1-3、ListView--创建方式三

//同样返回回调函数, 在需要的时候创建
return ListView.separated(
    itemBuilder: (BuildContext ctx, int index) {//item样式
      return Text("Hello World: $index");
    },
    separatorBuilder: (BuildContext ctx, int index) {//分割线样式
      return Divider(//也可以返回其他Widget
        color: Colors.red, //设置分割线的颜色
        height: 3, //设置分割线显示区域
        thickness: 10,//设置分割线的高度
        indent: 30, //设置距离开始侧的距离
        endIndent: 30,//设置距离结束侧的距离
      );
    },
    itemCount: 100
)

2-1、GridView-构造方法一

Padding(//给GridView设置水平外边距
  padding: const EdgeInsets.symmetric(horizontal: 8),//vertical无效
  child: GridView(
    //SliverGridDelegateWithFixedCrossAxisCount 每行固定数量
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3, //固定数量
      childAspectRatio: 1.5, //item的宽高比
      crossAxisSpacing: 8, //交叉轴间距
      mainAxisSpacing: 8,//主轴间距
    ),
    children: List.generate(100, (index) {
      return Container(
        color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
      );
    }),
  ),
)

2-2、GridView-构造方法二

Padding(//给GridView设置水平外边距
  padding: const EdgeInsets.symmetric(horizontal: 8),//vertical无效
  child: GridView(
    //SliverGridDelegateWithMaxCrossAxisExtent 每行设置宽度最大值
    gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
      maxCrossAxisExtent: 100,//宽度最大是100
      childAspectRatio: 1.5, //item的宽高比
      crossAxisSpacing: 8, //交叉轴间距
      mainAxisSpacing: 8,//主轴间距
    ),
    children: List.generate(100, (index) {
      return Container(
        color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
      );
    }),
  ),
)

2-3、GridView-构造方法三

Padding(//给GridView设置水平外边距
  padding: const EdgeInsets.symmetric(horizontal: 8),//vertical无效
    /// 当item准备在屏幕上显示时, 才加载
  child: GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 8, //交叉轴间距
        mainAxisSpacing: 8,//主轴间距
      ),
      itemBuilder: (BuildContext ctx, int index){
        return Container(//此时: 这里设置高度无效, 只能设置宽高比
          color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
        );
      }
  )
)

3-1、Slivers的基本使用

因为我们需要把很多的Sliver放在一个CustomScrollView中, 所以CustomScrollView有一个slivers属性, 里面让我们放对应的一些Sliver:
    SliverList: 类似于我们之前使用过的ListView;
    SliverFixedExtentList 类似于SliverList只是可以设置滚动的高度;
    SliverGrid: 类似于我们之前使用过的GridView;
    SliverPadding: 设置Sliver的内边距, 因为可能要单独给Sliver设置内边距;
    SliverAppBar: 添加一个AppBar, 通常用来作为CustomScrollView的HeaderView;
    SliverSafeArea: 设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)
 
存放一个sliver的实例代码: 
return SafeArea(//设置安全区域
  child: CustomScrollView(
    slivers: [// 这里面只能放sliver
      SliverSafeArea(//也可以在这里设置安全区域
        sliver: SliverPadding(
          padding: EdgeInsets.all(8),//设置内间距
          sliver: SliverGrid(
              delegate: SliverChildBuilderDelegate(
                  (BuildContext ctx, int int){
                    return Container(
                      color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                    );
                  },
                childCount: 100 //数量100个
              ),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 8, //交叉轴间距
                mainAxisSpacing: 8,//主轴间距
                childAspectRatio: 1.5
              )
          ),
        ),
      )
    ],
  ),
),

3-2、Slivers的基本使用-导航

return CustomScrollView(
  slivers: [// 这里面只能放sliver
    SliverAppBar(// slivers的导航
      pinned: true,//默认会随着内容的滚动而滚动的, 设置为true就不滚动了
      expandedHeight: 300,//扩展默认高度
      flexibleSpace: FlexibleSpaceBar(
        title: Text("Hello World"),
        background: Image.asset("assets/images/juren.jpeg", fit: BoxFit.cover),
      ),
    ),
    SliverSafeArea(//也可以在这里设置安全区域
      sliver: SliverPadding(
        padding: EdgeInsets.all(8),//设置内间距
        sliver: SliverGrid(
            delegate: SliverChildBuilderDelegate(
                (BuildContext ctx, int int){
                  return Container(
                    color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                  );
                },
              childCount: 100 //数量100个
            ),
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              crossAxisSpacing: 8, //交叉轴间距
              mainAxisSpacing: 8,//主轴间距
              childAspectRatio: 1.5
            )
        ),
      ),
    )
  ],
),

3-3、Slivers监听滚动

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

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

class _HYHomePageState extends State<HYHomePage> {
  ScrollController _controller = ScrollController(
      initialScrollOffset: 300 //设置默认偏移滚动值
  );
  bool _isShowFloatingBtn = false;

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

    _controller.addListener(() {
      print("监听到滚动...滚动值:${_controller.offset}");
      setState(() {
        _isShowFloatingBtn = _controller.offset >= 1000;
      });
    });
  }
  
  @override
  Widget build(BuildContext context) {
    /*
    * 两种方式可以监听:
    *   controller:
    *     1.可以设置默认值offset
    *     2.监听滚动, 也可以监听滚动的位置
    *   NotificationListener: (这是一个Widget)
    *     1.可以监听开始滚动和结束滚动
    * */
    return Scaffold(
      appBar: AppBar(
        title: Text("导航标题"),
      ),
      body: NotificationListener(
        onNotification: (ScrollNotification notification){

          if(notification is ScrollStartNotification) {
            print("开始滚动");
          } else if(notification is ScrollUpdateNotification){
            print("正在滚动... 总滚动范围:${notification.metrics.maxScrollExtent} 当前滚动位置: ${notification.metrics.pixels}");
          } else if(notification is ScrollEndNotification){
            print("结束滚动");
          }

          return true;//想冒泡返回false, 不想冒泡返回true
        },
        child: ListView.builder(
            controller: _controller,
            itemCount: 100,
            itemBuilder: (BuildContext ctx, int index){
              return ListTile(
                leading: Icon(Icons.people),
                title: Text("联系人$index"),
              );
            }
        ),
      ),
      /// 实现功能一键置顶
      floatingActionButton: _isShowFloatingBtn? FloatingActionButton(
        child: Icon(Icons.arrow_upward),
        onPressed: (){
          _controller.animateTo(
              0, //动画结束位置
              duration: Duration(seconds: 1), //动画时间
              curve: Curves.easeIn //动画效果: 由快到慢、由慢到快等
          );
        },
      ): null,
    );
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容