我正在尝试使用Flutter创建自定义路线转换。现有的路线过渡(渐变,缩放等)很好,但是我想创建屏幕过渡,通过捕获屏幕渲染并将效果应用于屏幕来操纵屏幕的内容。基本上,我想重新创建DOOM screen melt效果作为Flutter的路径转换。
在我看来,依靠Skia和自己的Canvas渲染屏幕元素将使这成为可能,即使这不是很琐碎的事情。不过,我无法做到这一点。我似乎无法捕获屏幕,或者至少无法使用剪切路径以块的形式呈现目标屏幕。这很大程度上与我对Flutter合成的工作原理缺乏了解有关,因此我仍然不确定要调查哪种途径。
我的第一种方法是通过基本上复制FadeTransition
的操作来创建自定义过渡。
Route createRouteWithTransitionCustom() {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => ThirdScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return CustomTransition(
animation: animation,
child: child,
);
},
);
}
RaisedButton(
child: Text('Open Third screen (custom transition, custom code)'),
onPressed: () {
Navigator.push(context, createRouteWithTransitionCustom());
},
),
在这种情况下,CustomTransition
是FadeTransition
的几乎完全相同的副本,并且有一些轻命名(opacity
变成了animation
)。
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'RenderAnimatedCustom.dart';
/// A custom transition to animate a widget.
/// This is a copy of FadeTransition: https://github.com/flutter/flutter/blob/27321ebbad/packages/flutter/lib/src/widgets/transitions.dart#L530
class CustomTransition extends SingleChildRenderObjectWidget {
const CustomTransition({
Key key,
@required this.animation,
this.alwaysIncludeSemantics = false,
Widget child,
}) : assert(animation != null),
super(key: key, child: child);
final Animation<double> animation;
final bool alwaysIncludeSemantics;
@override
RenderAnimatedCustom createRenderObject(BuildContext context) {
return RenderAnimatedCustom(
buildContext: context,
phase: animation,
alwaysIncludeSemantics: alwaysIncludeSemantics,
);
}
@override
void updateRenderObject(BuildContext context, RenderAnimatedCustom renderObject) {
renderObject
..phase = animation
..alwaysIncludeSemantics = alwaysIncludeSemantics;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('animation', animation));
properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
这个新的CustomTransition
还在RenderAnimatedCustom
内部创建了一个新的createRenderObject()
(而不是FadeTransition
自己的RenderAnimatedOpacity
)。当然,我的自定义RenderAnimatedCustom
与RenderAnimatedOpacity
几乎是重复的:
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// A custom renderer.
/// This is a copy of RenderAnimatedOpacity: https://github.com/flutter/flutter/blob/27321ebbad/packages/flutter/lib/src/rendering/proxy_box.dart#L825
class RenderAnimatedCustom extends RenderProxyBox {
RenderAnimatedCustom({
@required BuildContext buildContext,
@required Animation<double> phase,
bool alwaysIncludeSemantics = false,
RenderBox child,
}) : assert(phase != null),
assert(alwaysIncludeSemantics != null),
_alwaysIncludeSemantics = alwaysIncludeSemantics,
super(child) {
this.phase = phase;
this.buildContext = buildContext;
}
BuildContext buildContext;
double _lastUsedPhase;
@override
bool get alwaysNeedsCompositing => child != null && _currentlyNeedsCompositing;
bool _currentlyNeedsCompositing;
Animation<double> get phase => _phase;
Animation<double> _phase;
set phase(Animation<double> value) {
assert(value != null);
if (_phase == value) return;
if (attached && _phase != null) _phase.removeListener(_updatePhase);
_phase = value;
if (attached) _phase.addListener(_updatePhase);
_updatePhase();
}
/// Whether child semantics are included regardless of the opacity.
///
/// If false, semantics are excluded when [opacity] is 0.0.
///
/// Defaults to false.
bool get alwaysIncludeSemantics => _alwaysIncludeSemantics;
bool _alwaysIncludeSemantics;
set alwaysIncludeSemantics(bool value) {
if (value == _alwaysIncludeSemantics) return;
_alwaysIncludeSemantics = value;
markNeedsSemanticsUpdate();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_phase.addListener(_updatePhase);
_updatePhase(); // in case it changed while we weren't listening
}
@override
void detach() {
_phase.removeListener(_updatePhase);
super.detach();
}
void _updatePhase() {
final double newPhase = _phase.value;
if (_lastUsedPhase != newPhase) {
_lastUsedPhase = newPhase;
final bool didNeedCompositing = _currentlyNeedsCompositing;
_currentlyNeedsCompositing = _lastUsedPhase > 0 && _lastUsedPhase < 1;
if (child != null && didNeedCompositing != _currentlyNeedsCompositing) {
markNeedsCompositingBitsUpdate();
}
markNeedsPaint();
if (newPhase == 0 || _lastUsedPhase == 0) {
markNeedsSemanticsUpdate();
}
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_lastUsedPhase == 0) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
return;
}
if (_lastUsedPhase == 1) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
context.paintChild(child, offset);
return;
}
assert(needsCompositing);
// Basic example, slides the screen in
context.paintChild(child, Offset((1 - _lastUsedPhase) * 255, 0));
}
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && (_lastUsedPhase != 0 || alwaysIncludeSemantics)) {
visitor(child);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('phase', phase));
properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
最后,问题就是这样。在以上文件的paint()
内部,此占位符代码只是通过使用context.paintChild()
进行不同的偏移渲染来将屏幕横向移动。
但是我想改为绘制child
的块。在这种情况下,可以使用垂直条来创建屏幕融合效果。但这实际上只是一个例子。我想找到一种方法来操纵孩子的渲染,以便可以有其他基于图像的效果。
我不是简单地做context.paintChild(child, offset)
,而是尝试循环并逐块绘制它。这不是超级通用,但至少可以实现屏幕融化效果。
在paint()
内部(忽略原型代码的笨拙性):
int segments = 10;
double width = context.estimatedBounds.width;
double height = context.estimatedBounds.height;
for (var i = 0; i < segments; i++) {
double l = ((width / segments) * i).round().toDouble();
double r = ((width / segments) * (i + 1)).round().toDouble();
double y = (1 - _lastUsedPhase) * 50 * (i + 1);
layer = context.pushClipRect(
true,
Offset(l, y),
Rect.fromLTWH(0, 0, r - l, height),
(c, o) => c.paintChild(child, Offset(0, o.dy)),
oldLayer: layer
);
}
不幸的是,这不起作用。每次对paintChild()的调用似乎都清除了先前的调用,因此仅保留了最后的“条带”。
我已经尝试使用clipRectAndPaint()
等将其与不同的“图层”属性组合使用,但与上述示例没有什么不同。
toImage()
捕获图像在这方面我没有走得更远,但是我的第一次尝试当然是简单地将小部件捕获为图像,我认为这很简单。
不幸的是,这需要我的窗口小部件在自定义路由中包裹在RepaintBoundary()
周围。像这样:
return CustomTransition(
animation: animation,
child: RepaintBoundary(child: child),
);
然后也许我们可以做child.toImage()
,在画布中进行操作,然后显示出来。
我的问题是,每次定义过渡时,都需要用这种方式包装孩子。我希望CustomTransition()
来代替它,但是我还没有找到办法,我想知道这是否真的必要。
还有其他具有toImage()
函数的类-Picture
,Scene
,OffsetLayer
-但似乎没有一个是容易获得的。理想情况下,我可以采用一种简单的方法从RenderAnimatedCustom`上的paint()
内部将事物捕获为图像。然后,我可以对该图像进行任何形式的处理,然后绘制它。
我知道在StackOverflow(及其他地方)有几个关于如何“从小部件捕获图像”的答案,但是它们似乎特定于using an existing Canvas,使用RepaintBoundary
等。
总而言之,我需要的是:一种创建自定义画布操作屏幕过渡的方法。捕获任意窗口小部件(没有显式RepaintBoundary
)的能力似乎是实现这一目标的关键。
有任何提示吗?避免RepaintBoundary
是愚蠢的吗?这是唯一的方法吗?还是我还有其他方法可以使用“图层”来完成这种分段的子绘图?
此应用示例的最小源代码为available on GitHub。
PS。我知道过渡示例,因为它正在尝试操作即将到来的屏幕,而不是即将到来的屏幕,因为它应该可以像Doom的屏幕融化一样工作。这是我目前不在调查的另一个问题。