Flutter控件偏移管理插件flutter_widget_offset的使用

Flutter控件偏移管理插件flutter_widget_offset的使用

一个类库,用于获取和观察 widget 到根布局边缘的偏移量。此类库也适用于对话框(showDialog)。

支持

  • onChanged: 支持监控 widgets 相对于根布局边缘的偏移量。
  • onKeyboard: 支持监控软键盘状态变化(显示和隐藏)。

使用 OffsetDetector

使用 OffsetDetector 直接获取 widget 到根布局边缘的偏移量。

final OffsetDetectorController? _controller = OffsetDetectorController();

OffsetDetector(
  controller: _controller,
  onChanged: (Size size, EdgeInsets offset, EdgeInsets rootPadding) {
       print("The widget size: ${size.width}, ${size.height}");
       print(
           "The offset to edge of root(ltrb): ${offset.left}, ${offset.top}, ${offset.right}, ${offset.bottom}");
       print(
           "the root padding: ${rootPadding.left}, ${rootPadding.top}, ${rootPadding.right}, ${rootPadding.bottom}");
   },
  child: TextField(
    style: textFieldStyle,
    onChanged: (value) => _controller.notifyStateChanged(),
  ));

使用 OffsetChangeObserver

使用 OffsetChangeObserver 来观察 widget 到根布局边缘的偏移量。

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

  [@override](/user/override)
  State<StatefulWidget> createState() {
    return _OffsetDetectorState();
  }
}

class _OffsetDetectorState extends State<OffsetDetector> with WidgetsBindingObserver {
  late OffsetChangeObserver _observer;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return TextField();
  }

  void onChanged(Size size, EdgeInsets offset, EdgeInsets rootPadding) {
       print("The widget size: ${size.width}, ${size.height}");
       print(
           "The offset to edge of root(ltrb): ${offset.left}, ${offset.top}, ${offset.right}, ${offset.bottom}");
       print(
           "the root padding: ${rootPadding.left}, ${rootPadding.top}, ${rootPadding.right}, ${rootPadding.bottom}");
   }

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addObserver(this);
    _observer = OffsetChangeObserver(context: context, onChanged: this.onChanged);
    _observer.onInitState();
  }

  [@override](/user/override)
  void didChangeMetrics() {
    super.didChangeMetrics();
    _observer.onChangeMetrics();
  }

  [@override](/user/override)
  void didChangeDependencies() {
    super.didChangeDependencies();
    _observer.onChangeDependencies();
  }

  [@override](/user/override)
  void dispose() {
    _observer.onDispose();
    WidgetsBinding.instance!.removeObserver(this);
    super.dispose();
  }
}

示例

查看 这里 的完整示例代码。

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_widget_offset/flutter_widget_offset.dart';

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

/// flutter_widget_offset 示例应用
class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'OffsetDetector 示例',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'OffsetDetector 示例'),
    );
  }
}

/// flutter_widget_offset 示例应用主页
class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  /// 主页标题
  final String title;

  [@override](/user/override)
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final OffsetDetectorController _controller = OffsetDetectorController();
  final FocusNode _focusNode = FocusNode();
  final LayerLink _layerLink = LayerLink();
  late OverlayEntry? _overlayEntry;

  double _overlayEntryWidth = 100.0;
  double _overlayEntryHeight = 100.0;
  double _overlayEntryY = double.minPositive;
  AxisDirection _overlayEntryDir = AxisDirection.down;

  bool _isOpened = false;
  bool _isAboveCursor = false;

  final EdgeInsets _textFieldContentPadding = EdgeInsets.fromLTRB(3, 8, 3, 8);
  late TextStyle _textFieldStyle;

  [@override](/user/override)
  void initState() {
    super.initState();

    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
      DeviceOrientation.landscapeLeft,
      DeviceOrientation.landscapeRight,
    ]);

    WidgetsBinding.instance!.addPostFrameCallback((duration) {
      if (mounted) {
        _overlayEntry = OverlayEntry(
          builder: (context) {
            final suggestionsBox = Material(
              elevation: 2.0,
              color: Colors.yellow[200],
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  maxHeight: _overlayEntryHeight,
                ),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          "弹出窗口",
                          style: TextStyle(fontSize: 32),
                        ),
                        Switch(
                          value: _isAboveCursor,
                          onChanged: _onOverlayPositionChanged,
                        ),
                      ],
                    )
                  ],
                ),
              ),
            );
            return Positioned(
                width: _overlayEntryWidth,
                child: CompositedTransformFollower(
                  link: _layerLink,
                  showWhenUnlinked: false,
                  followerAnchor: _overlayEntryDir == AxisDirection.down
                      ? Alignment.topLeft
                      : Alignment.bottomLeft,
                  targetAnchor: Alignment.bottomLeft,
                  offset: Offset(0.0, _overlayEntryY),
                  child: suggestionsBox,
                ));
          },
        );
      }
    });
  }

  void _openSuggestionBox() {
    if (this._isOpened) return;
    assert(this._overlayEntry != null);
    Overlay.of(context)!.insert(this._overlayEntry!);
    this._isOpened = true;
  }

  void _closeSuggestionBox() {
    if (!this._isOpened) return;
    assert(this._overlayEntry != null);
    this._overlayEntry!.remove();
    this._isOpened = false;
  }

  void _updateSuggestionBox() {
    if (!this._isOpened) return;
    assert(this._overlayEntry != null);
    this._overlayEntry!.markNeedsBuild();
  }

  void _onKeyboardState(bool state) {
    if (state) {
      _focusNode.requestFocus();
    } else {
      _focusNode.unfocus();
    }
  }

  void _onOffsetChanged(Size size, EdgeInsets offset, EdgeInsets rootPadding) {
    _overlayEntryWidth = size.width;

    if (120 < offset.bottom || offset.top < offset.bottom) {
      _overlayEntryDir = AxisDirection.down;
      _overlayEntryHeight = offset.bottom - 10;
      _overlayEntryY = 5;
    } else {
      _overlayEntryDir = AxisDirection.up;
      if (_isAboveCursor) {
        _overlayEntryHeight = offset.top + size.height - _courseHeight - 5;
        _overlayEntryY = -_courseHeight;
      } else {
        _overlayEntryHeight = offset.top - 5;
        _overlayEntryY = -size.height;
      }
    }

    _updateSuggestionBox();
  }

  double get _courseHeight =>
      (_textFieldStyle.fontSize ?? 16) +
      _textFieldContentPadding.bottom +
      _textFieldContentPadding.top;

  void _onOverlayPositionChanged(bool value) {
    setState(() {
      this._isAboveCursor = value;
    });
    _onOffsetChanged(
        _controller.size, _controller.offset, _controller.rootPadding);
  }

  void _onTextChanged(String value) {
    if (value.isEmpty) {
      _closeSuggestionBox();
    } else {
      if (_isOpened) {
        _controller.notifyStateChanged();
      } else {
        _openSuggestionBox();
      }
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    _textFieldStyle = Theme.of(context).textTheme.headline5!;
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: [
          Switch(
            value: _isAboveCursor,
            onChanged: _onOverlayPositionChanged,
          ),
        ],
      ),
      body: Center(
        child: Padding(
          padding: EdgeInsets.all(3),
          child: CompositedTransformTarget(
            link: _layerLink,
            child: Container(
              child: OffsetDetector(
                  controller: _controller,
                  onChanged: _onOffsetChanged,
                  onKeyboard: _onKeyboardState,
                  child: TextField(
                    focusNode: _focusNode,
                    minLines: 1,
                    maxLines: 5,
                    onChanged: _onTextChanged,
                    style: _textFieldStyle,
                    textInputAction: TextInputAction.none,
                    decoration: InputDecoration(
                      contentPadding: _textFieldContentPadding,
                      hintText: "写点什么",
                    ),
                  )),
            ),
          ),
        ),
      ),
    );
  }

  [@override](/user/override)
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

更多关于Flutter控件偏移管理插件flutter_widget_offset的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter控件偏移管理插件flutter_widget_offset的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何使用 flutter_widget_offset 插件来管理 Flutter 控件偏移的示例代码。这个插件允许你动态地调整控件的位置,非常适合创建拖拽、动画效果或者任何需要精确控制控件位置的场景。

首先,确保你已经在 pubspec.yaml 文件中添加了 flutter_widget_offset 依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_widget_offset: ^最新版本号  # 替换为最新的版本号

然后运行 flutter pub get 来获取依赖。

以下是一个简单的示例,展示了如何使用 flutter_widget_offset 来移动一个按钮:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Widget Offset Example'),
        ),
        body: OffsetWidgetExample(),
      ),
    );
  }
}

class OffsetWidgetExample extends StatefulWidget {
  @override
  _OffsetWidgetExampleState createState() => _OffsetWidgetExampleState();
}

class _OffsetWidgetExampleState extends State<OffsetWidgetExample> {
  final GlobalKey _buttonKey = GlobalKey();
  Offset _offset = Offset.zero;

  void _handleDragUpdate(DragUpdateDetails details) {
    setState(() {
      _offset += details.delta;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Center(
          child: GestureDetector(
            onPanUpdate: _handleDragUpdate,
            child: OffsetWidget(
              key: _buttonKey,
              offset: _offset,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.blue,
                child: Center(child: Text('Drag Me')),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

在这个示例中,我们做了以下几件事:

  1. 添加依赖:在 pubspec.yaml 中添加了 flutter_widget_offset 依赖。
  2. 创建主应用:在 MyApp 中定义了一个简单的 Material 应用,包含一个 Scaffold 和一个 AppBar
  3. 定义状态类:在 OffsetWidgetExample 中创建了一个有状态的小部件,用于管理按钮的偏移。
  4. 使用 GlobalKey:为按钮定义了一个 GlobalKey,这样我们可以确保 OffsetWidget 有一个唯一的标识符。
  5. 处理拖拽事件:使用 GestureDetector 监听拖拽更新事件,并在 _handleDragUpdate 方法中更新按钮的偏移量。
  6. 使用 OffsetWidget:将按钮包裹在 OffsetWidget 中,并传入当前的偏移量 _offset

这个示例展示了如何使用 flutter_widget_offset 插件来动态调整控件的位置。你可以根据需要进一步扩展这个示例,比如添加更多的控件、处理更多的手势事件或者结合动画效果等。

回到顶部