Flutter入门到入土[总]

Flutter


Flutter是谷歌基于Dart语言开发的一款开源、免费且跨平台的App开发框架,所以建议先学习Dart语言的基本语法,很像JavaScriptJava,学起来很快。这篇文章打算直接总结如何使用Flutter框架,所以比较长,但是讲解的东西比较简短

环境配置

安装JDK


  1. 前往Java Downloads - Oracle下载所需要的jdkJDK17版本够用了

    image-20240221220731441

  2. 配置系统环境

    1. 新建系统变量,第一行输入JAVA_HOME,第二行输入安装jdk的目录

    image-20240221220920299

    1. 在系统变量中找到Path双击点开,点击新建,并输入%JAVA_HOME%\bin

      image-20240221221122941

    2. 一直点确定,然后再cmd中输入java -version,一般来说出现的就是版本号,若不行,重启下电脑,若还不行,重新安装吧!

      image-20240221221239951

安装Android Studio


  1. 前往下载 Android Studio ,并安装

  2. 安装完毕之后,点击左边的第三项,插件,我这里汉化了,汉化教程

    image-20240221222652529

  3. 搜索并安装这两个插件,然后重启Android Studio

    image-20240221222754434

安装FlutterSDK


  1. 前往Flutter官网下载FlutterSDKFlutter SDK archive - Flutter

    image-20240221221501653

  2. 将下载好的压缩包解压,然后配置环境,双击系统变量的Path,新建并输入刚刚解压到的文件夹到bin目录的路径

    image-20240221222100461

  3. cmd中输入flutter doctor,等待一下,出现这些

    image-20240221222239684

    前三项和倒数三项不是x一般就能用,如果第三项出现问题,先安装Android Studio

  4. 常见问题

    若第三项出现问题,先安装Android Studio

    Android-SDK的系统环境,未配置,在系统变量中新建并第一行输入ANDROID_HOME,第二行输入你安装Android Studio时,安装Android-SDK的路径,然后重启电脑,一般就行了

    image-20240221224246524

    如果flutter doctor时出现cmdline-tools component is missing

    根据以下步骤:

    1. 打开Android Studio,关闭项目,到开始页面,点最右边的竖着的三个点,选择SDK Manager

      image-20240227133113310

    2. 点第二项SDK Tools,在下面找到Android SDK Command-line Tools并勾选,点击确定

      image-20240227133341573

    3. 点击确定[我这里因为已经最新了,所以下载的东西与你们出问题的不一样]

      image-20240227133545643

    4. 等待这个界面下载,下载完成就点完成

      image-20240227133616137

    如果还不行,推荐重新安装Android SDK!

创建Flutter项目并运行

创建


  1. 一切准备好之后,会看到Android Studio右上角有个New Flutter Project的按钮,点这个按钮是为了创建Flutter项目

    image-20240221224925325

  2. 点进去之后,右边点击Flutter,然后下一步

  3. 这里就是项目的基本信息

    • 项目名称:随便你想怎么起怎么起
    • 项目路径:看你想放哪
    • 项目描述:这一条无所谓
    • 项目类型:想做app就选Application
    • 所属组织:随你
    • 安卓系统的编译语言:一般选第二项
    • IOS系统的编译语言:一般选第二项
    • 所在平台:一般全选

    然后填好之后,点左下角创建

    image-20240221225231548

    最后,我们需要编写的文件,就在lib

    运行


    先点击右上角的手机图标,然后选择Edge,再点击运行,如果没有这种运行,左边的项目找到项目的目录,然后点开lib,再双击.dart文件,再编辑器右键点击小三角运行

    image-20240221223029914

flutter的基本要素


  1. 创建第一个flutter,需要导入一个包package:flutter/material.dart

  2. 使用main函数,让他可以运行runApp()方法

  3. runApp()需要填写容器,这里先使用Center

  4. Center里面需要填写child

  5. Text()即是创建一个文本到Center

  6. Text里可以有textDirection为让文字从左到右[ltr]还是从右到左[rtl]

  7. Text里的style可以改变文字样式

    import 'package:flutter/material.dart';
    //demo1-Center
    void main() {
      runApp(const Center(
        child: Text(
          "hello world!",
          textDirection: TextDirection.ltr,
          style: TextStyle(
            fontSize: 20.0,
            color: Colors.brown
          ),
        )
      ));
    }
    

MaterialApp and Scaffold

MaterialApp常用的属性


属性名 用处
home 主页
title 标题
color 颜色
theme 主题
routes 路由

MaterialApp-home

填写容器,一般以Scaffold为主,Scaffold是与MaterialApp的另一种组件

import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
  runApp(MaterialApp(
    home: Scaffold()
  ));
}

MaterialApp-title|color


MaterialApp的标题和颜色,但是一般不用

MaterialApp-theme


整个软件的主题,填写的参数有要求

  • ThemeData.light():白色主题
  • ThemeData.dark():黑色主题
import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
  runApp(MaterialApp(
    theme:ThemeData.dark()
  ));
}

MaterialApp-routes


none

Scaffold的常用属性


  • appBar - 用于显示顶部部分
  • body - 用于显示主要内容
  • drawer - 左边抽屉菜单

Scaffold-appBar


如何使用appBar

import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
  runApp(MaterialApp(
    home:Scaffold(
      appBar:AppBar(
        title:Text("demo2")
      )
  ));
}

其实appBar还有一些属性

  • backgroundColor - 用于设置背景颜色
  • centerTitle - 是否居中文字
import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
  runApp(MaterialApp(
    home:Scaffold(
      appBar:AppBar(
        title:Text("demo2")
      ),
      backgroundColor: Colors.lightGreen,
      centerTitle: true,
    )
  ));
}

Scaffold-body


主体部分,填写容器就行了,这里用了Center

import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
  runApp(MaterialApp(
    home: Scaffold(
      body: const Center(
        child: Text(
          "Hello world!",
          textDirection: TextDirection.ltr,
          style: TextStyle(
              color: Colors.greenAccent,
              fontSize: 30.0
          ),
        ),
      ),
    )
  ));
}

Scaffold-drawer


  1. 编写一个简单的界面

    import 'package:flutter/material.dart';
    //demo4-Drawer
    void main(){
      runApp(MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: const Text(
                "demo4-Drawer",
                textDirection: TextDirection.ltr
            ),
            backgroundColor: Colors.blue,
            titleTextStyle:const TextStyle(
                color: Colors.amber
            ),
          ),
          drawer:
        ),
      ));
    }
    
  2. darwer需要填写与之相应的Darwer()

    里面常用的两个属性

    • backgroundColor - 抽屉菜单的背景颜色
    • child - 抽屉菜单的内容
    import 'package:flutter/material.dart';
    //demo4-Drawer
    void main(){
      runApp(MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: const Text(
                "demo4-Drawer",
                textDirection: TextDirection.ltr
            ),
            backgroundColor: Colors.blue,
            titleTextStyle:const TextStyle(
                color: Colors.amber
            ),
          ),
          drawer:const Drawer(
            backgroundColor: Colors.lightBlue,
            child:Text(
              "DrawerTitle",
              textDirection: TextDirection.ltr,
              style: TextStyle(
                color: Colors.greenAccent,
                fontSize: 30.0,
              ),
            ),
          ),
        ),
      ));
    }
    
  3. 总体代码

    import 'package:flutter/material.dart';
    //demo4-Drawer
    void main(){
      runApp(MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: const Text(
                "demo4-Drawer",
                textDirection: TextDirection.ltr
            ),
            backgroundColor: Colors.blue,
            titleTextStyle:const TextStyle(
                color: Colors.amber
            ),
          ),
          body: HomeComp(),
          drawer:const Drawer(
            backgroundColor: Colors.lightBlue,
            child:Text(
              "DrawerTitle",
              textDirection: TextDirection.ltr,
              style: TextStyle(
                color: Colors.greenAccent,
                fontSize: 30.0,
              ),
            ),
          ),
        ),
      ));
    }
    
    
    class HomeComp extends StatelessWidget{
      // const HomeComp ({Key ? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return const Center(
          child: Text(
            "Hello world",
            textDirection: TextDirection.ltr,
            style: TextStyle(
              color: Colors.green,
    
            ),
          ),
        );
      }
    
    }
    

分离组件


我们在编写软件的时候,不会把所有的东西功能和界面全部写在一起,那样会让我们看起来很烦躁,因为非常的乱

  1. 先编写一个普通的界面,现在,代码是报错的,因为body还没有填写东西

    import 'package:flutter/material.dart';
    //demo3-分离组件
    void main(){
      runApp(MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: const Text("demo3-分离组件"),
            backgroundColor: Colors.blue,
            titleTextStyle:const TextStyle(
              color: Colors.amber
            ),
          ),
          body: ,
        ),
      ));
    }
    
  2. 在main方法下面,编写一个组件类,让这个类继承StatelessWidget,此时会报错,将鼠标放上去,让系统自动帮你把方法写出来

    const HomeComp({super.key});这一行一般都是这样的形式,照写就行

    class HomeComp extends StatelessWidget{
      const HomeComp({super.key});
      @override
      Widget build(BuildContext context) {
       
      }
    }
    
  3. 在继承的方法返回容器即可

    class HomeComp extends StatelessWidget{
      const HomeComp({super.key});
    
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return const Center(
          child: Text(
            "Hello world",
            style: TextStyle(
              color: Colors.green,
            ),
          ),
        );
      }
    }
    
  4. 总体代码就是这样

    import 'package:flutter/material.dart';
    //demo3-分离组件
    void main(){
      runApp(MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: const Text("demo3-分离组件"),
            backgroundColor: Colors.blue,
            titleTextStyle:const TextStyle(
              color: Colors.amber
            ),
          ),
          body: const HomeComp(),
        ),
      ));
    }
    
    
    class HomeComp extends StatelessWidget{
      const HomeComp({super.key});
    
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return const Center(
          child: Text(
            "Hello world",
            style: TextStyle(
              color: Colors.green,
            ),
          ),
        );
      }
    }
    

基础组件

Container容器组件

alignment的参数


参数 说明
Alignment.topCenter 顶部居中对齐
Alignment.topLeft 顶部左对齐
Alignment.topRight 顶部右对齐
Alignment.topRight 顶部右对齐
Alignment.center 水平垂直居中对齐
Alignment.centerLeft 垂直居中水平居左对齐
Alignment.centerRight 垂直居中水平居右对齐
Alignment.bottomCenter 底部居中对齐
Alignment.bottomLeft 底部居左对齐
Alignment.bottomRight 底部居右对齐

decoration的属性


先使用BoxDecoration(),里面的参数为

属性 说明
gradient 渐变 LinearGradient()为背景线性渐变 RadialGradient()为径向渐变,这两个方法里面需要color参数,可以书数组
boxShadow 阴影 需要定义一个常量数组,数组里面需要BoxShadow(),里面有color[颜色]、offset[位移,参数为Offset(x, y)]、blurRadius[虚化程度]
border 边框 需要使用Border.all(),里面有color[颜色]、width[线条大小]
borderRadius 盒子圆角 需要使用BorderRadius.circular(),参数就是double类型的数字

其他属性


属性 说明
margin 容器与外部的距离,参数使用EdgeInsets.all(double)或者EdgeInsets.fromLTRB(double,double,double,double)
padding 容器边框与child的距离,同上
transform 让容器可以位移旋转
`height width`
child 子元素

Text组件

textAlign参数


文字对齐方式

参数 说明
TextAlign.center 文字居中
TextAlign.left 文字左对齐
TextAlign.right 文字右对齐
TextAlign.justfy 文字两端对齐

textDirection参数


文字方向

参数 说明
TextDirection.ltr 从左至右
TextDirection.rtl 从右至左

overflow参数


文字超出屏幕之后的处理方式

参数 说明
TextOverflow.visible 强制显示
TextOverflow.clip 裁剪隐藏
TextOverflow.fade 渐隐
TextOverflow.ellipsis 省略号

textScaler参数


textScaleFactor已弃用,用textScaler代替,用于字体显示倍率

参数 说明
TextScaler.linear(double) 字体显示倍率,1为正常

maxLines参数


显示最大行数

  • 填写整数即可

style属性


需要搭配TextStyle()使用,以下是TextStyle()属性

  • decoration:文字的装饰线

    参数 说明
    TextDecoration.none 无线条
    TextDecoration.lineThrough 删除线
    TextDecoration.overline 上划线
    TextDecoration.underline 下划线
  • decorationColor:装饰线的颜色

  • decorationStyle:装饰线样式

    参数 说明
    TextDecorationStyle.dashed 横杆虚线
    TextDecorationStyle.dotted 点虚线
    TextDecorationStyle.double 双线
    TextDecorationStyle.solid 实线
    TextDecorationStyle.wavy 波浪线
  • wordSpacing单词间隙,负值会让单词变得更紧凑

  • letterSpacing字母间隙,负值,会让字母变得更紧凑

  • fontStyle:文字样式

    参数 说明
    FontStyle.normal 正常字体
    FontStyle.italic 斜体
  • fontSize:文字大小

  • color:文字颜色

  • fontWeight:文字粗细

    参数 说明
    FontWeight.normal 正常粗细
    FontWeight.normal 粗体
    FontWeight.wx00 按照整百粗细(100-900)

图片组件


图片组件分为两种

  • Image.asset
  • Image.network

属性


两种的参数都是一样的,就是引用图片的方式不一样

alignment:图片对齐方式

参数 说明
Alignment.topCenter 顶部居中对齐
Alignment.topLeft 顶部左对齐
Alignment.topRight 顶部右对齐
Alignment.center 水平垂直居中对齐
Alignment.centerLeft 垂直居中水平居左对齐
Alignment.centerRight 垂直居中水平居右对齐
Alignment.bottomCenter 底部居中对齐
Alignment.bottomLeft 底部居左对齐
Alignment.bottomRight 底部居右对齐

fit:图片的填充

参数 说明
BoxFit.fill 拉伸填满
BoxFit.contain 按原始比例,可能会有空隙
BoxFit.cover 图片填满整个容器,不变形,但是会被剪裁
BoxFit.fitWidth 宽度充满
BoxFit.fitHeight 高度充满
BoxFit.scaleDown 效果和contain差不多,但是不允许显示超过原图片的大小,可小不可大

color:图片背景颜色

colorBlendMode:图片混合颜色选项

  • 参数比较多,都是比较基本的图片颜色混合

    image-20240208211305060

repeat:图片平铺

  • ImageRepeat.noRepeat:不重复
  • ImageRepeat.repeat:横向和总线都重复
  • ImageRepeat.repeatX:横向重复,纵向不重复
  • ImageRepeat.repeatY:纵向重复,横向不重复

width 和 height:图片宽高

Image.asset


  1. 在目录新建储存图片的文件夹

    image-20240208211756026

  2. pubspec.yaml文件中声明图片文件[注意格式和空格],在这个地方添加⬇

    flutter:
      # The following line ensures that the Material Icons font is
      # included with your application, so that you can use the icons in
      # the material Icons class.
      uses-material-design: true
      assets:
        - "img/head.jpg"
      #在这个地方添加,认准这个地方,下面这里的这四行注释下面也行
      # To add assets to your application, add an assets section, like this:
      # assets:
      #   - images/a_dot_burr.jpeg
      #   - images/a_dot_ham.jpeg
      
    
  3. child中使用Image.asset()

    // 这只是个组件
    class Demo6 extends StatelessWidget{
      const Demo6({super.key});
    
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Center(
          child: Image.asset(
            "img/head.jpg",
          )
        );
      }
    }
    

Image.network


这个也同理,就是不用配置什么东西

// 这只是个组件
class Demo6 extends StatelessWidget{
  const Demo6({super.key});

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Center(
      child: Image.network(
        "https://www.baidu.com/img/flexible/logo/pc/result.png",
      )
    );
  }
}

图标组件


使用Icon()来使用官方给的图标

class Demo7 extends StatelessWidget{
  const Demo7({super.key});

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return const Center(
      child: Column(
        children: [
          Icon(
            Icons.add,
          ),
          Icon(
            Icons.add_box_rounded,
            color: Colors.lightBlueAccent,
          ),
          Icon(
            Icons.accessibility_rounded,
            color: Colors.green,
            size: 30,
          )
        ],
      ),
    );
  }}

属性


属性 说明
Icons.图标名 使用官方的图标
color 图标颜色
size 图标大小,默认20

列表组件


通过使用组件ListView()创建列表

属性


属性 说明
scrollDirection 设置列表类型,参数为 Axis.vertical垂直列表[默认]、Axis.horizontal 水平列表
padding 内边距,与其他组件一样
resolve 是否反向排序,参数:truefalse
children 列表的元素,参数为数组

最简单的列表


通过使用ListTile()创建列表项,利用ListTile里的参数title可以将其他元素放进去列表项

// 垂直列表-title
class List1 extends StatelessWidget{
  const List1({super.key});
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: const <Widget>[
        ListTile(
          title: Text("标题1"),
        ),
        ListTile(
          title: Text("标题1"),
        ),
        ListTile(
          title: Text("标题1"),
        ),
        ListTile(
          title: Text("标题1"),
        ),
        ListTile(
          title: Text("标题1"),
        ),
      ],
    );
  }
}

列表项


ListTile(),有必要说一下里面的参数

属性


属性 说明
leading 列表项的最左边[前导],一般放图标或者图片
trailing 列表项的最右边[尾部],一般放图标或者图片
title 列表项的标题
subtitle 列表项的副标题
style 列表项的文本样式
iconColor 图标颜色
tileColor 列表项背景颜色
titleTextStyle 标题文本颜色
subtitleTextStyle 副标题文本颜色
leadingAndTrailingTextStyle 前导和尾部的文本样式
contentPadding 内容的内边距
titleAlignment 标题文本的对齐方式
isThreeLine 额外文本行[三行列表项]

布局

Grid布局

常用属性


属性 说明
scrollDirection 组件滚动方向,Axis.horizontal水平,Axis.vertical垂直
reverse 组件元素是否反向排序
controller 滚动监听
primary 内容不足时是否可以滑动
shrinkWrap 内容适配,默认false
padding 内边距
crossAxisCount 一行或一列的元素数量
mainAxisSpacing 主轴之间间距
crossAxisSpacing 横轴之间间距
childAspectRatio 元素的宽高比例
children 组件元素

使用GridView.count创建网络布局


使用这个方法创建网络布局必须要填写一个值crossAxisCount,也就是一行限制多少个元素

class GridTest1 extends StatelessWidget{
  const GridTest1({super.key});
  @override
  Widget build(BuildContext context) {
    return GridView.count(
      scrollDirection: Axis.horizontal,
      crossAxisCount: 3,
      childAspectRatio: 1.0,
      children: const <Widget>[
        Icon(Icons.account_balance_wallet,color: Colors.green,),
        Icon(Icons.balance,color: Colors.brown,),
        Icon(Icons.camera,color: Colors.lightBlueAccent,),
        Icon(Icons.data_object,color: Colors.orange,),
        Icon(Icons.egg,color: Colors.cyan,),
        Icon(Icons.facebook_outlined,color: Colors.greenAccent,),
        Icon(Icons.gamepad,color: Colors.deepPurple,),
        Icon(Icons.handshake_outlined,color: Colors.lightGreen,),
        Icon(Icons.image_rounded,color: Colors.deepOrange,),
      ],
    );
  }
}

使用GridView.count创建网络布局


使用此方法创建网络布局必须要填写一个值maxCrossAxisExtent,也就是固定横轴的最大长度

class GridTest2 extends StatelessWidget{
  const GridTest2({super.key});

  @override
  Widget build(BuildContext context) {
    return GridView.extent(
      maxCrossAxisExtent: 100,
      childAspectRatio: 1.0,
      children: const <Widget>[
        Icon(Icons.account_balance_wallet,color: Colors.green,),
        Icon(Icons.balance,color: Colors.brown,),
        Icon(Icons.camera,color: Colors.lightBlueAccent,),
        Icon(Icons.data_object,color: Colors.orange,),
        Icon(Icons.egg,color: Colors.cyan,),
        Icon(Icons.facebook_outlined,color: Colors.greenAccent,),
        Icon(Icons.gamepad,color: Colors.deepPurple,),
        Icon(Icons.handshake_outlined,color: Colors.lightGreen,),
        Icon(Icons.image_rounded,color: Colors.deepOrange,),
      ],
    );
  }
}

线性布局

属性


属性 说明 参数
mainAxisAlignment 主轴的排序方式 `MainAxisAlignment.center
crossAxisAlignment 次轴的排序方式 `CrossAxisAlignment.center
children 组件元素

Row布局


class RowTest extends StatelessWidget{
  const RowTest({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: double.infinity,
      width: double.infinity,
      child: const Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.account_balance_wallet,color: Colors.green,size: 50,),
          Icon(Icons.balance,color: Colors.brown,size: 50,),
          Icon(Icons.camera,color: Colors.lightBlueAccent,size: 50,),
          Icon(Icons.data_object,color: Colors.orange,size: 50,),
          Icon(Icons.egg,color: Colors.cyan,size: 50,),
          Icon(Icons.facebook_outlined,color: Colors.greenAccent,size: 50,),
          Icon(Icons.gamepad,color: Colors.deepPurple,size: 50,),
          Icon(Icons.handshake_outlined,color: Colors.lightGreen,size: 50,),
          Icon(Icons.image_rounded,color: Colors.deepOrange,size: 50,),
        ],
      ),
    );
  }
}

Colum布局


class ColumnTest extends StatelessWidget{
  const ColumnTest({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      child:const Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Icon(Icons.account_balance_wallet,color: Colors.green,size: 50,),
          Icon(Icons.balance,color: Colors.brown,size: 50,),
          Icon(Icons.camera,color: Colors.lightBlueAccent,size: 50,),
          Icon(Icons.data_object,color: Colors.orange,size: 50,),
          Icon(Icons.egg,color: Colors.cyan,size: 50,),
          Icon(Icons.facebook_outlined,color: Colors.greenAccent,size: 50,),
          Icon(Icons.gamepad,color: Colors.deepPurple,size: 50,),
          Icon(Icons.handshake_outlined,color: Colors.lightGreen,size: 50,),
          Icon(Icons.image_rounded,color: Colors.deepOrange,size: 50,),
        ],
      ),
    );
  }
}

Flex布局


使用flex布局一般需要搭配Expanded组件一起使用

Flex属性和Expanded属性


Flex属性 说明 Expanded属性 说明
direction flex布局方向,Axis.horizontal水平,Axis.vertical垂直 flex 该组件占总flex的多少,总flex=处于该组件内Expanded组件的flex值之和
children flex的元素 child Expanded的元素

水平布局


class FlexHor extends StatelessWidget{
  const FlexHor({super.key});

  @override
  Widget build(BuildContext context) {
    return Flex(
      direction: Axis.horizontal,
      children: [
        Expanded(flex: 1,child: Container(color: Colors.brown,child: Icon(Icons.egg,color: Colors.teal,size: 50,),)),
        Expanded(flex: 2,child: Container(color: Colors.orange,child: Icon(Icons.facebook_outlined,color: Colors.teal,size: 50,),)),
        Expanded(flex: 3,child: Container(color: Colors.cyan,child: Icon(Icons.data_object,color: Colors.teal,size: 50,),)),
      ],
    );
  }
}

垂直布局


class FlexVer extends StatelessWidget{
  const FlexVer({super.key});

  @override
  Widget build(BuildContext context) {
    return Flex(
      direction: Axis.vertical,
      children: [
        Expanded(flex: 1,child: Container(color: Colors.brown,child: Icon(Icons.egg,color: Colors.teal,size: 50,),)),
        Expanded(flex: 2,child: Container(color: Colors.orange,child: Icon(Icons.facebook_outlined,color: Colors.teal,size: 50,),)),
        Expanded(flex: 3,child: Container(color: Colors.cyan,child: Icon(Icons.data_object,color: Colors.teal,size: 50,),)),
      ],
    );
  }
}

水平与垂直布局的混合使用


image-20240215001347386

class RowAndColumn extends StatelessWidget{
  const RowAndColumn({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 300,
      margin:const EdgeInsets.all(10),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Expanded(
            flex: 1,
            child: Container(color: Colors.teal,),
          ),
          Container(height: 10,),//只是组件之间的分割线
          Expanded(
            flex: 1,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Expanded(flex: 2,child: Container(color: Colors.deepPurple,),),
                Container(width: 10,),//只是组件之间的分割线
                Expanded(flex: 1,child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    Expanded(flex: 1,child: Container(color: Colors.tealAccent,)),
                    Container(height: 10,),//只是组件之间的分割线
                    Expanded(flex: 1,child: Container(color: Colors.lightBlue,))
                  ],
                ))
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Wrap布局

属性


属性 说明
direction 组件主轴的方向,参数为Axis.horizontal水平,Axis.vertical垂直
spacing 主轴元素之间的间距
runSpacing 新行元素之间的间距
alignment 主轴元素的对齐方式
runAlignment 新行元素的对齐方式
verticalDirection 定义children摆放顺序,默认是VerticalDirection.down,参数还有VerticalDirection.up
children 组件元素

image-20240216010733781

class WrapTest extends StatelessWidget{
  const WrapTest({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.topCenter,
      child: Container(
        width: 300,
        alignment: Alignment.topCenter,
        child: Wrap(
          spacing: 5,
          runSpacing: 5,
          direction: Axis.horizontal,
          alignment: WrapAlignment.start,
          runAlignment: WrapAlignment.center,
          verticalDirection: VerticalDirection.down,
          children: [
            TextButton(onPressed: (){}, child: const Text("Btn1")),
            TextButton(onPressed: (){}, child: const Text("Btn22")),
            TextButton(onPressed: (){}, child: const Text("Btn333")),
            TextButton(onPressed: (){}, child: const Text("Btn4444")),
            TextButton(onPressed: (){}, child: const Text("Btn55555")),
            TextButton(onPressed: (){}, child: const Text("Btn666666")),
            TextButton(onPressed: (){}, child: const Text("Btn7777777")),
            TextButton(onPressed: (){}, child: const Text("Btn88888888")),
            TextButton(onPressed: (){}, child: const Text("Btn999999999")),
            TextButton(onPressed: (){}, child: const Text("Btn1000000000")),
            TextButton(onPressed: (){}, child: const Text("Btn11")),
          ],
        ),
      ),
    );
  }
}

Stack布局


一般来说,会使用其他组件来与Stack布局一起使用

属性


属性 说明
alignment 对齐方式,参数Alignment.*
textDirection 文本方向,参数TextDirection.*
children 子组件

使用stack布局,会让它的子组件都堆叠在一起,如下

image-20240218015044614

class StackTest extends StatelessWidget{
  const StackTest({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      child: const Stack(
        alignment: Alignment.topCenter,
        children: [
          Text("test1"),
          Text("test2"),
          Text("test3"),
          Text("test4"),
          Text("test5"),
          Text("test6"),
          Text("test7"),
        ],
      ),
    );
  }
}

结合Align使用


Align组件一般来说,只用通过alignment属性来控制组件的位置

Align属性
属性 说明
alignment 对齐方式,参数Alignment.*
child 子组件

如果位置不够,Stack布局里的组件依然会堆叠

image-20240218015411459

class StackAndAlign extends StatelessWidget{
  const StackAndAlign({super.key});

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      child: const Stack(
        textDirection: TextDirection.ltr,
        alignment: Alignment.topCenter,
        children: [
          Align(
            alignment: Alignment.topLeft,
            child: Text("test1"),
          ),
          Align(
            alignment: Alignment.topCenter,
            child: Text("test2"),
          ),
          Align(
            alignment: Alignment.topRight,
            child: Text("test3"),
          ),
          Align(
            alignment: Alignment.centerLeft,
            child: Text("test4"),
          ),
          Align(
            alignment: Alignment.center,
            child: Text("test5"),
          ),
          Align(
            alignment: Alignment.centerRight,
            child: Text("test6"),
          ),
          Align(
            alignment: Alignment.bottomLeft,
            child: Text("test7"),
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Text("test8"),
          ),
          Align(
            alignment: Alignment.bottomRight,
            child: Text("test9"),
          ),
        ],
      ),
    );
  }
}
创建一个底部导航栏

通过使用Align的对齐方式定位,将导航栏固定在底部

image-20240218015710797

class TabBarSelf extends StatelessWidget{
  const TabBarSelf({super.key});
  List<Widget> getLists(){
    List<Widget> lists = [];
    for(int i = 0;i<100;i++){
      lists.add(
          ListTile(
            leading: Text("[${i+1}]"),
            title: Text("标题${i+1}"),
            subtitle: Text("数据${i+1}"),
            trailing: Text("|${i+1}|"),
            tileColor: Color.fromARGB(Random().nextInt(255), Random().nextInt(255), Random().nextInt(255), 1),
          )
      );
    }
    return lists;
  }
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        ListView(
          children: getLists(),
        ),
        Align(
          alignment: Alignment.bottomRight,
          child: Container(
            width: double.infinity,
            height: 40,
            color: Colors.lightBlue,
            child: const Center(
              child: Text("我是个导航栏"),
            ),
          ),
        ),
      ],
    );
  }
}

结合Positioned使用


使用Positioned组件,可以让组件位于哪个地方,就位于哪个地方,相当于绝对定位

Positioned属性

属性 说明
`left top
`width height`
child 子组件

image-20240218020510178

class StackAndPositioned extends StatelessWidget{
  const StackAndPositioned({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      child: const Stack(
        alignment: Alignment.topCenter,
        children: [
          Positioned(
            child: Text("top0,left0"),
            top: 0,
            left: 0,
          ),
          Positioned(
            child: Text("top100,left0"),
            top: 100,
            left: 0,
          ),
          Positioned(
            child: Text("top0,left100"),
            top: 0,
            left: 100,
          ),
          Positioned(
            child: Text("top100,left100"),
            top: 100,
            left: 100,
          ),
          Positioned(
            child: Text("top0,right0"),
            top: 0,
            right: 0,
          ),
          Positioned(
            child: Text("bottom0,left0"),
            bottom: 0,
            left: 0,
          ),
          Positioned(
            child: Text("bottom0,right0"),
            bottom: 0,
            right: 0,
          ),
        ],
      ),
    );
  }
}
创建一个悬浮按钮

可以使用Positioned组件的绝对定位的特点,让按钮随时固定在窗口的一个位置

image-20240218020618137

class SpecialBtn extends StatelessWidget{
  const SpecialBtn({super.key});
  List<Widget> getLists(){
    List<Widget> lists = [];
    for(int i = 0;i<100;i++){
      lists.add(
          ListTile(
            leading: Text("[${i+1}]"),
            title: Text("标题${i+1}"),
            subtitle: Text("数据${i+1}"),
            trailing: Text("|${i+1}|"),
            tileColor: Color.fromARGB(Random().nextInt(255), Random().nextInt(255), Random().nextInt(255), 1),
          )
      );
    }
    return lists;
  }
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        ListView(
          children: getLists(),
        ),
        Positioned(
          right: 10,
          bottom: 10,
          child: Container(
            width: 40,
            height: 40,
            alignment: Alignment.center,
            child: ElevatedButton(
              style: ButtonStyle(
                backgroundColor: MaterialStateColor.resolveWith((states) => Colors.limeAccent),
                padding: MaterialStateProperty.all(const EdgeInsets.all(0)),
              ),
              onPressed: () {
                print("你点击了按钮");
              },
              child: const Text("Btn"),
            ),
          )
        ),
      ],
    );
  }
}

AspectRatio布局


此布局的理解比较的复杂,布局的宽高由aspectRatio参数去决定,即宽高比,它会在布局限制条件允许的范围内尽可能的扩展,按照固定比率去尽量占满区域

属性


属性 说明
aspectRatio 宽高比,可能会忽略这个数值
child 子组件
class AspectRatioTest extends StatelessWidget{
  const AspectRatioTest({super.key});

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 3/2,
      child: Container(
        color: Colors.limeAccent,
      ),
    );
  }

按钮


flutter内置了一些比较好的按钮,虽然样式设置起来有些许麻烦

属性


属性 说明
onPressed 点击事件 [必填]
style 按钮样式,需要使用ButtonStyle来调整样式
child 子组件 [必填]

Flutter内置的按钮

按钮 说明
ElevatedButton 最普通的按钮,有背景颜色,有少许阴影,点击有颜色变化,无边框
TextButton 文本按钮,无背景颜色,无阴影,点击有颜色变化,无边框
OutlinedButton 边框按钮,无背景颜色,无阴影,点击有颜色变化,有边框
IconButton 图标按钮,无背景颜色,无阴影,点击有颜色变化,无边框
ElevatedButton.icon 带图标的普通按钮,样式如上,icon必填
TextButton.icon 带图标的文本按钮,样式如上,icon必填
OutlinedButton.icon 带图标的边框按钮,样式如上,icon必填

image-20240218231334250

class BtnTest extends StatelessWidget{
  const BtnTest({super.key});

  @override
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 4,
      children: [
        ElevatedButton(onPressed: (){}, child: const Text("普通按钮")),
        TextButton(onPressed: (){}, child: const Text("文本按钮")),
        OutlinedButton(onPressed: (){}, child: const Text("边框按钮")),
        IconButton(onPressed: (){}, icon: const Icon(Icons.gamepad_outlined)),
        ElevatedButton.icon(onPressed: (){}, icon: const Icon(Icons.tag_faces), label: const Text("普通按钮[有图标]")),
        TextButton.icon(onPressed: (){}, icon: const Icon(Icons.tag_faces), label: const Text("文本按钮[有图标]")),
        OutlinedButton.icon(onPressed: (){}, icon: const Icon(Icons.tag_faces), label: const Text("边框按钮[有图标]")),
      ],
    );
  }
}

修改按钮宽高


通过使用SizeBox来调整按钮,SizeBox中的width和height调整盒子的大小,进而调整按钮宽高

image-20240218231425408

class BtnTest1 extends StatelessWidget{
  const BtnTest1({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 150,
      height: 50,
      child: ElevatedButton(
        onPressed: (){},
        child: const Text("修改宽高"),
      ),
    );
  }
}

修改按钮样式

想要修改按钮的样式,需要通过ButtonStyle去调整,而里面的参数比较的复杂

ButtonStyle的属性

属性 说明 参数
backgroundColor 背景颜色 MaterialStateProperty.all(Colors.*)
foregroundColor 组件内容颜色 MaterialStateProperty.all(Colors.*)
shadowColor 阴影颜色 MaterialStateProperty.all(Colors.*)
shape 按钮形状 MaterialStateProperty.all(*) 这个比较特殊
padding 内边距 MaterialStateProperty.all(const EdgeInsets.*)
elevation 阴影范围 MaterialStateProperty.all(num)
side 边框 MaterialStateProperty.all(const BorderSide(width: num,color: Colors.*))
alignment 组件对齐方式 Alignment.*

设置按钮圆角

shape这个属性的参数比较多变

若要设置圆角,参数为:MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(num)))

image-20240223160855740

class BtnTest2 extends StatelessWidget{
  const BtnTest2({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 150,
      height: 50,
      child: ElevatedButton(
        onPressed: (){},
        style: ButtonStyle(
          backgroundColor: MaterialStateProperty.all(Colors.lightBlueAccent),
          foregroundColor: MaterialStateProperty.all(Colors.purple),
          elevation: MaterialStateProperty.all(10),
          alignment:Alignment.center,
          shadowColor:MaterialStateProperty.all(Colors.black12),
          padding:MaterialStateProperty.all(const EdgeInsets.all(10)),
          shape: MaterialStateProperty.all(RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10)
          )),
        ),
        child: const Text("修改各种属性"),
      ),
    );
  }
}

若要设置圆形,参数为:MaterialStateProperty.*all*(const CircleBorder(side: BorderSide(color: Color.fromARGB(0, 0, 0, 0))))

image-20240223160921240

class BtnTest3 extends StatelessWidget{
  const BtnTest3({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 120,
      height: 120,
      child: ElevatedButton(
        onPressed: (){},
        style: ButtonStyle(
          shape: MaterialStateProperty.all(
            const CircleBorder(side: BorderSide(color: Color.fromARGB(0, 0, 0, 0))
          )),
        ),
        child: const Text("圆形按钮"),
      ),
    );
  }
}

设置按钮边框

image-20240223161246468

class BtnTest4 extends StatelessWidget{
  const BtnTest4({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 120,
      height: 120,
      child: OutlinedButton(
        onPressed: (){},
        style: ButtonStyle(
          side: MaterialStateProperty.all(
            const BorderSide(width: 1,color: Colors.brown)
          ),
        ),
        child: const Text("修改边框"),
      ),
    );
  }
}

事件

PointerEvents


最简单的事件监听,通过Listener()去监听子组件的行为 tips:以下的eevent都是event,只是个变量而已

属性

属性 说明
onPointerDown 按下组件触发,参数:(e)=>{}
onPointerMove 按下并在组件内移动触发,参数:(e)=>{}
onPointerHover 移动到组件触发,参数:(e)=>{}
onPointerUp 按下后松开才触发,参数:(e)=>{}

image-20240228002642882

class Test1 extends StatelessWidget{
  const Test1({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        ListTile(
          title: Listener(
            onPointerDown: (event) => print("按下组件触发:$event"),
            child: const Text("按下组件触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: Listener(
            onPointerMove: (event) => print("按下并在组件内移动触发:$event"),
            child: const Text("按下并在组件内移动触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: Listener(
            onPointerHover: (event) => print("移动到组件触发:$event"),
            child: const Text("移动到组件触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: Listener(
            onPointerUp: (event) => print("按下后松开才触发:$event"),
            child: const Text("按下后松开才触发"),
          ),
        ),
      ],
    );
  }
}

GestureDetector


GestureDetector封装了非常多的触发类型,所以直接使用GestureDetector()组件可以直接使用这些监听类型

Tap单击 属性


属性 说明
onTapDown 按下触发,参数(e)=>{}
onTapUp 抬起触发,参数(e)=>{}
onTap 按下后抬起[与抬起触发不同,这个两个条件都得有],参数()=>{}
onTapCancel 按下后取消[可移出组件触发],参数()=>{}

image-20240228002720931

class Test2 extends StatelessWidget{
  const Test2({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
         ListTile(
          title:GestureDetector(
            onTapDown: (e) => print("按下触发:$e"),
            child: const Text("按下触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title:GestureDetector(
            onTapUp: (e) => print("抬起触发:$e"),
            child: const Text("抬起触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title:GestureDetector(
            onTap: () => print("按下后抬起[与抬起触发不同,这个两个条件都得有]:======="),
            child: const Text("按下后抬起[与抬起触发不同,这个两个条件都得有]"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title:GestureDetector(
            onTapCancel: () => print("按下后取消[可移出组件触发]:======="),
            child: const Text("按下后取消[可移出组件触发]"),
          ),
        ),
      ],
    );
  }
}

DoubleTap双击 属性


属性 说明
onDoubleTap 双击触发[必须按下抬起各两次],参数()=>{}
onDoubleTapCancel 双击时取消[可移出组件触发],参数()=>{}
onDoubleTapDown 双击时第二次点击触发,参数(e)=>{}

image-20240228002755697

class Test3 extends StatelessWidget{
  const Test3({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        ListTile(
          title:GestureDetector(
            onDoubleTap: () => print("双击触发:======="),
            child: const Text("双击触发[必须按下抬起各两次]"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title:GestureDetector(
            onDoubleTapCancel: () => print("双击时取消[可移出组件触发]:======="),
            child: const Text("双击时取消[可移出组件触发]"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title:GestureDetector(
            onDoubleTapDown: (e) => print("双击时第二次点击触发:$e"),
            child: const Text("双击时第二次点击触发"),
          ),
        ),
      ],
    );
  }
}

LongPress长按 属性


属性 说明
onLongPress 长按触发,参数()=>{}
onLongPressStart 长按开始时[长按触发时]触发,参数(e)=>{}
onLongPressEnd 长按结束时[长按抬起]触发,参数(e)=>{}
onLongPressDown 长按按下时[单点也可]触发,参数(e)=>{}
onLongPressUp 长按抬起时触发,参数()=>{}
onLongPressMoveUpdate 长按后移动更新时触发,参数(e)=>{}
onLongPressCancel 长按取消时[也可移出组件]触发,参数()=>{}

image-20240228002910958

class Test4 extends StatelessWidget{
  const Test4({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        ListTile(
          title: GestureDetector(
            onLongPress: ()=>print("长按触发:====="),
            child: const Text("长按触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: GestureDetector(
            onLongPressStart: (e)=>print("长按开始时[长按触发时]触发:$e"),
            child: const Text("长按开始时触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: GestureDetector(
            onLongPressEnd: (e)=>print("长按结束时[长按抬起]触发:$e"),
            child: const Text("长按结束时触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: GestureDetector(
            onLongPressDown: (e)=>print("长按按下时[单点也可]触发:$e"),
            child: const Text("长按按下时触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: GestureDetector(
            onLongPressUp: ()=>print("长按抬起时触发:===="),
            child: const Text("长按抬起时触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: GestureDetector(
            onLongPressMoveUpdate: (e)=>print("长按后移动更新时触发:$e"),
            child: const Text("长按后移动更新时触发"),
          ),
        ),
        Container(height: 1,color: Colors.black87,),
        ListTile(
          title: GestureDetector(
            onLongPressCancel: ()=>print("长按取消时触发:===="),
            child: const Text("长按取消时触发"),
          ),
        ),
      ],
    );
  }
}

Drag拖动 属性


属性 说明
onVerticalDragStart 开始垂直移动,参数(e)=>{}
onVerticalDragUpdate 持续垂直移动,参数(e)=>{}
onVerticalDragEnd 结束垂直移动,参数(e)=>{}
onHorizontalDragStart 开始水平移动,参数(e)=>{}
onHorizontalDragUpdate 持续水平移动,参数(e)=>{}
onHorizontalDragEnd 结束水平移动,参数(e)=>{}

image-20240228003019407

class Test5 extends StatelessWidget{
  const Test5({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
          onVerticalDragStart: (e)=>print("开始垂直移动,$e"),
          onVerticalDragUpdate: (e)=>print("持续垂直移动,$e"),
          onVerticalDragEnd: (e)=>print("结束垂直移动,$e"),
          onHorizontalDragStart: (e)=>print("开始水平移动,$e"),
          onHorizontalDragUpdate: (e)=>print("持续水平移动,$e"),
          onHorizontalDragEnd: (e)=>print("结束水平移动,$e"),
          child: Container(
            width: 150,
            height: 50,
            color: Colors.green,
            child: const Center(
              child: Text("按住我移动一下"),
            ),
          )
      ),
    );
  }
}

事件隔离


因为通过Listener()可以拿到子组件Widget的信息,使用AbsorbPointer()组件和IgnorePointer()组件可以使得这些信息不允许打印出来,而两者的用法一样,只是属性不一样

组件 属性 说明
AbsorbPointer() absorbing true :不会打印,false :会打印
IgnorePointer() ignoring true :不会打印,false :会打印

image-20240228003116469

class Test6 extends StatelessWidget{
  const Test6({super.key});


  @override
  Widget build(BuildContext context) {
    return Flex(
      direction: Axis.horizontal,
      children: [
        Container(width: 5),
        Expanded(
          flex: 1,
          child: IgnorePointer(
            ignoring: true,
            child: Listener(
              onPointerDown: (e)=>print("点了第一个ignoring: true,$e"),
              child: Container(
                height: 50,
                color: Colors.green,
                child: const Text("ignoring: true"),
              ),
            ),
          )),
        Container(width: 5),
        Expanded(
          flex: 1,
          child: AbsorbPointer(
            absorbing: false,
            child: Listener(
              onPointerDown: (e)=>print("点了第二个,absorbing: false,$e"),
              child: Container(
                height: 50,
                color: Colors.lightBlueAccent,
                child: const Text("absorbing: false"),
              ),
            ),
          )),
        Container(width: 5),
      ],
    );
  }
}

输入框


使用TextField()可以创建出一个输入框,Flutter为输入框提供了非常多的属性,供开发人员自定义输入框

属性


属性 说明 参数
controller 控制器,文本的交互需要它 TextEditingController()
decoration 输入框的装饰选项 InputDecoration()
textInputAction 键盘左下角的按钮类型 TextInputAction.*
textCapitalization 键盘默认大小写 TextCapitalization.*
autofocus 是否自动聚焦到输入框 bool,默认false
obscureText 是否将文本变成点,即隐藏文本 bool,默认false
autocorrect 是否自动更正 bool,默认true
maxLength 最大输入字符数 int
cursorWidth 光标宽度 int,默认2.0
cursorColor 光标颜色 color
cursorRadius 光标圆角 Radius.circular(*)
cursorHeight 光标高度 int
cursorOpacityAnimates 光标是否开启闪烁动画 bool
inputFormatters 限制输入文字/数字 []
onTap 点击输入框事件 (){事件}
onTapOutside 点击输入框外事件 (){事件}
onChanged 输入框变更事件 (e){事件}
onEditingComplete 输入框完成输入事件 (){事件}
onSubmitted 输入框提交事件 (){事件}
enabled 是否启用输入框 bool

Controller


一般来说,使用控制器直接使用该对象就好了

TextEditingController controller = TextEditingController();
方法 用途
clear() 清除使用该控制器的输入框内容
text() 设置使用该控制器的输入框内容

但是,使用TextEditingController.fromValue(*)可以自定义输入框的一些属性,*为TextPosition()

TextPosition()有以下两个属性

属性 说明
text 设置使用该控制器的输入框内容,参数为String
selection 设置输入框内边距,参数请继续看↓

selection的参数为TextSelection.fromPosition(*),而里面又需要TextPosition(),里面只有两个属性

属性 说明
affinity 一般填TextAffinity.downstream
offset 输入框的光标距离最左边的距离,参数为int

decoration


可以让输入框自定义成想要的样式,参数为InputDecoration(),以下为InputDecoration()的属性

属性 说明 参数
icon 最左侧的图标,图标下无横线 Icon()
labelText 悬浮文字 String
labelStyle 悬浮文字样式 TextStyle()
helperText 帮助文字 String
helperStyle 帮助文字样式 TextStyle()
hintText 提示文字 String
hintStyle 提示文字样式 TextStyle()
errorText 错误文字 String
errorStyle 错误文字样式 TextStyle()
contentPadding 内容内边距 EdgeInsets.all(*)
prefixIcon 最左边的图标,包含在横线内 Icon()
prefix 最左边自定义Widget Widget
prefixText 最左边文字,只有当输入框聚焦才显示 String
prefixStyle 最左边文字样式 TextStyle()
suffixIcon 最右边的图标,包含在横线内 Icon()
suffix 最右边自定义Widget Widget
suffixText 最右边文字,只有当输入框聚焦才显示 String
suffixStyle 最右边文字样式 TextStyle()
counter 自定义计数器Widget Widget
counterText 计数器文本 String
counterStyle 计数器文本样式 TextStyle()
filled 是否填充颜色 bool,默认false
fillColor 填充颜色 Color
border 输入框整体边框,看下面教程 InputBorder.noneOutlineInputBorder()
enabledBorder 输入框可用时边框,看下面教程 InputBorder.none或不填OutlineInputBorder()
disabledBorder 输入框禁用时边框,看下面教程 InputBorder.none或不填OutlineInputBorder()
errorBorder 输入框错误时边框,看下面教程 InputBorder.none或不填OutlineInputBorder()
focusedBorder 输入框聚焦时边框,看下面教程 InputBorder.none或不填OutlineInputBorder()

border参数详解

参数为InputBorder.none时,输入框不会显示下横线

参数为OutlineInputBorder(),需要通过属性borderRadius设置圆角,borderRadius的参数为BorderRadius.all(Radius.circular(*))

参数为UnderlineInputBorder(),输入框只会在下面显示一条线

最后两个参数的属性borderSide可以设置线条颜色,参数为BorderSide(color: Colors.*)

放demo


image-20240302195145912

class AllInput extends StatelessWidget{
  AllInput({super.key});

  TextEditingController controller = TextEditingController();

  void clearInput(){
    controller.clear();
  }
  TextEditingController userInputController = TextEditingController();
  void clearUserInput(){
    userInputController.clear();
  }

  @override
  Widget build(BuildContext context) {
    controller = TextEditingController.fromValue(
      TextEditingValue(
        text: "初始化文本",
        selection: TextSelection.fromPosition(
          const TextPosition(
            affinity: TextAffinity.downstream,
            offset: 5
          )
        )
      )
    );
    return ListView(
      children: [
        const ListTile(
          title: TextField(),//最简单的输入框
        ),
        ListTile(
          title: TextField(
            controller: controller,
            textInputAction: TextInputAction.next,
            textCapitalization: TextCapitalization.characters,
            autofocus: true,
            obscureText: true,
            autocorrect: true,
            onTap: ()=>print("你点击了输入框"),
            onChanged: (e){
              print("你输入了${e.characters}/共${e.length}字");
            },
            cursorWidth: 10,
            cursorColor: Colors.green,
            cursorRadius: const Radius.circular(5),
            cursorHeight: 50,
            cursorOpacityAnimates: true,
            inputFormatters: [],

          ),
        ),
        ListTile(
          title: TextField(
            controller: controller,
            enabled: false,
          ),
        ),
        const ListTile(
          title: TextField(
            decoration: InputDecoration(
              hintText: "灰色提示文字",
              icon: Icon(Icons.account_box)
            ),
          ),
        ),
        const ListTile(
          title: TextField(
            decoration: InputDecoration(
              hintText: "灰色提示文字",
              prefixIcon: Icon(Icons.facebook_outlined),
              suffixIcon: Icon(Icons.clear),
            ),
          ),
        ),
        const ListTile(
          title: TextField(
            decoration: InputDecoration(
              hintText: "灰色提示文字",
              prefix: Text("user"),
              suffix: Text("后缀"),
            ),
          ),
        ),
        const ListTile(
          title: TextField(
            decoration: InputDecoration(
              hintText: "灰色提示文字",
              label: Text("label"),
              helperText: "帮助文字",
              errorText: "错误提示",
              contentPadding: EdgeInsets.all(10),
              counterText: "计数器",
              filled: true,
              fillColor: Colors.green
            ),
          ),
        ),
        ListTile(
          title: TextField(
            controller: userInputController,
            textInputAction: TextInputAction.next,
            decoration: const InputDecoration(
              hintText: "请输入用户名",
              labelText: "用户名",
              prefixIcon: Icon(Icons.account_box),
              filled: true,
              fillColor: Colors.white70,
              focusedBorder: OutlineInputBorder(
                borderSide: BorderSide(
                  color: Colors.lightBlue
                ),
                borderRadius: BorderRadius.all(Radius.circular(10))
              )
            ),

          ),
        )
      ],
    );
  }
}

Dialog

简单的Dialog


通过AlertDialog(),我们可以很快的创建出一个确认窗口

但是在使用AlertDialog()时,需要使用showDialog(),才能显示

  1. 新建一个方法,并携带context变量,定义一个result变量和打印result

    void alertDialog(context){
    	var result;
    	print("返回值:$result");
    }
    
  2. 使用showDialog(),以及将方法变为异步方法,这里来看一下showDialog的属性

    属性 说明
    context 不理,就填context
    builder 返回的Dialog类型
    barrierDismissible 是否点击蒙版关闭Dialog
    useSafeArea 是否预留安全区域
    void alertDialog(context)async{
    	var result = await showDialog(
    		barrierDismissible: true,
        	useSafeArea: true,
      		context: context,
            builder: builder
    	);
    	print("返回值:$result");
    }
    
  3. builder返回AlertDialog()

    void alertDialog(context)async{
        var result = await showDialog(
            barrierDismissible: true,
            useSafeArea: true,
            context: context,
            builder: (context)=>AlertDialog()
        );
        print("返回值:$result");
    }
    
  4. 编辑AlertDialog()title,content,action

    void alertDialog(context)async{
        var result = await showDialog(
            barrierDismissible: true,
            useSafeArea: true,
            context: context,
            builder: (context)=>AlertDialog(
              title: const Text("标题"),
              content: const Text("内容"),
              actions: [
                TextButton(
                    onPressed: (){
                      print("确定");
                      Navigator.pop(context,"confirm");
                    },
                    child: const Text("确定")
                ),
                TextButton(
                    onPressed: (){
                      print("取消");
                      Navigator.pop(context,"cancel");
                    },
                    child: const Text("取消")
                ),
              ],
            )
        );
        print("返回值:$result");
      }
    
  5. 最后创建一个按钮使用这个方法

    onPressed: (){
    	selectDialog(context);
    },
    
  6. 效果

    image-20240303005012077

多选项Dialog


使用showDialog()可以创建出多选项Dialog

与普通的Dialog创建方法相同,只是里面的children用了SimpleDialogOption(),相当于按钮

void selectDialog(context)async{
    var result = await showDialog(
        barrierDismissible: false,
        context: context,
        builder: (context)=>SimpleDialog(
          title: const Text("标题"),
          children: [
            SimpleDialogOption(
              onPressed: (){
                print("选项1");
                Navigator.pop(context,"选项1");
              },
              child: const Text("选项1"),
            ),
            const Divider(),
            SimpleDialogOption(
              onPressed: (){
                print("选项2");
                Navigator.pop(context,"选项2");
              },
              child: const Text("选项2"),
            ),
            const Divider(),
            SimpleDialogOption(
              onPressed: (){
                print("选项3");
                Navigator.pop(context,"选项3");
              },
              child: const Text("选项3"),
            ),
          ],
        )
    );
    print("返回值:$result");
  }

image-20240303005251254

自定义Dialog


通过继承Dialog这个类,可以创建出高自定义的Dialog

image-20240303005520021

class CustomDialog extends Dialog{
  String title;
  TextStyle titleTextStyle;
  String content;
  TextStyle contentTextStyle;
  Color barrierColor;
  Color backgroundColor;
  double height;
  double width;
  Function()? onFinish;
  CustomDialog({
    super.key,
    required this.title,
    required this.onFinish,
    this.content = "",
    this.barrierColor = Colors.black12,
    this.backgroundColor = Colors.brown,
    this.width = 300,
    this.height = 300,
    this.titleTextStyle = const TextStyle(
      fontSize: 30,
      fontWeight: FontWeight.bold,
      color: Colors.white
    ),
    this.contentTextStyle = const TextStyle(
        fontSize: 12,
        fontWeight: FontWeight.normal,
        color: Colors.white
    ),
  });

  @override
  Widget build(BuildContext context) {
    return Material(
      type: MaterialType.transparency,
      child: Stack(
        children: [
          Container(height: double.infinity,width: double.infinity,color: barrierColor,),
          Center(
            child: Container(
              width: width,
              height: height,
              padding: const EdgeInsets.all(10),
              decoration: BoxDecoration(
                color: backgroundColor,
                borderRadius: BorderRadius.circular(20)
              ),
              child: Stack(
                children: [
                  Align(
                    alignment: Alignment.topCenter,
                    child: Text(
                        title,
                        style: titleTextStyle
                    ),
                  ),
                  Align(
                    alignment: Alignment.centerLeft,
                    child: Text(
                      content,
                      style: contentTextStyle,
                    ),
                  ),
                  Align(
                    alignment: Alignment.bottomCenter,
                    child: Container(
                      height: 50,
                      child: Column(
                        children: [
                          Container(height: 2,color: Colors.white,),
                          Container(
                            height: 48,
                            child: Flex(
                              direction: Axis.horizontal,
                              children: [
                                Expanded(
                                  flex: 1,
                                    child: TextButton(
                                        onPressed: (){
                                          Navigator.pop(context,"confirm");
                                          onFinish;
                                        },
                                        child: const Text(
                                          "确定",
                                          style: TextStyle(
                                              color: Colors.white
                                          ),
                                        )
                                    )
                                ),
                                Container(width: 2,color: Colors.white,),
                                Expanded(
                                  flex: 1,
                                    child: TextButton(
                                        onPressed: (){
                                          Navigator.pop(context,"cancel");
                                        },
                                        child: const Text(
                                          "取消",
                                          style: TextStyle(
                                              color: Colors.white
                                          ),
                                        )
                                    )
                                )
                              ],
                            ),
                          )
                        ],
                      ),
                    )
                  )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

路由


在使用MaterialApp()时,可以使用路由,来实现页面之间的切换,而这里面的路由实现方式是通过将页面加入与移出的思想

最简单的页面切换


通过使用Navigator.push来将新页面加入

Navigator.push(context, MaterialPageRoute(
	builder: (context){
		return const NextPage();
	}
));

传参数入页面


  1. 新建一个页面,和一个实体类,并在页面入口方法中添加属性

    class Person{
      String name;
      int age;
      Person(this.name,this.age);
      @override
      String toString() {
        return "{name:$name,age:$age}";
      }
    }
    class AttrPage extends StatelessWidget{
      final String text;
      final Person person;
      const AttrPage({
        super.key,
        required this.text,
        required this.person
    });
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("AttrPage"),
            backgroundColor: Colors.cyanAccent,
          ),
          body: Center(
            child: Column(
              children: [
                Text("text:$text"),
                Text("person:${person.toString()}")
              ],
            ),
          ),
        );
      }
    }
    
  2. 在主页面的按钮添加点击事件,用Navigator.push将页面加入栈,并在返回页面的对象里写入属性的参数

    onPressed: (){
    	Navigator.push(context, MaterialPageRoute(
    		builder: (context){
    			return AttrPage(text: "TestText", person: Person("jack",10));
    		}
    	));
    }
    
  3. 效果

image-20240302204856669

退出页面返回值


通过Navigator.push加入的页面,左上角都会有一个返回的按钮,但是这个按钮返回值null,所以需要Navigator.pop在退出页面时,返回一个值

  1. 新建一个页面,并添加一个按钮,添加点击事件,使用Navigator.pop返回页面

    class BackAttrPage extends StatelessWidget{
      const BackAttrPage({super.key});
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("BackAttrPage"),
            backgroundColor: Colors.green,
          ),
          body: Center(
            child: ElevatedButton(
              onPressed: (){
                Navigator.pop(context,"hello world");
              },
              child: const Text("返回"),
            ),
          ),
        );
      }
    }
    
  2. 在主页添加按钮进入这个界面,但是这里的方法需要在()后加async表示这个方法为异步方法,并在Navigator.push前加await表示等待这个方法运行完,在进行下一步,然后用一个变量给Navigator.push赋值,为了接收页面的返回值

    onPressed: ()async{
    	var result = await Navigator.push(context, MaterialPageRoute(
    		builder: (context){
    			return const BackAttrPage();
    		}
    	));
    	 print("返回值:$result");
    }
    
  3. 点击默认的返回按钮和点击自己的返回按钮的效果

image-20240302204959441

使用MaterialApp()的routes


MaterialApp()有个routes属性,这个属性是为了方便添加页面,形式为"路由名":(context) => const 页面()

routes: {
	"route1":(context) => const Router1Page(),
	"route2":(context) => const Router2Page()
}

然后我们就可以通过Navigator.pushNamed(context,"路由名")去进入页面

传参问题


因为通过routes命名路由进入页面的方式无法直接传输参数,但是Navigator.pushNamed里的arguments可以将参数传输到页面里

  1. 在按钮处添加事件,并在Navigator.pushNamed里添加arguments属性,将要传的参数写在{}里,以,隔开

    onPressed: (){
    	Navigator.pushNamed(
    		context,
    		"route1",
    		arguments: [
    			"TestText",
    			123
    		]
    	);
    }
    
  2. 在返回页面的方法里,通过ModalRoute.of(context)?.settings.arguments获取传入的数据,如果只传一个参数,那就直接将变量转成String类型即可,但是如果是其他的形式,例如:我传的是一个List,我需要将变量转成字符串,然后再将它转换成List才能拿出来使用

    class Router1Page extends StatelessWidget{
      const Router1Page({super.key});
    
      @override
      Widget build(BuildContext context) {
        var args = ModalRoute.of(context)?.settings.arguments;
        List<String> argList = args.toString().substring(1,args.toString().length-1).split(",");
        return Scaffold(
          appBar: AppBar(
            title: const Text("Page1111"),
            backgroundColor: Colors.cyan,
          ),
          body: Center(
            child: Column(
              children: [
                Text("text:${argList[0]}"),
                Text("num:${argList[1]}"),
              ],
            ),
          ),
        );
      }
    }
    
  3. 效果

image-20240302212751760

返回值[与普通的一样]


返回值的方法与普通进入页面的方法一样,没有问题

效果:

image-20240302213141227

route的钩子函数


顾名思义就是访问routes时,执行的函数

函数 说明
onGenerateRoute 当页面没有Route命名但依旧使用Navigator.pushNamed会执行[需要单独出来讲]
onUnknownRoute 当页面没有Route命名但依旧使用Navigator.pushNamed会执行,防止访问未知页面用
navigatorObservers 监控页面的入栈出栈

onGenerateRoute与onUnknownRoute


onGenerateRoute

这个函数的使用方法是为了:

  • 使用路由命名方式传参的问题
  • 防止没有权限的用户访问页面
  • 访问空页面[不适用]

在使用这个函数的时候,需要一个参数RouteSettings settings,通过settings.name可以获取当前使用Navigator.pushNamed时,访问的路由名

onGenerateRoute: (RouteSettings settings){
      var routeName = settings.name;
      return null;
}

可以通过给访问特定的路由名加个权限,或者传参

//MaterialApp里的onGenerateRoute属性
onGenerateRoute: (RouteSettings settings){
      var routeName = settings.name;
      if(routeName == "noReg"){
        print("如果没有注册在route里,但是依旧要Navigator.pushNamed访问,会显示这一行,route name:$routeName");
        return MaterialPageRoute(builder: (context){
          return const Route2Page(text: "testText",);
        }
        );
      }
      return null;
}
//页面
class Route2Page extends StatelessWidget{
  const Route2Page({super.key,required this.text});
  final String text;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("route2"),
        backgroundColor: Colors.amberAccent,
      ),
      body: Center(
        child: Text("没在routes注册,也能通过Navigator.pushNamed访问,传入值:$text"),
      ),
    );
  }
}

onUnknownRoute

这个函数的执行时机在于当页面没有Route命名但依旧使用Navigator.pushNamedonGenerateRoute没有返回页面

也就是当页面不存在的时候,这个函数会执行,且可以自定义不存在页面

//MaterialApp里的onUnknownRoute属性
onUnknownRoute: (RouteSettings settings){
      print("访问了不存在的页面,");
      return MaterialPageRoute(builder: (context){
        return const NoExitPage();
      });
    }
//不存在页面提示页面
class NoExitPage extends StatelessWidget{
  const NoExitPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("页面不存在"),
        backgroundColor: Colors.amberAccent,
      ),
      body: const Center(
        child: Text("访问了不存在的页面!"),
      ),
    );
  }
}

这个钩子函数,会全程监控页面的入栈和出栈,只要栈堆有变化,就会执行

import 'package:flutter/material.dart';
// demo21 监控路由
void main(){
  runApp(MaterialApp(
    navigatorObservers: [DemoNO()],
    home: const HomePage(),
    routes: {
      "test1": (context) => const Test1Page(),
      "test2": (context) => const Test2Page(),
    },
  ));
}

class DemoNO extends NavigatorObserver{
  @override
  void didPush(Route route, Route? previousRoute) {
    var name = route.settings.name;
    print("$name页面被加入栈");
    super.didPush(route, previousRoute);
  }
  @override
  void didPop(Route route, Route? previousRoute) {
    var name = route.settings.name;
    print("$name页面被移出栈");
    super.didPop(route, previousRoute);
  }
}

image-20240302220954064

demo


Turing158/flutter-demo (github.com)

Turing_ICE/flutter-demo (gitee.com)


待更新 ...

posted @ 2024-09-02 21:38  TuringICE  阅读(26)  评论(0编辑  收藏  举报