要偏移的颤振像素坐标



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

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

xy属性为屏幕坐标(以像素表示)。假设现在我想绘制一个Text(带有消息中对象的name属性),它根据消息中的数据从(x1, y1)动画到(x2, y2)

我查看了animatedpositioning,它指向了SlideTransition

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

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

我故意给出了一些背景信息,以注意我不能更改这些消息的格式。我只需要将屏幕坐标转换为相对于应用程序根的偏移量,而不是相对于其父的偏移量。

double leftPosition = data[0]['x1'];
double topPosition = data[0]['y1'];

调用future.delayed设置left and top position to next value,如

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,
)
]
),

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

如果你"需要完全控制我的widget ";我会推荐CustomMultiChildLayout,它不仅可以让你知道你的孩子小部件的大小在布局阶段(所以你可以对齐他们在一些Offset),但也支持"animated"布局没有重建的孩子- in "normal"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;
}

编辑添加了一个修改版本的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 animationnnalso 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;
}

最新更新