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

使用GetX实现Flutter物理模拟动画:同步AnimationController与dragAlignment

Flutter GetX 重写物理模拟示例:复位动画失效问题解决

问题描述

我是Flutter新手,了解到GetX可简化开发流程,尝试用GetX重写Flutter官方物理模拟示例。目前已实现拖拽动画,但通过_aniC.animateWith(simulation)实现的物理模拟复位动画无法正常运行。推测问题在于AnimationController执行animateWith(simulation)后,未将动画值同步至_dragAlignment.value,添加addListener()方法也未能解决问题。

问题代码

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

//pageview
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Physics'),
        actions: const [
          Row(children: [Icon(Icons.share)])
        ],
        centerTitle: true,
        elevation: 0,
      ),
      body: Center(
        child: DraggableCard(child: const FlutterLogo(size: 128)),
      ),
    );
  }
}

// widget's view
class DraggableCard extends GetView {
  DraggableCard({required this.child, super.key});
  final Widget child;
  final logic = _DraggableCardLogic();

  @override
  Widget build(BuildContext context) {
    final size = Get.size;
    return GetX(
      init: _DraggableCardLogic(),
      initState: (_) {},
      builder: (_) {
        return GestureDetector(
          onPanDown: (details) {
            logic._aniC.stop();
          },
          onPanUpdate: (details) {
            logic._dragAlignment.value += Alignment(
              details.delta.dx / (size.width / 2),
              details.delta.dy / (size.height / 2),
            );
          },
          onPanEnd: (details) {
            logic._runAni(details.velocity.pixelsPerSecond, size);
          },
          child: Align(
            alignment: logic._dragAlignment.value,
            child: Card(child: child),
          ),
        );
      },
    );
  }
}

//logic&state
class _DraggableCardLogic extends GetxController
    with GetSingleTickerProviderStateMixin {
  late AnimationController _aniC = AnimationController(vsync: this);
  final _dragAlignment = Alignment.center.obs;
  late final _aniBack = _aniC
      .drive(AlignmentTween(
        begin: _dragAlignment.value,
        end: Alignment.center,
      ))
      .obs;

  void _runAni(Offset pixelsPerSec, Size size) {
    _aniBack.value = _aniC.drive(AlignmentTween(
      begin: _dragAlignment.value,
      end: Alignment.center,
    ));
    print('DRAG:${_aniBack.value}');

    final unitsPerSecX = pixelsPerSec.dx / size.width;
    final unitsPerSecY = pixelsPerSec.dy / size.height;
    final unitsPerSec = Offset(unitsPerSecX, unitsPerSecY);
    final unitVelocity = unitsPerSec.distance;

    const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1);

    final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
    _aniC.animateWith(simulation);
  }

  @override
  void onInit() {
    super.onInit();
    _aniC = AnimationController(vsync: this);
    _aniC.addListener(() {
      _dragAlignment.value = _aniBack.value.value;
    });
    print('init!');
  }

  @override
  void onClose() {
    _aniC.dispose();
    super.onClose();
  }
}

效果对比

  • 期望效果:拖拽卡片松手后,卡片通过弹簧物理效果平滑复位至屏幕中心
  • 当前效果:拖拽松手后无复位动画,卡片停留在松手位置

问题根源与修复方案

1. 重复初始化Controller(核心问题)

在DraggableCard中,同时创建了logic = _DraggableCardLogic(),又在GetX的init参数中初始化新实例,导致视图监听的Controller和实际操作动画的Controller不是同一个,动画回调无法同步到UI。

修复:删除DraggableCard中的final logic = _DraggableCardLogic(),使用Get.put()确保全局单例Controller。

2. 冗余的_aniBack.obs

_aniBack作为Animation对象不需要被包装成obs,只需在动画回调中直接将动画值同步到_dragAlignment即可。

修复:移除_aniBack的obs包装,通过Alignment.lerp在动画监听中实时计算对齐值。

3. 优化AnimationController初始化

onInit中重复赋值_aniC属于冗余代码,可简化。

修复后的完整代码

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

class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Physics'),
        actions: const [Icon(Icons.share)],
        centerTitle: true,
        elevation: 0,
      ),
      body: const Center(
        child: DraggableCard(child: FlutterLogo(size: 128)),
      ),
    );
  }
}

class DraggableCard extends StatelessWidget {
  const DraggableCard({required this.child, super.key});
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final size = Get.size;
    final logic = Get.put(_DraggableCardLogic());
    
    return Obx(() => GestureDetector(
          onPanDown: (_) => logic._aniC.stop(),
          onPanUpdate: (details) {
            logic._dragAlignment.value += Alignment(
              details.delta.dx / (size.width / 2),
              details.delta.dy / (size.height / 2),
            );
          },
          onPanEnd: (details) => logic._runAni(details.velocity.pixelsPerSecond, size),
          child: Align(
            alignment: logic._dragAlignment.value,
            child: Card(child: child),
          ),
        ));
  }
}

class _DraggableCardLogic extends GetxController with GetSingleTickerProviderStateMixin {
  late final AnimationController _aniC = AnimationController(vsync: this);
  final _dragAlignment = Alignment.center.obs;
  Alignment _startAlignment = Alignment.center;

  void _runAni(Offset pixelsPerSec, Size size) {
    _startAlignment = _dragAlignment.value;
    
    final unitsPerSecX = pixelsPerSec.dx / size.width;
    final unitsPerSecY = pixelsPerSec.dy / size.height;
    final unitsPerSec = Offset(unitsPerSecX, unitsPerSecY);
    final unitVelocity = unitsPerSec.distance;

    const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1);
    final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
    
    _aniC.reset();
    _aniC.animateWith(simulation);
  }

  @override
  void onInit() {
    super.onInit();
    _aniC.addListener(() {
      _dragAlignment.value = Alignment.lerp(
        _startAlignment,
        Alignment.center,
        _aniC.value,
      )!;
    });
  }

  @override
  void onClose() {
    _aniC.dispose();
    super.onClose();
  }
}

关键修改说明

  • 使用Get.put()确保Controller单例,避免重复初始化
  • 移除冗余的_aniBack.obs,通过Alignment.lerp在动画监听中实时计算对齐值
  • 简化GestureDetector的回调写法,代码更简洁
  • 添加_aniC.reset()确保每次动画从头开始

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 05:47:13