You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter如何检测设备旋转(方向变化) 适配可拖动悬浮按钮位置

Flutter可拖动悬浮按钮旋转越界问题解决方案

不需要单独获取旋转前的屏幕方向,直接监听父容器尺寸变化(屏幕旋转、分屏、折叠屏状态切换都会触发该变化),每次尺寸更新后重新计算按钮可移动边界,同时将当前按钮偏移量钳位到新的合法坐标区间即可。该方案比单独监听方向事件覆盖更多适配场景,也不需要为横屏单独实现独立组件。

具体修改点

  • 移除组件中冗余的deviceWidth、deviceHeight入参,所有尺寸直接通过父容器的RenderBox实时获取,避免外部传入的静态尺寸和实际渲染尺寸不一致
  • 新增尺寸变化校验逻辑:每次帧渲染完成后检查父容器尺寸是否变化,若变化则重新计算可移动边界
  • 边界更新完成后自动修正按钮坐标,确保按钮始终在屏幕可视范围内,不会出现越界无法点击的问题

修改后完整代码

import 'package:flutter/material.dart';

class DraggableFloatingActionButton extends StatefulWidget {
  final Widget child;
  final Function onPressed;
  final GlobalKey parentKey;

  DraggableFloatingActionButton({
    required this.child,
    required this.onPressed,
    required this.parentKey,
  });

  @override
  State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
}

class _DraggableFloatingActionButtonState extends State<DraggableFloatingActionButton> {
  final GlobalKey _key = GlobalKey();

  bool _isDragging = false;
  late Offset _offset;
  late Offset _minOffset;
  late Offset _maxOffset;
  // 缓存上一次父容器尺寸,用于判断是否需要更新边界
  Size _lastParentSize = Size.zero;

  @override
  void initState() {
    super.initState();
    // 首帧渲染完成后初始化边界和初始位置
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _updateBoundary();
      // 初始位置保持原逻辑,放在屏幕右下区域
      final initSize = _lastParentSize;
      _offset = Offset(initSize.width * 0.8, initSize.height * 0.75);
      // 钳位初始位置避免越界
      _clampOffset();
      if (mounted) setState(() {});
    });
    // 注册帧回调,持续监听尺寸变化
    WidgetsBinding.instance.addPersistentFrameCallback((_) {
      _updateBoundary();
    });
  }

  // 更新按钮可移动边界
  void _updateBoundary() {
    final RenderBox? parentRenderBox = widget.parentKey.currentContext?.findRenderObject() as RenderBox?;
    final RenderBox? renderBox = _key.currentContext?.findRenderObject() as RenderBox?;

    if (parentRenderBox == null || renderBox == null) return;

    final Size parentSize = parentRenderBox.size;
    // 父容器尺寸无变化时直接返回,不做多余更新
    if (parentSize == _lastParentSize) return;
    _lastParentSize = parentSize;

    final Size btnSize = renderBox.size;
    _minOffset = const Offset(0, 0);
    _maxOffset = Offset(
      parentSize.width - btnSize.width,
      parentSize.height - btnSize.height
    );
    // 边界更新后自动修正按钮位置,防止越界
    _clampOffset();
    if (mounted) setState(() {});
  }

  // 将偏移量钳位到合法坐标区间
  void _clampOffset() {
    double newX = _offset.dx.clamp(_minOffset.dx, _maxOffset.dx);
    double newY = _offset.dy.clamp(_minOffset.dy, _maxOffset.dy);
    _offset = Offset(newX, newY);
  }

  void _updatePosition(PointerMoveEvent pointerMoveEvent) {
    double newOffsetX = _offset.dx + pointerMoveEvent.delta.dx;
    double newOffsetY = _offset.dy + pointerMoveEvent.delta.dy;

    newOffsetX = newOffsetX.clamp(_minOffset.dx, _maxOffset.dx);
    newOffsetY = newOffsetY.clamp(_minOffset.dy, _maxOffset.dy);

    setState(() {
      _offset = Offset(newOffsetX, newOffsetY);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: _offset.dx,
      top: _offset.dy,
      child: Listener(
        onPointerMove: (PointerMoveEvent pointerMoveEvent) {
          _updatePosition(pointerMoveEvent);
          setState(() {
            _isDragging = true;
          });
        },
        onPointerUp: (PointerUpEvent pointerUpEvent) {
          if (_isDragging) {
            setState(() {
              _isDragging = false;
            });
          } else {
            widget.onPressed();
          }
        },
        child: Container(
          key: _key,
          child: widget.child,
        ),
      ),
    );
  }
}

使用说明

  • 调用组件时无需再传入deviceWidth、deviceHeight参数,仅需传入child、onPressed、parentKey三个必传参数即可
  • 实现自动适配竖屏/横屏切换、分屏模式、折叠屏展开/折叠等所有导致父容器尺寸变化的场景,按钮位置会在布局变化后自动修正到可视区域内,不会出现越界无法点击的问题
  • 原有拖动、点击交互逻辑完全保留,无使用层面的改动

内容的提问来源于stack exchange,提问作者Captain Mockery

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 07:39:22