Я пытаюсь создать собственный переход по маршруту, используя Flutter. Существующие переходы маршрута (fade, scale, et c) хороши, но я хочу создать экранные переходы, которые управляют содержимым экранов, захватывая их рендеринг и применяя к нему эффекты. В основном, я хочу воссоздать эффект DOOM melt как переход маршрута с Flutter.
Мне кажется, что его использование Skia и его собственного Canvas для рендеринга элементов экрана сделало бы это возможным, если не несколько тривиальным. Но я не смог этого сделать. Кажется, я не могу сделать снимок экрана или, по крайней мере, отобразить целевой экран кусками, используя обтравочные контуры. Во многом это связано с моим непониманием того, как работает композиция Флаттера, поэтому я до сих пор не знаю, какие пути исследовать.
Мой первый подход - создание собственного перехода путем репликации того, что 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)
, я пробовал зацикливать и рисовать его по частям. Это не супер универсальный c, но он будет работать как минимум для эффекта плавления экрана.
Внутри 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 () очищает предыдущий вызов, поэтому сохраняется только последняя «полоса».
Я пробовал комбинации с другими свойствами «layer», используя clipRectAndPaint()
, et c, но не может получить ничего отличного от приведенного выше примера.
Захват изображения с помощью toImage()
Я не пошел намного дальше в этом, но моей первой попыткой было, конечно, просто захватить виджет как изображение, что я предполагаю, было просто.
К сожалению, это требует, чтобы мой виджет был обернут вокруг RepaintBoundary()
в пользовательском маршруте. Примерно так:
return CustomTransition(
animation: animation,
child: RepaintBoundary(child: child),
);
Тогда, может быть, мы могли бы просто сделать child.toImage()
, манипулировать этим внутри холста и представить это.
Моя проблема в том, что каждый раз, когда определяется переход, ребенок должен быть обернут таким образом. Я бы хотел, чтобы CustomTransition()
справился с этим, но я не нашел способа, и мне интересно, действительно ли это необходимо.
Существуют другие классы с функцией toImage()
- Picture
, Scene
, OffsetLayer
- но ни один из них, казалось, не был легко доступен. В идеале для меня было бы простым способом захватывать вещи в виде изображения из paint()
на RenderAnimatedCustom`. Затем я мог бы выполнить любые манипуляции с этим изображением и нарисовать его.
Другие ортогональные решения
Я знаю, что есть несколько ответов на StackOverflow (и в других местах) о том, как "захватить «Изображение из виджета», но они, кажется, указывают c до , используя существующий Canvas , используя RepaintBoundary
, et c.
В итоге, что мне нужно: способ создания пользовательских переходов экрана, управляющих холстом. Способность захватывать произвольные виджеты (без явного RepaintBoundary
) кажется ключом к этому.
Есть подсказки? Я дурак sh за то, что избегаю RepaintBoundary
? Это единственный способ? Или есть какой-то другой способ использовать «слои» для выполнения sh такого рода сегментированного дочернего чертежа?
Минимальный исходный код для этого примера приложения доступен на GitHub .
PS. Мне известен пример перехода, поскольку он пытается манипулировать экраном oncoming , а не outgoing , так как он должен работать так, чтобы он работал как расплавление экрана в Doom. Это еще одна проблема, которую я сейчас не расследую.