From 9bed37b52cddfb04531881a846c9be9147e50441 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 6 Aug 2015 15:13:50 -0700 Subject: [PATCH] Add widgets for reparenting widgets Wrap widgets you want to reparent in a Mimicable widget and assign the Mimicable widget a global key. Then, given the same global key to a Mimic widget to make it appear elsewhere in the view hierarchy. --- .../example/widgets/pop_out_and_expand.dart | 90 ++++++++++++ packages/flutter/lib/widgets.dart | 8 +- packages/flutter/lib/widgets/framework.dart | 5 + packages/flutter/lib/widgets/mimic.dart | 131 ++++++++++++++++++ 4 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 packages/flutter/example/widgets/pop_out_and_expand.dart create mode 100644 packages/flutter/lib/widgets/mimic.dart diff --git a/packages/flutter/example/widgets/pop_out_and_expand.dart b/packages/flutter/example/widgets/pop_out_and_expand.dart new file mode 100644 index 0000000000..ae26ee8a21 --- /dev/null +++ b/packages/flutter/example/widgets/pop_out_and_expand.dart @@ -0,0 +1,90 @@ +// 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. + +import 'package:sky/painting/box_painter.dart'; +import 'package:sky/widgets.dart'; + +class Circle extends Component { + Circle({ this.child }); + + Widget child; + + Widget build() { + return new Container( + height: 100.0, + margin: new EdgeDims.symmetric(horizontal: 20.0, vertical: 4.0), + decoration: new BoxDecoration( + backgroundColor: const Color(0xFF0000FF) + ), + child: new Center( + child: child + ) + ); + } +} + +class CircleData { + final GlobalKey key; + final String content; + + CircleData({ this.key, this.content }); +} + +class ExampleApp extends App { + ExampleApp() { + for (int i = 0; i < 20; ++i) { + _data.add(new CircleData( + key: new GlobalKey(), + content: '$i' + )); + } + } + + final List _data = new List(); + + GlobalKey _keyToMimic; + + Widget _buildCircle(CircleData circleData) { + return new Mimicable( + key: circleData.key, + child: new Listener( + child: new Circle( + child: new Text(circleData.content) + ), + onGestureTap: (_) { + setState(() { + _keyToMimic = circleData.key; + }); + } + ) + ); + } + + Widget build() { + List circles = new List(); + for (int i = 0; i < 20; ++i) { + circles.add(_buildCircle(_data[i])); + } + + List layers = new List(); + layers.add(new ScrollableBlock(circles)); + + if (_keyToMimic != null) { + layers.add( + new Positioned( + top: 50.0, + left: 50.0, + child: new Mimic( + original: _keyToMimic) + ) + ); + } + + return new Stack(layers); + } +} + +void main() { + runApp(new ExampleApp()); +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 02d619981f..be7e073772 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -21,11 +21,14 @@ export 'widgets/drawer_item.dart'; export 'widgets/flat_button.dart'; export 'widgets/floating_action_button.dart'; export 'widgets/focus.dart'; -export 'widgets/icon_button.dart'; +export 'widgets/framework.dart'; export 'widgets/icon.dart'; +export 'widgets/icon_button.dart'; export 'widgets/ink_well.dart'; -export 'widgets/material_button.dart'; export 'widgets/material.dart'; +export 'widgets/material_button.dart'; +export 'widgets/mimic.dart'; +export 'widgets/mimic.dart'; export 'widgets/modal_overlay.dart'; export 'widgets/navigator.dart'; export 'widgets/popup_menu.dart'; @@ -41,4 +44,3 @@ export 'widgets/task_description.dart'; export 'widgets/theme.dart'; export 'widgets/tool_bar.dart'; export 'widgets/transitions.dart'; -export 'widgets/framework.dart'; diff --git a/packages/flutter/lib/widgets/framework.dart b/packages/flutter/lib/widgets/framework.dart index 5c8a99f0a5..76b6813ece 100644 --- a/packages/flutter/lib/widgets/framework.dart +++ b/packages/flutter/lib/widgets/framework.dart @@ -110,6 +110,11 @@ abstract class GlobalKey extends Key { assert(removed); } + static Widget getWidget(GlobalKey key) { + assert(key != null); + return _registry[key]; + } + static void _notifyListeners() { assert(!_inRenderDirtyComponents); assert(!Widget._notifyingMountStatus); diff --git a/packages/flutter/lib/widgets/mimic.dart b/packages/flutter/lib/widgets/mimic.dart new file mode 100644 index 0000000000..824b0aa04e --- /dev/null +++ b/packages/flutter/lib/widgets/mimic.dart @@ -0,0 +1,131 @@ +// 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. + +import 'package:sky/widgets/basic.dart'; + +class Mimic extends StatefulComponent { + Mimic({ Key key, this.original }) : super(key: key); + + GlobalKey original; + + void initState() { + _requestToStartMimic(); + } + + void syncFields(Mimic source) { + if (original != source.original) { + _stopMimic(); + original = source.original; + _requestToStartMimic(); + } + } + + void didMount() { + super.didMount(); + // TODO(abarth): Why is didMount being called without a call to didUnmount? + if (_mimicable == null) + _requestToStartMimic(); + } + + void didUnmount() { + super.didUnmount(); + _stopMimic(); + } + + Mimicable _mimicable; + Size _size; + + void _requestToStartMimic() { + assert(_mimicable == null); + assert(_size == null); + if (original == null) + return; + _mimicable = GlobalKey.getWidget(original) as Mimicable; + assert(_mimicable != null); + _mimicable._requestToStartMimic(this); + } + + void _startMimic(GlobalKey key, Size size) { + assert(key == original); + setState(() { + _size = size; + }); + } + + void _stopMimic() { + if (_mimicable != null) + _mimicable._didStopMimic(this); + _mimicable = null; + _size = null; + } + + Widget build() { + if (_size == null || !_mimicable.mounted) + return new Container(); + return new ConstrainedBox( + constraints: new BoxConstraints.tight(_size), + child: _mimicable.child + ); + } +} + +class Mimicable extends StatefulComponent { + Mimicable({ GlobalKey key, this.child }) : super(key: key); + + Widget child; + + Size _size; + Size get size => _size; + + Mimic _mimic; + bool _didStartMimic = false; + + void syncFields(Mimicable source) { + child = source.child; + } + + void _requestToStartMimic(Mimic mimic) { + assert(mounted); + if (_mimic != null) + return; + setState(() { + _mimic = mimic; + _didStartMimic = false; + }); + } + + void _didStopMimic(Mimic mimic) { + assert(_mimic != null); + assert(mimic == _mimic); + setState(() { + _mimic = null; + _didStartMimic = false; + }); + } + + void _handleSizeChanged(Size size) { + setState(() { + _size = size; + }); + } + + void _startMimicIfNeeded() { + if (_didStartMimic) + return; + assert(_mimic != null); + _mimic._startMimic(key, _size); + _didStartMimic = true; + } + + Widget build() { + if (_mimic != null) { + _startMimicIfNeeded(); + return new ConstrainedBox(constraints: new BoxConstraints.tight(_size)); + } + return new SizeObserver( + callback: _handleSizeChanged, + child: child + ); + } +}