【Flutter小记】 addPostFrameCallback 使用详解

4,230 阅读3分钟

什么是addPostFrameCallback

addPostFrameCallback方法是Flutter中WidgetsBinding类的一部分,该类从SchedulerBinding继承而来。它允许使用者注册一个回调,这个回调将在当前帧渲染完毕后被调用。这个回调接收一个时间戳参数,表示调用它的时间。

实践部分

接下来我们探讨两个示例,如下所示:

1.创建一个在页面渲染完成后显示的对话框,并确保 setState 不会重新渲染该对话框。

2. 获取创建的小部件的尺寸。

1.第一个示例

9bf2de49-0be2-417b-928e-5ad2ab47c41e.gif

Dialog出现一次后就永远消失了,任凭button呼叫setState

以下是整个代码块:

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

  @override
  State<ExplorePage> createState() => _DialogPageState();
}

class _DialogPageState extends State<ExplorePage> {

  void _showDialog(BuildContext context) {
    showDialog(
        context: context,
        barrierDismissible: true,
        builder: (context) {
          return Container(
            padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 100),
            child: const Card(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  Icon(
                    Icons.emoji_emotions,
                    color: Colors.yellow,
                    size: 50,
                  ),
                  Text("哈哈,现在button永远也找不到我了",
                  style: TextStyle(fontSize: 16),
                    textAlign: TextAlign.center,
                  )
                ],
              ),
            ),
          );
        });
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _showDialog(context);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: InkWell(
        onTap: () {
          setState(() {});
        },
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 30,vertical: 20),
          decoration: const BoxDecoration(
              color: Colors.teal,
              borderRadius: BorderRadius.all(Radius.circular(6))),
          child: const Text(
            "呼叫 Dialog",
            style: TextStyle(fontSize: 16, color: Colors.black),
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }
}

关键就在initState中:

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
    _showDialog(context);
  });
}

我们在这里注册了一个帧渲染完毕时的回调,一旦这个回调被触发,这个部件就会拥有关于自身上下文的全部信息,这进一步包含了部件的布局信息(我们将在第二个例子中看到如何提取这些信息)。这个方法的文档部分描述如下:

Schedule a callback for the end of this frame.(安排一个在本帧结束时的回调 )

Does not request a new frame. (不会请求新的帧)

This callback is run during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven’t been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.(这个回调会在固定帧回调之后的单帧期间运行(这是主要的渲染管道已经被清空的时候)。如果单帧正在进程中并且帧后回调还没被执行,那么这个已注册的回调仍会在该帧执行。否则,已注册的回调将在下一帧执行。 回调会按照它们被添加的顺序执行。 帧后回调无法被注销。它们只会被调用一次

2.获取部件的尺寸

addPostFrameCallback另一个用途是执行计算或测量:如果你需要在帧渲染后计算或测量UI元素的某些属性,如它们的大小或位置,你可以使用addPostFrameCallback来安排代码的执行。

以下是示例代码:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: WidgetMeasurements(),
    );
  }
}

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

  @override
  State<WidgetMeasurements> createState() => _WidgetMeasurementsState();
}

class _WidgetMeasurementsState extends State<WidgetMeasurements> {
  final GlobalKey textKey = GlobalKey();
  Size size = const Size(0, 0);
  Offset offset = const Offset(0, 0);

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

  void getDimensions() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      getSizeAndPosition();
    });
  }

  getSizeAndPosition() {
    RenderBox logoBox = textKey.currentContext!.findRenderObject() as RenderBox;
    size = logoBox.size;
    offset = logoBox.localToGlobal(Offset.zero);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Text(
            key: textKey,
            'Flutter is awesome',
            style: const TextStyle(fontSize: 20),
          ),
          Container(
            height: size.height,
            width: size.width,
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topRight,
                end: Alignment.bottomLeft,
                stops: [0.1, 0.5, 0.7, 0.9],
                colors: [
                  Colors.red,
                  Colors.orange,
                  Colors.yellow,
                  Colors.green
                ],
              ),
            ),
            child: const Text(
              'Flutter is awesome',
              style: TextStyle(fontSize: 20),
            ),
          ),
          Container(
            alignment: Alignment.center,
            color: Colors.black,
            child: Text(
              'Height = ${size.height} && Width = ${size.width} && Position is $offset',
              style: const TextStyle(
                color: Colors.white,
                fontSize: 22,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ],
      ),
    );
  }
}

在上面的代码块中,我们在initState中注册了一个addPostFrameCallbackgetSizeAndPosition()使用RenderBox来获取一个部件的大小和位置。代码行RenderBox logoBox = textKey.currentContext!.findRenderObject() as RenderBox;检索与特定部件关联的RenderBox。它使用findRenderObject()方法来找到相关的RenderBox对象。 一旦获得RenderBox,代码会检索其大小并将其赋值给size变量。它还使用localToGlobal(Offset.zero)获取部件相对于屏幕的全局偏移量。最后,调用setState()方法来更新小部件的状态。

页面前后状态对比:

image.png

image.png

祝编程愉快!