如何将图像处理路线转换添加到Flutter

时间:2020-01-24 15:26:50

标签: flutter dart skia

我正在尝试使用Flutter创建自定义路线转换。现有的路线过渡(渐变,缩放等)很好,但是我想创建屏幕过渡,通过捕获屏幕渲染并将效果应用于屏幕来操纵屏幕的内容。基本上,我想重新创建DOOM screen melt效果作为Flutter的路径转换。

DOOM Screen melt example

在我看来,依靠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());
  },
),

在这种情况下,CustomTransitionFadeTransition的几乎完全相同的副本,并且有一些轻命名(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)。当然,我的自定义RenderAnimatedCustomRenderAnimatedOpacity几乎是重复的:

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()的调用似乎都清除了先前的调用,因此仅保留了最后的“条带”。

Transition example

我已经尝试使用clipRectAndPaint()等将其与不同的“图层”属性组合使用,但与上述示例没有什么不同。

使用toImage()捕获图像

在这方面我没有走得更远,但是我的第一次尝试当然是简单地将小部件捕获为图像,我认为这很简单。

不幸的是,这需要我的窗口小部件在自定义路由中包裹在RepaintBoundary()周围。像这样:

return CustomTransition(
  animation: animation,
  child: RepaintBoundary(child: child),
);

然后也许我们可以做child.toImage(),在画布中进行操作,然后显示出来。

我的问题是,每次定义过渡时,都需要用这种方式包装孩子。我希望CustomTransition()来代替它,但是我还没有找到办法,我想知道这是否真的必要。

还有其他具有toImage()函数的类-PictureSceneOffsetLayer-但似乎没有一个是容易获得的。理想情况下,我可以采用一种简单的方法从RenderAnimatedCustom`上的paint()内部将事物捕获为图像。然后,我可以对该图像进行任何形式的处理,然后绘制它。

其他正交解

我知道在StackOverflow(及其他地方)有几个关于如何“从小部件捕获图像”的答案,但是它们似乎特定于using an existing Canvas,使用RepaintBoundary等。

总而言之,我需要的是:一种创建自定义画布操作屏幕过渡的方法。捕获任意窗口小部件(没有显式RepaintBoundary)的能力似乎是实现这一目标的关键。

有任何提示吗?避免RepaintBoundary是愚蠢的吗?这是唯一的方法吗?还是我还有其他方法可以使用“图层”来完成这种分段的子绘图?

此应用示例的最小源代码为available on GitHub

PS。我知道过渡示例,因为它正在尝试操作即将到来的屏幕,而不是即将到来的屏幕,因为它应该可以像Doom的屏幕融化一样工作。这是我目前不在调查的另一个问题。

0 个答案:

没有答案