【问题标题】:Flutter pixel coordinates to offset颤振像素坐标以偏移
【发布时间】:2022-06-15 03:55:18
【问题描述】:

所以我的 Flutter 应用程序中有一个第三方库,可以向我发送消息。 例如,消息可以用以下 json 格式表示

{
  objects: [
    {
      'name': 'Name of obj1',
      'x1': 107,
      'y1': 1012,
      'x2': 117,
      'y2': 974
    },
    ...
  ]
}

xy 属性是屏幕坐标(以像素表示)。 现在假设我想根据消息中的数据绘制一个从(x1, y1)(x2, y2) 动画的文本(具有消息中对象的名称属性)。

我查看了AnimatedPositioned,它把我推荐给SlideTransition

如果要保持大小不变​​,只有位置随时间变化,那么请考虑使用 SlideTransition。 SlideTransition 只会触发动画的每一帧重新绘制,而 AnimatedPositioned 也会触发重新布局。

现在SlideTransition 的颤振文档中提供的示例适用于从Offset.zeroOffset(1.5, 0.0) 的偏移量。但是这些单位究竟是用什么表示的呢?以及如何将我的屏幕坐标提供给 Flutter Offset 单元?

我特意提供了一些背景信息,以说明我无法更改这些消息的格式。我只需将屏幕坐标转换为相对于应用程序根目录的偏移量,而不是应用程序的根目录当然是家长。

【问题讨论】:

  • 如果此值以像素为单位,您如何管理小型设备或平板电脑? X 轴 500 像素将超出小型设备的屏幕,甚至不会在平板电脑中显示一半。您收到的列表中是否还有参考像素值?
  • @KaushikChandru 发送给我的像素是正确的。第三方库嵌入在 Flutter 应用程序中,可以访问屏幕尺寸/尺寸。 Flutter 也有这个使用 window.physicalSize
  • 还有什么不能与AnimatedPositioned一起使用?
  • @pskink 文档明确告诉我考虑SlideTransition。我还在我的应用程序中需要完全控制我的小部件。 AnimatedPositioned 导致了奇怪的行为,尤其是当动画仍然很忙时消息进来时。在这种情况下,我需要从当前位置设置动画到最新位置,而不是之前设置的位置。
  • SlideTransition 很难做你想做的事,从我看到你想将你的标签放在任何地方(我的意思是他们可以留在屏幕的任何地方:appbar,主要内容区域,底部导航栏) - 这是真的吗?

标签: flutter flutter-layout flutter-animation


【解决方案1】:
double leftPosition = data[0]['x1'];
double topPosition = data[0]['y1'];

调用一个future.delayed设置left和top位置到下一个值

    Future.delayed(duration: Duration (seconds:1), (){
  leftPosition = data[0]['x2'];
  topPosition = data[0]['y2'];
 setState((){});

});

然后

 Stack(
  children:[
  AnimatedPositioned(
  duration : Duration(seconds : 1),
  left : leftPosition,
  top: topPosition,
 )
 ]
),

这应该以像素为单位。如有错误请见谅。用我的手机写的。

【讨论】:

    【解决方案2】:

    如果您“需要完全控制我的小部件”,我会推荐 CustomMultiChildLayout,它不仅可以让您在布局阶段了解您的子小部件大小(因此您可以在一些 Offset 内对齐它们)而且还支持“动画" 布局 重建子级 - 在“正常”Stack 小部件中,您需要用 LayoutBuilder(以获得整个布局大小)和 AnimatedBuilder(用于运动动画)和你还需要在每个动画帧上重建你的孩子

    这里有两个版本:第一个使用Overlay,子定位逻辑非常简单,第二个使用RenderBox,子定位更复杂

    使用Overlay的版本:

    class AnimatedLabels extends StatefulWidget {
      @override
      State<AnimatedLabels> createState() => _AnimatedLabelsState();
    }
    
    class _AnimatedLabelsState extends State<AnimatedLabels> with TickerProviderStateMixin {
      late AnimationController controller;
      late OverlayEntry entry;
    
      @override
      void initState() {
        super.initState();
        controller = AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 600),
        );
        entry = OverlayEntry(
          builder: (ctx) {
            final colors = [
              Colors.orange, Colors.green, Colors.lime,
            ];
            final data = [
              {'name': 'orange label', 'x1': 150.0, 'y1': 400.0, 'x2': 50.0, 'y2': 100.0},
              {'name': 'green label',  'x1': 10.0,  'y1': 56.0,  'x2': 65.0, 'y2': 125.0},
              {'name': 'lime label',   'x1': 200.0, 'y1': 56.0,  'x2': 80.0, 'y2': 150.0},
            ];
            return CustomMultiChildLayout(
              delegate: LabelDelegate(controller, data),
              children: List.generate(3, (i) => LayoutId(
                id: i,
                child: Card(
                  margin: EdgeInsets.zero,
                  elevation: 3,
                  color: colors[i],
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text(data[i]['name'] as String),
                  ),
                ),
              )),
            );
          },
        );
        SchedulerBinding.instance.addPostFrameCallback(insertOverlay);
      }
    
      insertOverlay(Duration d) {
        print('insertOverlay $entry');
        Overlay.of(context)!.insert(entry);
      }
    
      @override
      Widget build(BuildContext context) {
        print('build');
        return Center(
          child: ElevatedButton(
            child: const Text('press me to start animation'),
            onPressed: () => controller.value < 0.5? controller.forward() : controller.reverse()
          ),
        );
      }
    }
    
    class LabelDelegate extends MultiChildLayoutDelegate {
      final AnimationController controller;
      final List<Map> offsets;
    
      LabelDelegate(this.controller, List<Map> data) :
        offsets = [for (final d in data) {
          'from': Offset(d['x1'] as double, d['y1'] as double),
          'to': Offset(d['x2'] as double, d['y2'] as double),
        }],
        super(relayout: controller);
    
      @override
      void performLayout(ui.Size size) {
        final looseBoxConstraints = BoxConstraints.loose(size);
        // final curve = controller.status == AnimationStatus.forward? Curves.elasticOut : Curves.elasticIn;
        final curve = controller.status == AnimationStatus.forward? Curves.easeOutBack : Curves.easeInBack;
        final t = curve.transform(controller.value);
    
        int id = 0;
        for (final offsetMap in offsets) {
          // print('$id: $offsetMap');
          final size = layoutChild(id, looseBoxConstraints);
    
          // the most important line of this code:
          positionChild(id, Offset.lerp(offsetMap['from'], offsetMap['to'], t)!);
          id++;
        }
      }
    
      @override
      bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
    }
    

    使用RenderBox的版本:

    class AnimatedLabels2 extends StatefulWidget {
      @override
      State<AnimatedLabels2> createState() => _AnimatedLabels2State();
    }
    
    class _AnimatedLabels2State extends State<AnimatedLabels2> with TickerProviderStateMixin {
      late AnimationController controller;
      final key = GlobalKey();
      final globalOffset = ValueNotifier<Offset?>(null);
    
      final colors = [
        Colors.orange, Colors.green, Colors.lime,
      ];
      final data = [
        {'name': 'orange label', 'x1': 150.0, 'y1': 400.0, 'x2': 50.0, 'y2': 100.0},
        {'name': 'green label',  'x1': 10.0,  'y1': 56.0,  'x2': 65.0, 'y2': 125.0},
        {'name': 'lime label',   'x1': 200.0, 'y1': 56.0,  'x2': 80.0, 'y2': 150.0},
      ];
    
      @override
      void initState() {
        super.initState();
        controller = AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 600),
        );
        SchedulerBinding.instance.addPostFrameCallback(setGlobalOffset);
      }
    
      setGlobalOffset(Duration d) {
        final renderBox = key.currentContext!.findRenderObject() as RenderBox;
        globalOffset.value = renderBox.localToGlobal(Offset.zero);
        print('globalOffset: ${globalOffset.value}');
      }
    
      @override
      Widget build(BuildContext context) {
        print('build');
        return Stack(
          key: key,
          children: [
            Center(
              child: ElevatedButton(
                child: const Text('press me to start animation'),
                onPressed: () => controller.value < 0.5? controller.forward() : controller.reverse()
              ),
            ),
            CustomMultiChildLayout(
              delegate: LabelDelegate2(controller, data, globalOffset),
              children: List.generate(3, (i) => LayoutId(
                id: i,
                child: Card(
                  margin: EdgeInsets.zero,
                  elevation: 3,
                  color: colors[i],
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text(data[i]['name'] as String),
                  ),
                ),
              )),
            ),
          ],
        );
      }
    }
    
    class LabelDelegate2 extends MultiChildLayoutDelegate {
      final AnimationController controller;
      final List<Map> offsets;
      final ValueNotifier<Offset?> globalOffset;
    
      LabelDelegate2(this.controller, List<Map> data, this.globalOffset) :
        offsets = [for (final d in data) {
          'from': Offset(d['x1'] as double, d['y1'] as double),
          'to': Offset(d['x2'] as double, d['y2'] as double),
        }],
        super(relayout: Listenable.merge([controller, globalOffset]));
    
      @override
      void performLayout(ui.Size size) {
        // print(globalOffset);
    
        final looseBoxConstraints = BoxConstraints.loose(size);
        // final curve = controller.status == AnimationStatus.forward? Curves.elasticOut : Curves.elasticIn;
        final curve = controller.status == AnimationStatus.forward? Curves.easeOutBack : Curves.easeInBack;
        final t = curve.transform(controller.value);
    
        int id = 0;
        for (final offsetMap in offsets) {
          // print('$id: $offsetMap');
          final size = layoutChild(id, looseBoxConstraints);
    
          // the most important line of this code:
          positionChild(id, Offset.lerp(offsetMap['from'], offsetMap['to'], t)! - (globalOffset.value ?? Offset.zero));
          id++;
        }
      }
    
      @override
      bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
    }
    

    EDIT添加了RenderBox解决方案的修改版本,展示了如何实现在动画仍在运行时发生位置变化的情况:注意不仅整个动画是连续的,而且动画对象的行为就好像它是一个具有一定质量的真实对象(因此它不会立即改变最终方向) - 只需使用 SpringSimulation

    class AnimatedLabels3 extends StatefulWidget {
      @override
      State<AnimatedLabels3> createState() => _AnimatedLabels3State();
    }
    
    class _AnimatedLabels3State extends State<AnimatedLabels3> with TickerProviderStateMixin {
      late AnimationController controllerX, controllerY;
      late LabelDelegate3 delegate;
      final key = GlobalKey();
      final globalOffset = ValueNotifier<Offset?>(null);
    
      @override
      void initState() {
        super.initState();
        controllerX = AnimationController.unbounded(
          vsync: this,
          duration: const Duration(milliseconds: 1000),
        );
        controllerY = AnimationController.unbounded(
          vsync: this,
          duration: const Duration(milliseconds: 1000),
        );
        delegate = LabelDelegate3(controllerX, controllerY, globalOffset);
        SchedulerBinding.instance.addPostFrameCallback(setGlobalOffset);
      }
    
      setGlobalOffset(Duration d) {
        final renderBox = key.currentContext!.findRenderObject() as RenderBox;
        globalOffset.value = renderBox.localToGlobal(Offset.zero);
        print('globalOffset: ${globalOffset.value}');
      }
    
      @override
      Widget build(BuildContext context) {
        print('build');
        return Stack(
          children: [
            const Center(child: Text('tap anywhere to start animation\n\nalso try to tup again while animation is still in progress')),
            GestureDetector(
              onPanDown: (d) {
                delegate.simulateNewTargetPosition(d.globalPosition);
              },
            ),
            CustomSingleChildLayout(
              key: key,
              delegate: delegate,
              child: SizedBox.square(
                dimension: 64,
                child: Container(
                  width: 64,
                  height: 64,
                  decoration: const ShapeDecoration(
                    color: Colors.deepOrange,
                    shape: CircleBorder(),
                    shadows: [BoxShadow(blurRadius: 4, spreadRadius: 1, offset: Offset(3, 3))],
                  ),
                ),
              ),
            ),
          ],
        );
      }
    }
    
    final springDescription = SpringDescription.withDampingRatio(mass: 8, stiffness: 100);
    
    class LabelDelegate3 extends SingleChildLayoutDelegate {
      final AnimationController controllerX;
      final AnimationController controllerY;
      final ValueNotifier<Offset?> globalOffset;
      Offset current = Offset.zero;
      Simulation
        sx = SpringSimulation(springDescription, 0, 0, 0),
        sy = SpringSimulation(springDescription, 0, 0, 0);
    
      LabelDelegate3(this.controllerX, this.controllerY, this.globalOffset) :
        super(relayout: Listenable.merge([controllerX, controllerX, globalOffset]));
    
      @override
      Offset getPositionForChild(Size size, Size childSize) {
        current = Offset(controllerX.value, controllerY.value);
        // the most important line of this code:
        return current - (globalOffset.value ?? Offset.zero) - childSize.center(Offset.zero);
      }
    
      void simulateNewTargetPosition(ui.Offset position) {
        // timeDilation = 5;
        sx = SpringSimulation(springDescription, current.dx, position.dx, 2 * controllerX.velocity);
        sy = SpringSimulation(springDescription, current.dy, position.dy, 2 * controllerY.velocity);
        controllerX.animateWith(sx);
        controllerY.animateWith(sy);
      }
    
      @override
      bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) => true;
    }
    

    【讨论】:

      猜你喜欢
      • 2014-01-11
      • 2011-12-20
      • 2021-08-07
      • 2018-06-23
      • 1970-01-01
      • 2022-01-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多