Flutter中获取widget坐标和大小

926 阅读2分钟

获取widget坐标和大小

可以通过给widget指定GlobalKey 对象,之后再通过GlobalKey 对象获取所关联widget的坐标和大小的信息。
如下代码。这里需要在WidgetsBinding.instance.addPostFrameCallback的回调获取renderBox 的原因是,只有widget 绘制完成后,才能确定其最终的位置和大小,即此时获取到的RenderBox 不为空。
也可以选择在widget点击事件后获取,因为此时widget 必定绘制完成。

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

  @override
  State<_GetCoordinateExample> createState() => _GetCoordinateExampleState();
}

class _GetCoordinateExampleState extends State<_GetCoordinateExample> {
  final GlobalKey titleKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {
        final titleRenderBox =
            titleKey.currentContext!.findRenderObject() as RenderBox;
        final globalCoordinates = titleRenderBox.localToGlobal(Offset.zero);
        debugPrint(
            'titleRenderBox 全局坐标: $globalCoordinates');
        debugPrint( 'titleRenderBox 大小: ${titleRenderBox.size}');            
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      'Title',
      key: titleKey,
      style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
    );
  }
}

获取坐标的常见问题

获取坐标常会出现获取的坐标不正确,且每次获取坐标的数值都不一样的问题。
出现上述问题,很可能是widget的位置并不是恒定不变的。

常见情况如:

  • widget进行了动画。
  • widget所在的页面进行了路由动画。

该widget进行了动画

那可以根据你是想要获取widget动画执行前的坐标还是执行坐标后,决定获取坐标的方式。

获取widget动画执行前的坐标

可以在widget渲染完成后获取坐标,之后再执行动画。可以参考如下代码。

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

  @override
  State<_GetCoordinateBeforeAnimatedExample> createState() =>
      _GetCoordinateBeforeAnimatedExampleState();
}

class _GetCoordinateBeforeAnimatedExampleState
    extends State<_GetCoordinateBeforeAnimatedExample> {
  final GlobalKey titleKey = GlobalKey();

  late bool isEndOfFrame = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {
        isEndOfFrame = true;
        final titleRenderBox =
            titleKey.currentContext!.findRenderObject() as RenderBox;
        debugPrint(
            'titleRenderBox coordinate: ${titleRenderBox.localToGlobal(Offset.zero)}');
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.sizeOf(context);
    return SizedBox(
      width: size.width,
      height: size.height,
      child: Column(
        children: [_buildAnimatedTitleAsNeed()],
      ),
    );
  }

  Widget _buildAnimatedTitleAsNeed() {
    final Widget child = Text(
      'Title',
      key: titleKey,
      style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
    );

    return isEndOfFrame
        ? AnimatedContainer(
            duration: const Duration(milliseconds: 300),
            child: Container(
              margin: const EdgeInsets.only(top: 30),
              child: child,
            ),
          )
        : child;
  }
}

获取widget动画执行后的坐标

获取widget动画执行后的坐标也是跟之前的方式类似。

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

  @override
  State<_GetCoordinateAfterAnimatedExample> createState() =>
      _GetCoordinateAfterAnimatedExampleState();
}

class _GetCoordinateAfterAnimatedExampleState
    extends State<_GetCoordinateAfterAnimatedExample>
    with SingleTickerProviderStateMixin {
  final GlobalKey titleKey = GlobalKey();

  late final AnimationController animationController =
      AnimationController(vsync: this);

  late final Animation<Offset> _offsetAnimation = Tween<Offset>(
    begin: Offset.zero,
    end: const Offset(1.5, 0.0),
  ).animate(CurvedAnimation(
    parent: animationController,
    curve: Curves.elasticIn,
  ));

  @override
  void initState() {
    super.initState();
    animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        final titleRenderBox =
            titleKey.currentContext!.findRenderObject() as RenderBox;
        debugPrint(
            'titleRenderBox coordinate: ${titleRenderBox.localToGlobal(Offset.zero)}');
      }
    });
  }
  
  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.sizeOf(context);
    return SizedBox(
      width: size.width,
      height: size.height,
      child: Column(
        children: [_buildAnimatedTitleAsNeed()],
      ),
    );
  }

  Widget _buildAnimatedTitleAsNeed() {
    return SlideTransition(
      position: _offsetAnimation,
      child: Container(
        margin: const EdgeInsets.only(top: 30),
        child: Text(
          'Title',
          key: titleKey,
          style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
        ),
      ),
    );
  }
}

该widget所在的页面进行了路由动画

@override
Widget build(BuildContext context) {
  final route = ModalRoute.of(context);
  route?.animation?.addStatusListener((status) {
    if (status == AnimationStatus.completed) {
      final titleRenderBox =
      titleKey.currentContext!.findRenderObject() as RenderBox;
      debugPrint(
          'titleRenderBox coordinate: ${titleRenderBox.localToGlobal(
              Offset.zero)}');
    }
  });
  return _buildBody();
}