对两个单独的小部件之间的定位容器进行动画处理

时间:2020-10-31 18:18:43

标签: flutter flutter-layout flutter-animation

我构建了一个解决方案,该解决方案可以解决我认为简单的问题,但是花了很多时间才能使它起作用。当我的解决方案有效时,我想知道我是否过度设计了一些应该简单得多的东西。

我正在尝试为两个不同的文本小部件之间的下划线动画制作动画。在示例解决方案中,我包括两个文本小部件“ Sign In”和“ Sign Up”。我的应用程序已本地化为多种语言,因此这些单词可能会因地区而异。我的应用程序还需要响应(窗口/方向更改)。我尝试了我能想到的Stack,Positioned,Row,Column的所有变体,但无法正常工作。

我的最终解决方案是将覆盖动画容器窗口小部件与GetX状态管理结合使用。为了使我的解决方案在下面起作用,您需要将get: ^3.15.0添加到pubspec.yaml文件中。此解决方案不适用于setState((){})

我想知道是否有一种更简单/更好的解决方案来对PositionedContainer进行动画处理,以使其与其他两个小部件的位置和大小对齐。请记住需要支持本地化和响应能力。无论哪种方式,也许这都可以作为示例叠加层的示例,对他人有用。

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart';

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

class OverlayApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Overlay Test',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: OverlayHome(),
    );
  }
}

class OverlayHome extends StatelessWidget {
  var _isSignIn = true.obs;
  final _signinKey = GlobalKey();
  final _signupKey = GlobalKey();

  OverlayEntry _indicatorOverlayEntry;

  void _setIndicator() {
    _indicatorOverlayEntry?.remove();
    SchedulerBinding.instance.addPostFrameCallback((_) {
      _indicatorOverlayEntry = _overlayEntry();
      Overlay.of(_signinKey.currentContext).insert(_indicatorOverlayEntry);
    });
  }

  OverlayEntry _overlayEntry() {
    RenderBox signinRenderBox = _signinKey.currentContext.findRenderObject();
    RenderBox signupRenderBox = _signupKey.currentContext.findRenderObject();
    final signinWidgetPosition = signinRenderBox.localToGlobal(Offset.zero);
    final signupWidgetPosition = signupRenderBox.localToGlobal(Offset.zero);
    final signinWidgetSize = signinRenderBox.size;
    final signupWidgetSize = signupRenderBox.size;

    return OverlayEntry(
      builder: (context) => Stack(
        children: [
          Obx(
            () => AnimatedPositioned(
              duration: const Duration(milliseconds: 500),
              curve: Curves.easeOutQuint,
              top: signinWidgetPosition.dy + signinWidgetSize.height,
              height: 3.0,
              left: (_isSignIn.value)
                  ? signinWidgetPosition.dx
                  : signupWidgetPosition.dx,
              width: (_isSignIn.value)
                  ? signinWidgetSize.width
                  : signupWidgetSize.width,
              child: Container(color: Theme.of(context).primaryColor),
            ),
          )
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: LayoutBuilder(
          builder: (context, constraints) {
            _setIndicator();
            return Column(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                SizedBox(height: 20),
                Text(
                  'Animated Overlay Test',
                  style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 30),
                Obx(
                  () => Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      FlatButton(
                        child: Text(
                          "Sign In",
                          key: _signinKey,
                          style: (_isSignIn.value)
                              ? TextStyle(
                                  color: Theme.of(context).primaryColor,
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold)
                              : TextStyle(
                                  color: Colors.black,
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold),
                        ),
                        onPressed: () {
                          _isSignIn.value = true;
                        },
                      ),
                      FlatButton(
                        child: Text(
                          "Sign Up",
                          key: _signupKey,
                          style: (!_isSignIn.value)
                              ? TextStyle(
                                  color: Theme.of(context).primaryColor,
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold)
                              : TextStyle(
                                  color: Colors.black,
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold),
                        ),
                        onPressed: () {
                          _isSignIn.value = false;
                        },
                      ),
                    ],
                  ),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

1 个答案:

答案 0 :(得分:-2)

您可以使用类似这样的内容:

child: Column(
  children: [
    TwinRow(
      twins: [
        Text('short'),
        Text('looooooooooooooooooooong'),
      ],
    ),
    TwinRow(
      twins: [
        Text('a bit longer'),
        Text('even looooooooooooooooooooonger'),
      ],
      tabColor: Colors.red,
      tabThickness: 1.5,
    ),
    TwinRow(
      twins: [
        Text('Sign In'),
        Text('Sign Up'),
      ],
      onPressed: (i) => print('pressed $i'),
      tabColor: Colors.orange,
      tabThickness: 4.5,
    ),
  ],
),

主要思想是使用由CustomPainter驱动的AnimationController

class TwinRow extends StatefulWidget {
  final List<Widget> twins;
  final Function(int) onPressed;
  final Color tabColor;
  final double tabThickness;
  const TwinRow({
    Key key,
    @required this.twins,
    this.onPressed,
    this.tabColor,
    this.tabThickness = 3.0,
  }) : assert(twins.length == 2), super(key: key);

  @override
  _TwinRowState createState() => _TwinRowState();
}

class _TwinRowState extends State<TwinRow> with SingleTickerProviderStateMixin {
  GlobalKey _rowKey = GlobalKey();
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: kTabScrollDuration,
      vsync: this,
    );
  }

  @override
  Widget build(BuildContext context) {
    var tabColor = widget.tabColor ?? Theme.of(context).indicatorColor;
    return CustomPaint(
      painter: _TabPainter(_controller, _rowKey, tabColor, widget.tabThickness),
      child: Padding(
        padding: EdgeInsets.only(bottom: widget.tabThickness),
        child: Row(
          key: _rowKey,
          children: [
            FlatButton(onPressed: () => _handlePress(0.0, 0), child: widget.twins[0]),
            FlatButton(onPressed: () => _handlePress(1.0, 1), child: widget.twins[1]),
          ],
        ),
      ),
    );
  }

  _handlePress(double target, int index) {
    _controller.animateTo(target, curve: Curves.ease);
    widget.onPressed?.call(index);
  }
}

class _TabPainter extends CustomPainter {
  final Animation<double> animation;
  final GlobalKey rowKey;
  final Color tabColor;
  final double tabThickness;

  _TabPainter(this.animation, this.rowKey, this.tabColor, this.tabThickness) : super(repaint: animation);

  @override
  void paint(Canvas canvas, Size size) {
    var rects = (rowKey.currentContext.findRenderObject() as RenderFlex)
      .getChildrenAsList()
      .map((e) => (e.parentData as BoxParentData).offset & e.size);
    // print(rects);
    var rect = Rect.lerp(rects.elementAt(0), rects.elementAt(1), animation.value);

    canvas.drawRect(rect.bottomLeft & Size(rect.width, tabThickness), Paint()..color = tabColor);
    /// elevated version with some shadow:
    // var path = Path()..addRect(rect.bottomLeft & Size(rect.width, tabThickness));
    // canvas.drawShadow(path, Colors.black, 2, true);
    // canvas.drawPath(path, Paint()..color = tabColor);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}