Flutter渐变动画,绘制签名

82 阅读4分钟

前言

Android和Flutter分别采用命令式(主动设置)和声明式(被动变化)。

class _TestPageState extends State<MyHomePage> {
  var visible = false;

  void _switchVisible() {
    setState(() {
      visible = !visible;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Visibility(visible: visible, child: const Text("文本")),
        ElevatedButton(
            onPressed: _switchVisible, child: Text(visible ? "隐藏文本" : "显示文本"))
      ],
    );
  }
}

声明式UI只需要配置好状态(数据)和界面(控件)关系,Flutter会根据状态变化自动更新UI。命令式->你让它动;声明式->它自己动。

三棵树

Flutter三棵树渲染机制。

  1. Widget: 对视图的结构化描述,存储视图渲染相关的配置信息:布局、渲染属性、事件响应等信息
  2. Element: Widget实例化对象,承载视图构建的上下文数据,链接Widget到完成最终渲染的桥梁
  3. RenderObject: 负责实现视图渲染对象。

flutter渲染三步:

  1. 通过Widget树生成对应的Element
  2. 创建相应的RenderObject并关联到Element.renderObject属性上。
  3. 构建成RenderObject树,深度优先遍历,确定树中各个对象的位置和尺寸(布局),把他们绘制到不同图层上。Skia在Vsync信号同步时直接从渲染树合成Bitmap,最后交给GPU渲染。

增加Element中间层的好处(提高渲染效率):

将Widget树的变化(diff)做抽象,只将真正需要修改的部分同步到RenderObject树中,最大程度降低对真实渲染视图的修改,提高渲染效率,不是销毁整个渲染视图重建。Element是可复用的,Widget触发重建,Flutter会根据重新前后Widget树的渲染类型及属性变化情况决定后续的复用或新建。如:只是调整了渲染样式,Flutter会通知Element复用现有节点。

  1. 没有状态改变,只是用作展示用 StatelessWidget
  2. 需要保存(绑定)状态,可能出现状态变化,用StatefulWidget

Widget是不变的,变化的是与之绑定的State,State任何更改都会强制Widget的重新构建。

StatefulWdiget发生状态改变流程: setState->build()->didUpdateWidget(处理Widget更新)->dispose(释放资源)->initState(初始化状态)->didChangeDependencies(处理依赖关系的变化)->build(根据新的状态构建Widget)

根据标记来更改不同的Widget:

class _SampleAppPageState extends State<MyHomePage>{
  bool toggle = true;
  void _toggle(){
    setState(() {
      toggle = !toggle;
    });
  }
  // 根据标记返回不同的Widget
  Widget _getToggleChild(){
    if(toggle){
      return const Text("Toggle One");
    }else{
      return ElevatedButton(onPressed: (){}, child: const Text("Toggle Two"));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Sample App"),),
      body: Center(child: _getToggleChild(),),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: "update Text",
        child: const Icon(Icons.update),
      ),
    );
  }
}

Widget淡出动画

class _MyFadeTest extends State<MyHomePage> with TickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    // 初始化,控制动画的执行过程:开始、暂停、停止、反向播放
    controller = AnimationController(
      /// 垂直同步信号,用于同步屏幕刷新和动画更新,避免出现屏幕闪烁、撕裂等问题
      /// this->TickerProviderStateMixin
      /// 这个Ticker除了在垂直同步时发出信号,还在运行时创建一个介于0-1间的线性差值。
        vsync: this,
        // 动画执行时间
        duration: const Duration(milliseconds: 2000));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("动画"),
      ),
      body: Center(
        // 过度动画组件,透明度渐变动画
        child: FadeTransition(
          opacity: controller,// 子空间透明度
          child: const FlutterLogo(size: 100,),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: "Fade",
        onPressed: (){
          controller.forward();// 开始执行动画
        },
        child: const Icon(Icons.brush),
      ),
    );
  }
}
  1. 自定义state类 with TickerProviderStateMixin
  2. 重写 initState(),初始化AnimationController
  3. 调用controller.forwart()执行动画。

自定义Widget

class CustomButton extends StatelessWidget {
  final String label;

  const CustomButton(this.label, {super.key});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(onPressed: () {}, child: Text(label));
  }
}
// 水平方向排列
child: Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text("Row one"),
    // 竖直方向排列
    Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text("Column One"),
        Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            Text('Row one'),
            Text('Row two')
          ],
        )
      ],
    )
  ],
)

ListView

class _ListPagesState extends State<MyHomePage> {
  List<Widget> _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(
        padding: const EdgeInsets.all(10),
        child: Text("Row $i"),
      ));
    }
    return widgets;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("listView"),
      ),
      body: ListView(
        children: _getListData(),
      ),
    );
  }
}

ListView.builder

少量数据用上面的ListView方式创建;当LisetView过多时,使用ListView.builder来构建。可以知道点击了哪个item。

class _ListPagesState extends State<MyHomePage> {
  List<Widget> widgets = [];
  List<Widget> _getListData() {
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $i'),
      ));
    }
    return widgets;
  }

  @override
  void initState() {
    super.initState();
    // 初始化item
    _getListData();
  }

  Widget _getRow(int i) {
      // 对于支持点击事件的Widget可以在外层包GestureDetector来通过onTap实现点击事件。
    return GestureDetector(
      onTap: () {
        setState(() {
          // 点击增加一个item
          widgets.add(_getRow(widgets.length));
          // 打印点击了哪个item
          print("row $i");
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text("Row $i"),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    print("build...");
    return Scaffold(
      appBar: AppBar(
        title: const Text("listView"),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (context, position) {
          return _getRow(position);
        },
      ),
    );
  }
}

绘制:CustomPaint、CutomPainter(支持自定义绘制算法)

绘制签名功能

class SignatureState extends State<MyHomePage> {
  List<Offset?> _points = <Offset>[];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // appBar: AppBar(title: const Text("签名"),),
      body: GestureDetector(
        /// 当用户触摸屏幕并拖动时触发,接收一个details,包含触摸事件的详细信息。
        onPanUpdate: (details) {
          setState(() {
            /// RenderBox对象表示组件在屏幕上几何形状和位置信息
            RenderBox? referenceBox = context.findRenderObject() as RenderBox;

            /// 将全局坐标转换为局部坐标,在SignaturePainter中绘制
            Offset localPosition =
                referenceBox.globalToLocal(details.globalPosition);

            /// 存储用户绘制点
            _points = List.from(_points)..add(localPosition);
          });
        },

        /// 用户松开手指时触发,将null添加到点列表中,表示绘制结束
        onPanEnd: (details) {
          _points.add(null);
        },

        /// 使用CustomPaint组件来绘制签名,传入自定义的Painter
        /// 并设置组件的大小和无线大,占满整个屏幕。
        child: CustomPaint(
          painter: SignaturePainter(_points),
          size: Size.infinite, // 无线大
        ),
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);

  final List<Offset?> points;

  @override
  void paint(Canvas canvas, Size size) {
    // 创建paint对象,设置绘制签名所需要的样式和属性。
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5;
    // 遍历点列表,当前点和下一个点都不会null,绘制从当前到下个点的线段。
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null) {
        canvas.drawLine(points[i]!, points[i + 1]!, paint);
      }
    }
  }

  // 判断是否需要重新绘制签名,新旧点数组比较
  @override
  bool shouldRepaint(SignaturePainter oldDelegate) {
    return oldDelegate.points != points;
  }
}