mimic.dart 6.43 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 10

import 'basic.dart';
import 'framework.dart';
11
import 'overlay.dart';
12

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

17
  final MimicableState _state;
18

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

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

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

  Rect _initialGlobalBounds;

37
  MimicableHandle _key;
38 39 40 41 42
  OverlayEntry _overlayEntry;

  // Animation state
  GlobalKey _targetKey;
  Curve _curve;
43
  AnimationController _controller;
44

45 46 47 48 49
  /// 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.
  ///
50
  /// This function can only be called once per overlay entry.
51 52 53 54 55 56 57 58 59 60 61 62 63
  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.
64 65
    assert(_controller == null);
    _controller = new AnimationController(duration: duration)
66
      ..addListener(_overlayEntry.markNeedsBuild);
67
    return _controller.forward();
68 69
  }

70 71 72 73 74
  /// 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].
75 76 77 78
  void markNeedsBuild() {
   _overlayEntry?.markNeedsBuild();
 }

79 80 81
  /// 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.
82 83 84
  void dispose() {
    _targetKey = null;
    _curve = null;
85 86
    _controller?.stop();
    _controller = null;
87 88 89 90 91 92 93 94 95 96 97 98
    _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) {
99
      assert(_controller != null);
100 101 102 103 104
      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);
105 106
        double t = _curve.transform(_controller.value);
        globalPosition = Point.lerp(globalPosition, localPosition, t);
107 108 109
      }
    }

Ian Hickson's avatar
Ian Hickson committed
110
    RenderBox stack = context.ancestorRenderObjectOfType(const TypeMatcher<RenderStack>());
111
    // TODO(abarth): Handle the case where the transform here isn't just a translation.
112 113 114
    // 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);
115 116 117 118 119 120 121 122 123 124
    return new Positioned(
      left: localPosition.x,
      top: localPosition.y,
      width: globalBounds.width,
      height: globalBounds.height,
      child: new Mimic(original: _key)
    );
  }
}

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

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

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

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

143
  final Widget child;
144

145
  MimicableState createState() => new MimicableState();
146
}
147

148 149 150
/// The state for a [Mimicable].
///
/// Exposes an API for starting and stopping mimicking.
151
class MimicableState extends State<Mimicable> {
152 153
  Size _size;
  bool _beingMimicked = false;
154

155 156 157 158 159 160
  /// 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() {
161 162
    assert(!_beingMimicked);
    assert(_size != null);
163
    setState(() {
164
      _beingMimicked = true;
165
    });
166
    return new MimicableHandle._(this);
167 168
  }

169 170 171 172 173 174
  /// 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.
175 176 177 178 179 180 181 182
  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;
  }

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

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

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

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