mimic.dart 7.84 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
import 'dart:async';

7
import 'package:flutter/rendering.dart' show RenderStack;
8 9

import 'basic.dart';
10
import 'container.dart';
11
import 'framework.dart';
12
import 'overlay.dart';
13

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

18
  final MimicableState _state;
19

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

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

29
/// An overlay entry that is mimicking another widget.
30
class MimicOverlayEntry {
31
  MimicOverlayEntry._(this._handle, this._overlay) {
Hixie's avatar
Hixie committed
32
    _initialGlobalBounds = _handle.globalBounds;
33 34
    _overlayEntry = new OverlayEntry(builder: _build);
    _overlay.insert(_overlayEntry);
35 36 37 38
  }

  Rect _initialGlobalBounds;

Hixie's avatar
Hixie committed
39
  MimicableHandle _handle;
40
  OverlayState _overlay;
41 42 43 44 45
  OverlayEntry _overlayEntry;

  // Animation state
  GlobalKey _targetKey;
  Curve _curve;
46
  AnimationController _controller;
47

48 49 50 51 52
  /// 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.
  ///
53
  /// This function can only be called once per overlay entry.
54
  Future<Null> animateTo({
55 56 57 58
    GlobalKey targetKey,
    Duration duration,
    Curve curve: Curves.linear
  }) {
Hixie's avatar
Hixie committed
59
    assert(_handle != null);
60 61 62 63 64 65 66
    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.
67
    assert(_controller == null);
68 69
    // TODO(ianh): Need to get a TickerProvider that's tied to the Overlay's TickerMode.
    _controller = new AnimationController(duration: duration, vsync: _overlay)
70
      ..addListener(_overlayEntry.markNeedsBuild);
71
    return _controller.forward();
72 73
  }

74 75 76 77 78
  /// 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].
79 80 81 82
  void markNeedsBuild() {
   _overlayEntry?.markNeedsBuild();
 }

83 84 85
  /// 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.
86 87 88
  void dispose() {
    _targetKey = null;
    _curve = null;
89
    _controller?.dispose();
90
    _controller = null;
Hixie's avatar
Hixie committed
91 92
    _handle.stopMimic();
    _handle = null;
93 94 95 96 97
    _overlayEntry.remove();
    _overlayEntry = null;
  }

  Widget _build(BuildContext context) {
Hixie's avatar
Hixie committed
98
    assert(_handle != null);
99 100 101 102
    assert(_overlayEntry != null);
    Rect globalBounds = _initialGlobalBounds;
    Point globalPosition = globalBounds.topLeft;
    if (_targetKey != null) {
103
      assert(_controller != null);
104 105 106 107 108
      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);
109 110
        double t = _curve.transform(_controller.value);
        globalPosition = Point.lerp(globalPosition, localPosition, t);
111 112 113
      }
    }

Ian Hickson's avatar
Ian Hickson committed
114
    RenderBox stack = context.ancestorRenderObjectOfType(const TypeMatcher<RenderStack>());
115
    // TODO(abarth): Handle the case where the transform here isn't just a translation.
116 117 118
    // TODO(ianh): We should probably be getting the overlay's render object rather than looking for a RenderStack.
    assert(stack != null);
    Point localPosition = stack.globalToLocal(globalPosition);
119 120 121 122 123
    return new Positioned(
      left: localPosition.x,
      top: localPosition.y,
      width: globalBounds.width,
      height: globalBounds.height,
Hixie's avatar
Hixie committed
124
      child: new Mimic(original: _handle)
125 126 127 128
    );
  }
}

129
/// A widget that copies the appearance of another widget.
130
class Mimic extends StatelessWidget {
131
  /// Creates a widget that copies the appearance of another widget.
132
  Mimic({ Key key, this.original }) : super(key: key);
133

134 135
  /// A handle to the widget that this widget should copy.
  final MimicableHandle original;
136

137
  @override
138
  Widget build(BuildContext context) {
Hixie's avatar
Hixie committed
139
    if (original != null && original._state.mounted && original._state._placeholderSize != null)
140 141
      return original._state.config.child;
    return new Container();
142 143 144
  }
}

145
/// A widget that can be copied by a [Mimic].
146 147 148
///
/// This widget's State, [MimicableState], contains an API for initiating the
/// mimic operation.
149
class Mimicable extends StatefulWidget {
150
  /// Creates a widget that can be copies by a [Mimic].
151
  Mimicable({ Key key, this.child }) : super(key: key);
152

153
  /// The widget below this widget in the tree.
154
  final Widget child;
155

156
  @override
157
  MimicableState createState() => new MimicableState();
158
}
159

160 161 162
/// The state for a [Mimicable].
///
/// Exposes an API for starting and stopping mimicking.
163
class MimicableState extends State<Mimicable> {
Hixie's avatar
Hixie committed
164 165 166 167 168 169 170 171 172 173 174
  Size _placeholderSize;

  Rect get _globalBounds {
    assert(mounted);
    RenderBox box = context.findRenderObject();
    assert(box != null);
    assert(box.hasSize);
    assert(!box.needsLayout);
    // TODO(abarth): The bounds will be wrong if there's a scale or rotation transform involved
    return box.localToGlobal(Point.origin) & box.size;
  }
175

176 177
  /// Start the mimicking process.
  ///
Hixie's avatar
Hixie committed
178 179 180 181 182 183 184 185 186
  /// 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.
  ///
  /// If you use startMimic(), it is your responsibility to do
  /// something with the returned [MimicableHandle]; typically,
  /// passing it to a [Mimic] widget. To mimic the child in the
  /// [Overlay], consider using [liftToOverlay()] instead.
187
  MimicableHandle startMimic() {
188 189
    assert(() {
      if (_placeholderSize != null) {
190
        throw new FlutterError(
191 192 193 194 195 196 197 198 199
          'Mimicable started while already active.\n'
          'When startMimic() or liftToOverlay() is called on a MimicableState, the mimic becomes active. '
          'While active, it cannot be reactivated until it is stopped. '
          'To stop a Mimicable started with startMimic(), call the MimicableHandle object\'s stopMimic() method. '
          'To stop a Mimicable started with liftToOverlay(), call dispose() on the MimicOverlayEntry.'
        );
      }
      return true;
    });
Hixie's avatar
Hixie committed
200 201 202 203
    RenderBox box = context.findRenderObject();
    assert(box != null);
    assert(box.hasSize);
    assert(!box.needsLayout);
204
    setState(() {
Hixie's avatar
Hixie committed
205
      _placeholderSize = box.size;
206
    });
207
    return new MimicableHandle._(this);
208 209
  }

Hixie's avatar
Hixie committed
210 211
  /// Start the mimicking process and mimic this object in the
  /// enclosing [Overlay].
212
  ///
Hixie's avatar
Hixie committed
213 214 215 216 217
  /// 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.
218
  MimicOverlayEntry liftToOverlay() {
219
    OverlayState overlay = Overlay.of(context, debugRequiredFor: config);
220
    return new MimicOverlayEntry._(startMimic(), overlay);
221 222
  }

223
  void _stopMimic() {
Hixie's avatar
Hixie committed
224 225 226 227 228
    assert(_placeholderSize != null);
    if (mounted) {
      setState(() {
        _placeholderSize = null;
      });
229
    }
230 231
  }

232
  @override
233
  Widget build(BuildContext context) {
Hixie's avatar
Hixie committed
234
    if (_placeholderSize != null) {
235
      return new ConstrainedBox(
Hixie's avatar
Hixie committed
236
        constraints: new BoxConstraints.tight(_placeholderSize)
237
      );
238
    }
Hixie's avatar
Hixie committed
239
    return config.child;
240 241
  }
}