Как добавить манипулирующие изображениями переходы маршрута в Flutter - PullRequest
0 голосов
/ 24 января 2020

Я пытаюсь создать собственный переход по маршруту, используя Flutter. Существующие переходы маршрута (fade, scale, et c) хороши, но я хочу создать экранные переходы, которые управляют содержимым экранов, захватывая их рендеринг и применяя к нему эффекты. В основном, я хочу воссоздать эффект DOOM melt как переход маршрута с Flutter.

DOOM Screen melt example

Мне кажется, что его использование 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 () очищает предыдущий вызов, поэтому сохраняется только последняя «полоса».

Transition example

Я пробовал комбинации с другими свойствами «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. Это еще одна проблема, которую я сейчас не расследую.

...