mimic.dart 6.39 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7 8
import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/animation.dart';
9
import 'package:flutter/rendering.dart';
10 11 12

import 'basic.dart';
import 'framework.dart';
13
import 'overlay.dart';
14

15 16 17
/// An opaque reference to a widget that can be mimicked.
class MimicableHandle {
  MimicableHandle._(this._state);
18

19
  final MimicableState _state;
20

21
  /// The size and position of the original widget in global coordinates.
22
  Rect get globalBounds => _state._globalBounds;
23

24
  /// Stop the mimicking process, restoring the widget to its original location in the tree.
25 26
  void stopMimic() {
    _state._stopMimic();
27
  }
28
}
29

30
/// An overlay entry that is mimicking another widget.
31 32 33 34 35 36 37 38
class MimicOverlayEntry {
  MimicOverlayEntry._(this._key) {
    _overlayEntry = new OverlayEntry(builder: _build);
    _initialGlobalBounds = _key.globalBounds;
  }

  Rect _initialGlobalBounds;

39
  MimicableHandle _key;
40 41 42 43 44 45 46
  OverlayEntry _overlayEntry;

  // Animation state
  GlobalKey _targetKey;
  Curve _curve;
  Performance _performance;

47 48 49 50 51
  /// Animate the entry to the location of the widget that has the given target key.
  ///
  /// The animation will take place over the given duration and will apply the
  /// given curve.
  ///
52
  /// This function can only be called once per overlay entry.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
  Future animateTo({
    GlobalKey targetKey,
    Duration duration,
    Curve curve: Curves.linear
  }) {
    assert(_key != null);
    assert(_overlayEntry != null);
    assert(targetKey != null);
    assert(duration != null);
    assert(curve != null);
    _targetKey = targetKey;
    _curve = curve;
    // TODO(abarth): Support changing the animation target when in flight.
    assert(_performance == null);
    _performance = new Performance(duration: duration)
      ..addListener(_overlayEntry.markNeedsBuild);
    return _performance.play();
  }

72 73 74 75 76
  /// Cause the overlay entry to rebuild during the next pipeline flush.
  ///
  /// You need to call this function if you rebuild the widget that this entry
  /// is mimicking in order for the overlay entry to pick up the changes that
  /// you've made to the [Mimicable].
77 78 79 80
  void markNeedsBuild() {
   _overlayEntry?.markNeedsBuild();
 }

81 82 83
  /// Remove this entry from the overlay and restore the widget to its original place in the tree.
  ///
  /// Once removed, the overlay entry cannot be used further.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
  void dispose() {
    _targetKey = null;
    _curve = null;
    _performance?.stop();
    _performance = null;
    _key.stopMimic();
    _key = null;
    _overlayEntry.remove();
    _overlayEntry = null;
  }

  Widget _build(BuildContext context) {
    assert(_key != null);
    assert(_overlayEntry != null);
    Rect globalBounds = _initialGlobalBounds;
    Point globalPosition = globalBounds.topLeft;
    if (_targetKey != null) {
      assert(_performance != null);
      assert(_curve != null);
      RenderBox box = _targetKey.currentContext?.findRenderObject();
      if (box != null) {
        // TODO(abarth): Handle the case where the transform here isn't just a translation.
        Point localPosition = box.localToGlobal(Point.origin);
        double t = _curve.transform(_performance.progress);
        // TODO(abarth): Add Point.lerp.
        globalPosition = new Point(ui.lerpDouble(globalPosition.x, localPosition.x, t),
                                 ui.lerpDouble(globalPosition.y, localPosition.y, t));
      }
    }

    RenderBox stack = context.ancestorRenderObjectOfType(RenderStack);
    // TODO(abarth): Handle the case where the transform here isn't just a translation.
    Point localPosition = stack == null ? globalPosition: stack.globalToLocal(globalPosition);
    return new Positioned(
      left: localPosition.x,
      top: localPosition.y,
      width: globalBounds.width,
      height: globalBounds.height,
      child: new Mimic(original: _key)
    );
  }
}

127
/// A widget that copies the appearance of another widget.
128 129
class Mimic extends StatelessComponent {
  Mimic({ Key key, this.original }) : super(key: key);
130

131 132
  /// A handle to the widget that this widget should copy.
  final MimicableHandle original;
133

134 135 136 137
  Widget build(BuildContext context) {
    if (original != null && original._state._beingMimicked)
      return original._state.config.child;
    return new Container();
138 139 140
  }
}

141
/// A widget that can be copied by a [Mimic].
142
class Mimicable extends StatefulComponent {
143
  Mimicable({ Key key, this.child }) : super(key: key);
144

145
  final Widget child;
146

147
  MimicableState createState() => new MimicableState();
148
}
149

150
class MimicableState extends State<Mimicable> {
151 152
  Size _size;
  bool _beingMimicked = false;
153

154 155 156 157 158 159
  /// Start the mimicking process.
  ///
  /// The child of this object will no longer be built at this location in the
  /// tree.  Instead, this widget will build a transparent placeholder with the
  /// same dimensions as the widget had when the mimicking process started.
  MimicableHandle startMimic() {
160 161
    assert(!_beingMimicked);
    assert(_size != null);
162
    setState(() {
163
      _beingMimicked = true;
164
    });
165
    return new MimicableHandle._(this);
166 167
  }

168 169 170 171 172 173
  /// Mimic this object in the enclosing overlay.
  ///
  /// The child of this object will no longer be built at this location in the
  /// tree.  Instead, (1) this widget will build a transparent placeholder with
  /// the same dimensions as the widget had when the mimicking process started
  /// and (2) the child will be placed in the enclosing overlay.
174 175 176 177 178 179 180 181
  MimicOverlayEntry liftToOverlay() {
    OverlayState overlay = Overlay.of(context);
    assert(overlay != null); // You need an overlay to lift into.
    MimicOverlayEntry entry = new MimicOverlayEntry._(startMimic());
    overlay.insert(entry._overlayEntry);
    return entry;
  }

182 183 184 185 186 187
  void _stopMimic() {
    assert(_beingMimicked);
    if (!mounted) {
      _beingMimicked = false;
      return;
    }
188
    setState(() {
189
      _beingMimicked = false;
190 191 192
    });
  }

193 194 195 196 197
  Rect get _globalBounds {
    RenderBox box = context.findRenderObject();
    return box.localToGlobal(Point.origin) & box.size;
  }

198 199 200 201 202 203
  void _handleSizeChanged(Size size) {
    setState(() {
      _size = size;
    });
  }

204 205
  Widget build(BuildContext context) {
    if (_beingMimicked) {
206 207 208
      return new ConstrainedBox(
        constraints: new BoxConstraints.tight(_size)
      );
209 210
    }
    return new SizeObserver(
211
      onSizeChanged: _handleSizeChanged,
212
      child: config.child
213 214 215
    );
  }
}