Loading

flutter基础学习.md

Flutter基础

学习来源:《Flutter实战·第二版》

介绍

Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。

技术特点:

  1. 跨平台自绘引擎。底层使用 Skia 作为其 2D 渲染引擎。
  2. 高性能;
  3. dart开发; 开发效率高、 高性能、 快速内存分配、 类型安全和空安全。

Flutter框架图:

1-1.82c25693

安装

# 使用archlinuxcn源安装
yay -S flutter  

# 追加flutter组
sudo gpasswd -a {user} flutterusers
# 重载配置
source /etc/profile
# 更换组
newgrp flutterusers 

# 其他
# 配置仓库 (加入环境变量)
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
# 检查
flutter doctor

运行

# 1. 创建新项目
flutter create hello

# 2. 运行
flutter run

Dart语言

变量声明

var关键字

// var被赋值之后,类型将确定,不可再更改类型
var t = 'hello'; // t的类型为String
t = 100; // 错误

dynamicObject

// Object是所有对象的根基类。
// 所以任何数据类型都可以赋值给Object对象
dynamic t;
Object c;
t = 'as'; c = 'world';
t = 100; c = 200;  // 正确

// 两者不同之处在于 dynamic可以使用所有可能的方法。而 Object只能使用Object的方法
dynamic a = '';
Object b = '';
// 正常
print(a.length);
// 报错 The getter 'length' is not defined for the class 'Object'
print(b.length);

finalconst

// final和const都是常量,不可变
// 区别在于 const是编译期初始化。而final为运行是初始化

空安全

int i = 9; // 默认不为空,定义时必须初始化
int j?; // 定义可为空类型,可以不初始化

// 预期变量之后初始化,但在定义时无法确定值,可以使用late (编译器提示,要求使用前必须初始化)
late int k;
k = 9;

// 可以显式指定已经初始化完成,使用`!`
int i?;
if(i!=null) {
  print(i! * 8); //因为已经判过空,所以能走到这 i 必不为null,如果没有显式申明,则 IDE 会报错
}
// 如果函数变量可空时,调用的时候可以用语法糖
fun?.call();

函数

// 函数声明
bool isNobel(int a) {
    return a > 0;
}

// 不指定返回类型,将当作 dynamic处理
isNobel(int a) {
    return a > 0;
}
typedef bool CALLBACK();
void test(CALLBACK b) {
    print(b());
}
test(isNobel); // 错误:类型不一致

// 简写
bool isNobel(int a) => true;

// 函数作为变量
var say = (str) {
    print(str);
};
say("asda");

// 函数作为参数
void exe(CALLBACK cb) {
    cb();
}

// 可选的位置参数
String say(String f, String msg, [String? device]) {
    var result = '$f says $msg';
    if (device != null) {
        result = '$result with a $device';
    }
    return result;
}

// 可选的命名参数
void enable({bool display, bool hidden}) {
    // ...
}
enable(bold: true, hidden: true);

mixin组合

dart不支持多继承。使用mixin进行组合。类似interface。\

如果多个mixin 中有同名方法,with 时,会默认使用最后面的 mixin 的,mixin 方法中可以通过 super 关键字调用之前 mixin 或类中的方法。

class Person {
  say() {
    print('say');
  }
}

mixin Eat {
  eat() {
    print('eat');
  }
}

mixin Walk {
  walk() {
    print('walk');
  }
}

mixin Code {
  code() {
    print('key');
  }
}

class Dog with Eat, Walk{}
class Man extends Person with Eat, Walk, Code{}

异步支持

Future与JavaScript中的Promise非常相似。

// Future.then
Future.delayed(Duration(seconds: 2), (){
    return 'hi';
}).then((data) {
    print(data)
});

// Future.catchError
Future.delayed(Duration(seconds: 2),(){
   throw AssertionError("Error");  
}).then((data){
   print("success");
}).catchError((e){
   print(e);
});

// then函数有个onError错误参数来捕获异常
Future.delayed(Duration(seconds: 2), () {
	//return "hi world!";
	throw AssertionError("Error");
}).then((data) {
	print("success");
}, onError: (e) {
	print(e);
});

// whenComplete 无论成功失败,都做这个事情
Future.delayed(Duration(seconds: 2),(){
   //return "hi world!";
   throw AssertionError("Error");
}).then((data){
   //执行成功会走到这里 
   print(data);
}).catchError((e){
   //执行失败会走到这里   
   print(e);
}).whenComplete((){
   //无论成功或失败都会走到这里
});

// Future.wait 等待事件完成之后,再运行之后代码
Future.wait([
  // 2秒后返回结果  
  Future.delayed(Duration(seconds: 2), () {
    return "hello";
  }),
  // 4秒后返回结果  
  Future.delayed(Duration(seconds: 4), () {
    return " world";
  })
]).then((results){
  print(results[0]+results[1]);
}).catchError((e){
  print(e);
});

// async 和 await 只是个Future的语法糖

Stream

Stream也可以进行接收异步事件,但它可以同时接收多个异步操作的结果。

Stream.fromFutures([
  // 1秒后返回结果
  Future.delayed(Duration(seconds: 1), () {
    return "hello 1";
  }),
  // 抛出一个异常
  Future.delayed(Duration(seconds: 2),(){
    throw AssertionError("Error");
  }),
  // 3秒后返回结果
  Future.delayed(Duration(seconds: 3), () {
    return "hello 3";
  })
]).listen((data){
   print(data);
}, onError: (e){
   print(e.message);
},onDone: (){
});

/*
结果为:
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
*/

基本介绍

Widget

flutter中几乎所有的对象都是一个 widget。用来表示组件功能

widget是描述一个UI元素的配置信息。

StatelessWidgetStatefulWidget继承了Widget

flutter 框架的的处理流程是这样的:

  1. 根据widget树生成Element树。Element树的节点都继承Element类;
  2. 根据Element树生成Render树,渲染树的节点都继承RenderObject类;
  3. 根据Render树生成Layer树,然后显式,Layer树的节点都继承Layer类。

StatelessWidget

StatelessWidget用于不需要维护状态的场景。在build方法中嵌套其他widget来构建UI

StatefulWidget

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  @override
  StatefulElement createElement() => StatefulElement(this);
    
  @protected
  State createState();
}

在StatefulWidget 中,State 对象和StatefulElement具有一一对应的关系。当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例。

State

一个 StatefulWidget 类会对应一个 State 类。State信息可以:

  1. widget被构建时同步读取;
  2. 调用setState通知重新构建widgte树。

State有两个常用属性:

  1. widget:表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置State. widget 为新的 widget 实例。
  2. context:StatefulWidget对应的 BuildContext,作用同StatelessWidget 的BuildContext。

2-5.a59bef97

状态管理

管理状态的最常见的方法:

  • Widget 管理自己的状态。

  • Widget 管理子 Widget 状态。

  • 混合管理(父 Widget 和子 Widget 都管理状态)。

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。

  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。

  • 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。

自身管理

import 'package:flutter/material.dart';

void main() {
  runApp(const TapboxA());
}

class TapboxA extends StatefulWidget {
  const TapboxA({super.key});

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

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
    debugPrint("active: $_active");
  }

  @override
  Widget build(BuildContext context) {
    final tb = TextButton(
      onPressed: _handleTap,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
            color: _active ? Colors.lightBlue[700] : Colors.grey[600]),
        child: Center(
          child: Text(
            _active ? "active" : "inactive",
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
            textDirection: TextDirection.ltr,
          ),
        ),
      ),
    );
    return Directionality(textDirection: TextDirection.ltr, child: tb);
  }
}

父Widget管理子Widget

import 'package:flutter/material.dart';

void main() {
  runApp(const ParentWidget());
}

class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

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

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTap(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
        textDirection: TextDirection.ltr,
        child: TapBoxB(active: _active, onChanged: _handleTap));
  }
}

class TapBoxB extends StatelessWidget {
  final bool active;
  final ValueChanged<bool> onChanged;

  const TapBoxB({Key? key, this.active = false, required this.onChanged})
      : super(key: key);

  void _handleTap() {
    onChanged(!active);
  }

  @override
  Widget build(BuildContext context) {
    return TextButton(
        onPressed: _handleTap,
        child: Container(
          width: 200.0,
          height: 200.0,
          decoration: BoxDecoration(
              color: active ? Colors.lightBlue[700] : Colors.grey[600]),
          child: Center(
            child: Text(
              active ? "Active" : "Inactive",
              style: const TextStyle(fontSize: 32.0, color: Colors.white),
            ),
          ),
        ));
  }
}

路由管理

路由在移动开发中通常指页面。

简单路由:

// 路由
class NewRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("New Route"),
        ),
        body: const Center(
          child: Text("新页面"),
        ));
  }
}

// 导航代码
TextButton(
    onPressed: () {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return NewRoute();
      }));
    },
    child: const Text("跳转新路由")
)

MaterialPageRoute

MaterialPageRoute继承PageRoute类。PageRoute定义路由构建及切换时过度动画相关接口及属性。

  MaterialPageRoute({
    WidgetBuilder builder,  // 回调函数。构建路由页面的具体内容,返回值是一个widget
    RouteSettings settings, // 包含路由的配置信息,如路由名称、是否初始路由
    bool maintainState = true, // 表示原来的路由是否被保存。如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false
    bool fullscreenDialog = false, // 表示新的路由页面是否是一个全屏的模态对话框
  })

Navigator是一个路由管理的组件。

  1. Future push(BuildContext context, Route route) :将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。
  2. bool pop(BuildContext context, [ result ]): 将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据。

路由传值

// 新页面
class TipRoute extends StatelessWidget {
  final String text;

  const TipRoute({super.key, required this.text});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("提示"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(18),
        child: Center(
          child: Column(
            children: <Widget>[
              Text(text),
              ElevatedButton(
                onPressed: () => Navigator.pop(context, "返回值是我"),  // 弹出时,在这里返回参数
                child: const Text("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        onPressed: () async { 
          var result = await Navigator.push(context,  // 异步等待返回信息
              MaterialPageRoute(builder: (context) {
            return const TipRoute(text: "传入123"); // 构造参数
          })); 
          debugPrint("$result");
        },
        child: const Text("打开新页面"),
      ),
    );
  }
}

命名路由

使用路由表进行命名路由管理。

// 1. 路由表
Map<String, WidgeBuilder> routes;

// 2. 注册路由表
MaterialApp (
	// .....
    routes: {
        "new_paget" : (context) => NewRoute(),
     	"/" : (context) => MyHome() // 注册路由首页
    }
);

// 3. 打开路由
Navigator.pushNamed(context, "new_page");

// 4. 获取路由参数
var args = ModalRoute.of(context).settings.arguments;

路由钩子

MaterialApp(
	// ..........
    onGenerateRoute: (RouteSettings settings) { // 仅对命名路由生效
        return MaterialPageRoute(builder: (context) {
            String routeName = settings.name;
            // 判读
            return xxx();
        });  
    }
)

包管理

flutter的项目默认配置为: pubspec.yaml

可以在https://pub.dev查找依赖。

name: study1  # 项目名称
description: A new Flutter project. # 项目描述
publish_to: 'none' # 阻止使用 flutter pub publish

version: 1.0.0+1  # 版本

environment:
  sdk: '>=3.0.3 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:  # flutter相关配置
  uses-material-design: true

资源管理

Flutter APP 安装包中会包含代码和 assets(资源)两部分。Assets 是会打包到程序安装包中的,可在运行时访问。

使用pubspec.yaml文件来管理应用程序所需的资源。

flutter:
	assets:
		- assets/icon1.png # flutter会加载相邻子目录的同命文件。如果有资源 assets/a/icon1.png,它也会被加载
		- assets/icon2.png

加载assets

  1. 加载文本资源

    • 使用rootBundle访问。位于package:flutter/services.dart
    • 使用DefaultAssetBundle:用于运行时动态替换不同的AssetBundle
  2. 加载图片资源

    • 声明分辨率相关的图片

      • .../2.0x/icon.png .../3.0x/icon.png
    • 加载图片

      AssetImage("assets/background.png");
      
      Image.asset("assets/background.png"); // 返回widget
      
      // 使用默认asset bundle,内部会自动处理分辨率
      

调试

  1. debugger()

    debugger(when a > 20)
    
  2. debug debugPrint flutter logs

  3. assert

异常捕获

dart为单线程,出现异常后不会导致进程的退出。

图2-21

dart有两个队列:

  • 微任务队列: 优先级别高。主要来源于dart内部。
  • 事件队列:所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等。,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡。可以在事件队列中插入新的微任务和事件任务。
  1. flutter框架异常捕获:

    flutter框架对build添加了异常捕获。所以build出错后会弹出错误提示:

    @override
    void performRebuild() {
     ...
      try {
        //执行build方法  
        built = build();
      } catch (e, stack) {
        // 有异常时则弹出错误提示  
        built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
      } 
      ...
    }   
    
    static void reportError(FlutterErrorDetails details) {
      ...
      if (onError != null)
        onError(details); //调用了onError回调
    }
    
    // 自己上报异常,只需要提供一个自定义的错误处理回调即可
    FlutterError.onError = (FlutterErrorDetails details) {
    	reportError(details);
    };
    
    1. 其他异常捕获与日志收集

    2. 同步异常:使用trycatch

    3. 异步异常:使用runZone()handleUncaughtError

      runZoned(
        () => runApp(MyApp()),
        zoneSpecification: ZoneSpecification(
          // 拦截print 
          print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
            parent.print(zone, "Interceptor: $line");
          },
          // 拦截未处理的异步错误
          handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
                                Object error, StackTrace stackTrace) {
            parent.print(zone, '${error.toString()} $stackTrace');
          },
        ),
      );
      

布局类组件

布局原理和约束

尺寸限制类用于限制容器大小。比如ConstrainedBoxSizedBoxUnconstrainedBoxAspectRatio等。

布局模型

  • 基于RenderBox的盒模型布局
  • 基于SliverRenderSliver)按需加载列表布局

两者细节略有不同,但大体一致。布局流程如下:

  1. 上层向下层组件传递约束条件(BoxConstraints
  2. 下层确定自己的大小,然后通知上层。下层必须符合上层的约束
  3. 上层组件确定下层组件相对于自身的偏移和确定自身的大小

ConstrainedBox

用于对子组件添加约束。如果想让子组件最小高度为80,则使用const ConstraintsBox(minHeight: 80.0)

@override
Widget build(BuildContext context) {
Widget redBox = DecoratedBox(decoration: BoxDecoration(color: Colors.red));
return Column(
  children: [
    ConstrainedBox(
      constraints: BoxConstraints(
        minWidth: 50.0,
        minHeight: 50.0
      ),
      child: Container(
        height: 5.0,    // 即使指定高度为5,由于约束,高度实际为50
        child: redBox,
      ),
    )
  ],
);

当多重限制时,取父组件数值较大的值。

SizedBox

用于给子元素指定固定的宽高。

SizedBox(
  width: 20.0,
  height: 20.0,
  child: redBox,
)
// SizedBox由BoxConstraints实现

UnconstrainedBox

UnconstrainedBox可以”去除“约束。

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0),
  child: UnconstrainedBox( // '去除'限制
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
      child: redBox,
    ),
  ),
)

实际上,子组件不可能违反父组件的约束(如果违反父组件的约束,则无法绘制)。

线性布局

沿水平或垂直排列子组件的布局。使用RowColumn来实现线性布局(两者均继承Flex)。

  • 主轴: 布局沿水平方向 主轴对齐 MainAxisAlignment
  • 纵轴:布局沿垂直方向 纵轴对齐 CrossAxisAlignment

Row的定义:

Row({
 // ...
    TextDirection textDirection, // 文字方向。默认为从左往右
    MainAxisSize mainAxisSzie = MainAxisSize.max, // Row在主轴方向占用的空间。默认是max,即尽可能多,不管子组件宽度,Row的宽度始终等于水平方向的最大宽度。min表示尽可能少的占用空间,此时Row的宽度等于所有子组件的宽度
    MainAxisAligment mainAxisAligment = MainAxisAligment.start //表示子组件在Row内的水平空间对齐方式。只有size为max时才有意义。start表示沿textDirection的初始方向对齐。center表示居中对齐
    VerticalDirection verticalDirection = VerticalDirection.down // 表示Row纵轴的对齐方向,默认为从上到下
     CrossAxisAligment crossAxisAligment = CrossAxisAligment.center, // 表示子组件在纵轴的对齐方式。Row的高度为子组件最高的高度。当verticalDirection为down时,start表示顶部对齐。当verticalDirection为up时,start表示底部对齐
    List<Widget> children = const <Widget>[],
})

Column的定义同Row

Row里面嵌套Row,或者Column里面嵌套Column时,只有外面的RowColumn会占用尽可能大的空间。里面的为实际大小。可以使用Expanded组件抵销此影响

Flex 弹性布局

Flex组件可以沿着水平或垂直方向排列子组件。RowColumn都继承Flex,参数基本相同。

// Flex定义
Flex({
    // ........
    required this.direction, // 默认为水平方向
    children: const <Widget>[]
})

Expanded只能作为Flex的孩子。可以按照比例扩展子组件占用的空间。

// Expanded定义
Expanded({
    int flex = 1, child
})

流式布局

Wrap用于折行

// wrap定义
Wrap({
    // ...
    direction = Axis.horizontal,
    alignment = start,
    spacing = 0.0,   // 主轴方向子Widget的间距
    runAlignment = start,   // 纵轴方向的对齐方式
    runSpacing = 0.0, // 纵轴方向的间距
    crossAxisAlignment = start,
    textDirection,
    certicalDirection = down,
    children       
})

Flow

层叠布局

层叠布局与Web中的绝对定位相似。子组件可以根据父容器的四个角位置来确定自身位置。

// Stack定义
Stack({
    alignment = topStart, // 决定没有定位 或部分定位的子组件
    textDirection,
    fit = losse,  // 没有定位的子组件任何适应Stack的大小。loose表示使用子组件的大小,expand表示扩展到Stack的大小
    clipBehavior = hardEdge,  // 决定超出stack的如何裁剪。hardEdge表示直接裁剪,不使用抗抗锯齿
    children
});
    
// Positioned
Positioned({
    left,         // 表示距离stack左、上、右、底四边的距离
    top,
    right,
    bottom,
    width,       // left right width 只能使用2个,其余一个会计算出来
    height,     // 用于配合top bottom的
    child           
})

对齐和相对定位Align

// Align定义
Align({
    alignment: center,  // 表示子组件在父组件的启始位置
    widthFactor,   // 用于确定Align组件本身宽高的属性:乘以子组件的宽高,就是Align组件的宽高
    heightFactor,
    child
})

容器类组件

容器类组件和布局类Widget都作用于其子Widget,不同的是:

  1. 布局类子组件为children,容器类子组件为child
  2. 布局类组件对其子组件进行排列,容器类组件对其子组件进行包装。

Padding填充

// Padding
Padding({
    //...
    padding,  // 定义填充的方法
    child
})

DecoratedBox 装饰容器

// DecoratedBox
DecoratedBox({
    decoration, // 代表要绘制的装饰
    position = DecorationPosition.background,  // 决定在哪里装饰,默认为背景。foreground为前景
    child
})
// BoxDecoration定义
BoxDecoration({
    color,  // 颜色
    image,  // 图片
    border, // 边框
    borderRadius, // 圆角
    boxShadow, // 阴影
    gradient, // 渐变
    backgroundBlendMode,   // 背景混合模式
    shape = BoxShape.rectangle  // 形状
});

// 示例
Widget build(BuildContext context) {
 return DecoratedBox(
  decoration: BoxDecoration(
    gradient: LinearGradient(colors: [Colors.red, Colors.orange.shade200]), // 背景渐变
    boxShadow: [
      BoxShadow(
        color: Colors.black87,
        offset: Offset(2.0, 2.0),
        blurRadius: 4.0
      )
    ]
  ),
  child: Padding(
    padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
    child: Text("Login", style: TextStyle(color: Colors.white)),
  ),
 );
}

Tranform变换

Transform可以实现子组件绘制时的特效。Matrix4时一个4D矩阵。

变换

// 变换
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black,
      child: Transform(
        alignment: Alignment.topLeft, // 相对于原点的对齐方式
        transform: Matrix4.skewY(0.3), // 沿Y轴倾斜0.3的弧度
        child: Container(
          padding: const EdgeInsets.all(8.0),
          color: Colors.deepPurple,
          child: Text("dsaas"),
        ),
      ),
    );
  }

平移

// 平移
  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: Colors.red,
      ),
      child: Transform.translate(
        offset: Offset(-20.0, -5.0),
        child: Text('hello'),
      ),
    );
  }

旋转

// 旋转
Widget build(BuildContext context) {
return DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red,
  ),
  child: Transform.rotate(
    angle: pi / 2 ,
    child: Text('hello'),
  ),
);

缩放

// 缩放
Widget build(BuildContext context) {
return DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red,
  ),
  child: Transform.scale(
    scale: 2.0,
    child: Text('hello'),
  ),
);
}

Container容器

Container组件是DecpratedBoxConstrainedBoxTransformPaddingAlign等组件的一个多功能容器。

// container定义
Container({
    this.alignment,
    this.padding,
    color,
    decoration,
    foregroundDecoration,
    width,
    height,
    constraints,
    margin,
    transform,
    child
})

Clip裁剪

裁剪Widget 默认行为
ClipOval 子组件为正方形时裁剪为内贴圆形;为矩形时裁剪为内贴椭圆
ClipRRect 将子组件裁剪为圆角矩形
ClipRect 默认裁剪掉子组件布局外的绘制内容
ClipPath 按照自定义路径裁剪
  Widget build(BuildContext context) {
    Widget avatar = Image.network('xxx', width: 60);
    return Column(
      children: [
        avatar, // 不裁剪
        ClipOval(child: avatar,), // 裁剪为圆形
        ClipRRect(
          borderRadius: BorderRadius.circular(5.0),
          child: avatar,
        ), // 裁剪为圆角矩形
      ],
    );
  }

空间适配 FittedBox

遇到子组件大小超过父组件时,父组件会将自身最大空间作为约束传递给子组件。当子组件原始大小超过父组件的约束区域,则需要进行一些缩放、裁剪或其他处理。不同组件的处理方式是特定的。Text组件默认为换行。如果我们想改变此行为,需要使用FittedBox。本质为子组件如何适配父组件的空间。

// FittedBox定义
FittedBox({
    fit = BoxFit.contain, // 适配方式
    alignment = Alignment.center, // 对齐方式
    clipBehavior = Clip.none, // 是否裁剪
    child
})

页面骨架 Scaffold

可滚动组件

简介

Flutter有两种布局模型:

  • 基于RenderBox的盒模型布局
  • 基于Sliver按需加载列表布局

Sliver模型是按需加载模型,只有在viewport中才会加载。ListViewGridView都是。

  1. Scrollable:用于处理滑动手势、确定滑动偏移
  2. ViewPort:显示的视窗
  3. Sliver:视窗里显示的元素
// Scrollable
Scrollable({
    // ...
    axisDirection = AxisDirection.down, // 滚动方向
    controller,         // 控制滚动位置及监听滚动事件
    physics,            // 定义如何响应用户的操作
    viewportBuilder     // 构建Viewport的回调
});

// Viewport
Viewport({
    axisDirection - AxisDirection.down,
    crossAxisDirection,
    anchor = 0.0,
    offset, // 用户的滚动偏移
    center,
    cacheExtent, // 预渲染区域
    cacheExtentStyle = CacheExtentStyle.pixel, // 描述cacheExtent的含义
    clipBehavior = Clip.hardEdge,
    slivers
});

SingleChildScrollView

只能接收一个子组件。它没有延迟加载模型,所以只应当在期望的内容不会超出屏幕太多时使用。

SingleChildScrollView({
    scrollDirection = Axis.vertical, //滚动方向,默认是垂直
    reverse = false,
    padding,
    primary,
    physics,
    controllrt,
    child
})
  Widget build(BuildContext context) {
    String s = "ahsdksadhksahdkashdskahdakhdak";
    return Scrollbar(    // 显示进度条
      child: SingleChildScrollView(
        padding: EdgeInsets.all(16.0),
        child: Center(
          child: Column(
            children: s.split("").map((e)=> Text(e, textScaleFactor: 2.0,)).toList(),
          ),
        ),
      ),
    );
  }

ListView

ListView可以沿一个方向线性排列所有子组件,也支持懒加载。

// ListView定义
ListView({
    // ...
    scrollDirection = Axis.vertical, // 滚动方向
    reverse = false,
    controller,
    bool primary,
    physics,
    padding,
    itemExtent,  // 参数不为null时,强制children的‘长度’为此值。指定它会有更好的性能,因为不需要构建 子组件时再去计算
    prototypeItem, // 如果知道列表每一项长度都相同,但不知道具体长度,可以指定它。滚动组件会在layout时,计算它的长度。所以和itemExtent性能一样好,但两者互斥。
    shrinkWrap = false, // 是否根据使所有子组件长度设置ListView的长度。默认会尽可能占用更多的空间。当滚动方向无边界时,必须设置为true。
    addAutomaticKeepAlives = true,
    addRepaintBoundaries = true, // 是否包含在repaint组件中,此组件重绘开销特别小。
    cacheExtent,  // 预渲染区域长度
    children  // 适合少量的子组件数量。否则,应该使用ListView.builder
})
Widget build(BuildContext context) {
List<Text> texts = "ahsdksadhksahdkashdskahdasssssssssssssssssssssssssssssssssssssskhdak".split("")
  .map((e) => Text(e)).toList();
return Scrollbar(child:ListView(
  shrinkWrap: true,
  padding: const EdgeInsets.all(10.0),
  children: texts,
));
}
// 默认为懒加载,但使用children仍然要提前创建Widget

ListViewBuilder

// ListView.builder
ListView.builder({
    // ...
    IndexedWidgetBuilder itemBuilder, // 列表的构建项,返回具体index项时的子组件
    int itemCount,  // 列表项的数量
    // ....
})
Widget build(BuildContext context) {
return Scrollbar(
  child: ListView.builder(
    itemCount: 50,
    itemExtent: 50,
    itemBuilder: (BuildContext context, int index) {
      return ListTile(title: Text("$index"));
    },
  ),
);

ListView.separated可以在列表项之间添加一个分割组件

Widget build(BuildContext context) {
Widget divider1 = Divider(color: Colors.blue);
Widget divider2 = Divider(color: Colors.red);
return Scrollbar(
  child: ListView.separated(
    itemCount: 100,
    itemBuilder: (context, index) {
      return ListTile(title: Text("$index"));
    },
    separatorBuilder: (context, index) {
      return index%2 == 0? divider1 : divider2;
    },
  ),
);

滚动监听及控制

可以使用ScrollController来控制可滚动组件的滚动位置。

ScrollController

// ScrollController
ScrollController({
    initialScrollOffest = 0.0,// 初始滚动位置
    keepScrollOffset = true
});

offset // 可滚动组件的当前位置
jumpTo()      // 无动画
animateTo()   // 动画滚动
  _MyScrollState createState() => _MyScrollState();
}

class _MyScrollState extends State<MyScroll> {

  ScrollController _controller = ScrollController();
  bool showBtn = false;

  @override
  void initState() {
    super.initState();
    // 监听滚动事件,打印滚动位置
    _controller.addListener(() {
      print(_controller.offset);
      if (_controller.offset < 1000 && showBtn) {
        setState(() {
                  showBtn = false;
        });
      } else if (_controller.offset >= 1000 && showBtn == false) {
        setState(() {
          showBtn = true;
        });
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("滚动控制"),),
      body: Scrollbar(
        child: ListView.builder(
          itemCount: 150,
          itemExtent: 50.0,
          controller: _controller,
          itemBuilder: (cobtext, index) {
              return ListTile(title: Text("现在是$index"),);
          },
        ),
      ),
      floatingActionButton: !showBtn ? null : FloatingActionButton(
        child: Icon(Icons.arrow_upward),
        onPressed: () {
          _controller.animateTo(
            .0,
            duration: Duration(seconds: 1),
            curve: Curves.ease,
          );
        },
      ),
    ) ;
  }
}

PageStorage是一个保存页面相关数据 的组件,并不会参与UI。每次滚动结束,可滚动组件都将滚动位置存储至PostStorage中。

bitsdojo_window

功能:自定义窗口大小及位置。

使用:flutter pub add bitsdojo_window

// main.dart
import 'package:flutter/material.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';

void main() {
  // 处理原生与flutter通信
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());

  doWhenWindowReady(() {
    const initialSize = Size(800, 600);
    appWindow.minSize = initialSize; // 最小窗口
    appWindow.size = initialSize;   // 默认窗口
    appWindow.maxSize = const Size(800, 800);  // 最大窗口
    appWindow.alignment = Alignment.topLeft;  // 打开位置
    appWindow.show();
  });
}

自定义窗口导航栏:

https://pub.dev/packages/bitsdojo_window#for-linux-apps

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

class WindowButtons extends StatefulWidget {
  const WindowButtons({super.key});

  @override
  State<WindowButtons> createState() => _WindowButtonsState();
}

class _WindowButtonsState extends State<WindowButtons> {
  final buttonColors = WindowButtonColors(
    iconNormal: Colors.grey[600],
    mouseOver: Colors.grey[400],
    mouseDown: Colors.grey[400],
    iconMouseOver: Colors.grey[600],
    iconMouseDown: Colors.grey[600]
      );
  void maximizeOrRestore() {
    appWindow.maximizeOrRestore();
  }
  
  @override
  Widget build(BuildContext context) {
    return Row(children: [
        MouseRegion(
          cursor: SystemMouseCursors.click,
          child: MinimizeWindowButton(colors: buttonColors),
        ),
        MouseRegion(
          cursor: SystemMouseCursors.click,
          child: appWindow.isMaximized 
            ? RestoreWindowButton(
              colors: buttonColors,
              onPressed: maximizeOrRestore,
            ) 
            : MaximizeWindowButton(
              colors: buttonColors,
              onPressed: maximizeOrRestore,
              ),
        ),
        MouseRegion(
          cursor: SystemMouseCursors.click,
          child: CloseWindowButton(colors: buttonColors),
          )
    ],);
  }
}
posted @ 2022-06-22 23:02  nsfoxer  阅读(67)  评论(0编辑  收藏  举报