diff --git a/examples/widgets/smooth_resize.dart b/examples/widgets/smooth_resize.dart new file mode 100644 index 0000000000..d493476a92 --- /dev/null +++ b/examples/widgets/smooth_resize.dart @@ -0,0 +1,115 @@ +// 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:flutter/animation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +final List> _kColors = [ + Colors.amber, + Colors.yellow, + Colors.blue, + Colors.purple, + Colors.indigo, + Colors.deepOrange, +]; + +class SmoothBlock extends StatefulComponent { + SmoothBlock({ this.color }); + + final Map color; + + SmoothBlockState createState() => new SmoothBlockState(); +} + +class CardTransition extends StatelessComponent { + CardTransition({ + this.child, + this.performance, + this.x, + this.opacity, + this.scale + }); + + final Widget child; + final Performance performance; + final AnimatedValue x; + final AnimatedValue opacity; + final AnimatedValue scale; + + Widget build(BuildContext context) { + + return new BuilderTransition( + performance: performance, + variables: [x, opacity, scale], + builder: (BuildContext context) { + Matrix4 transform = new Matrix4.identity() + ..translate(x.value) + ..scale(scale.value, scale.value); + return new Opacity( + opacity: opacity.value, + child: new Transform( + transform: transform, + child: child + ) + ); + } + ); + } +} + +class SmoothBlockState extends State { + + double _height = 100.0; + + Widget _handleEnter(PerformanceView performance, Widget child) { + return new CardTransition( + x: new AnimatedValue(-200.0, end: 0.0, curve: Curves.ease), + opacity: new AnimatedValue(0.0, end: 1.0, curve: Curves.ease), + scale: new AnimatedValue(0.8, end: 1.0, curve: Curves.ease), + performance: performance, + child: child + ); + } + + Widget _handleExit(PerformanceView performance, Widget child) { + return new CardTransition( + x: new AnimatedValue(0.0, end: 200.0, curve: Curves.ease), + opacity: new AnimatedValue(1.0, end: 0.0, curve: Curves.ease), + scale: new AnimatedValue(1.0, end: 0.8, curve: Curves.ease), + performance: performance, + child: child + ); + } + + Widget build(BuildContext context) { + return new GestureDetector( + onTap: () { + setState(() { + _height = _height == 100.0 ? 200.0 : 100.0; + }); + }, + child: new EnterExitTransition( + duration: const Duration(milliseconds: 1500), + onEnter: _handleEnter, + onExit: _handleExit, + child: new Container( + key: new ValueKey(_height), + height: _height, + decoration: new BoxDecoration(backgroundColor: config.color[_height.floor() * 4]) + ) + ) + ); + } +} + +class SmoothResizeDemo extends StatelessComponent { + Widget build(BuildContext context) { + return new Block(_kColors.map((Map color) => new SmoothBlock(color: color)).toList()); + } +} + +void main() { + runApp(new SmoothResizeDemo()); +} diff --git a/packages/flutter/lib/src/rendering/overflow.dart b/packages/flutter/lib/src/rendering/overflow.dart index 586cdeeba7..24b3ec081b 100644 --- a/packages/flutter/lib/src/rendering/overflow.dart +++ b/packages/flutter/lib/src/rendering/overflow.dart @@ -146,6 +146,67 @@ class RenderOverflowBox extends RenderBox with RenderObjectWithChildMixin { + RenderSizedOverflowBox({ + RenderBox child, + Size requestedSize + }) : _requestedSize = requestedSize { + assert(requestedSize != null); + this.child = child; + } + + /// The size this render box should attempt to be. + Size get requestedSize => _requestedSize; + Size _requestedSize; + void set requestedSize (Size value) { + assert(value != null); + if (_requestedSize == value) + return; + _requestedSize = value; + markNeedsLayout(); + } + + double getMinIntrinsicWidth(BoxConstraints constraints) { + return constraints.constrainWidth(_requestedSize.width); + } + + double getMaxIntrinsicWidth(BoxConstraints constraints) { + return constraints.constrainWidth(_requestedSize.width); + } + + double getMinIntrinsicHeight(BoxConstraints constraints) { + return constraints.constrainWidth(_requestedSize.height); + } + + double getMaxIntrinsicHeight(BoxConstraints constraints) { + return constraints.constrainWidth(_requestedSize.height); + } + + double computeDistanceToActualBaseline(TextBaseline baseline) { + if (child != null) + return child.getDistanceToActualBaseline(baseline); + return super.computeDistanceToActualBaseline(baseline); + } + + void performLayout() { + size = constraints.constrain(_requestedSize); + if (child != null) + child.layout(constraints); + } + + void hitTestChildren(HitTestResult result, { Point position }) { + if (child != null) + child.hitTest(result, position: position); + else + super.hitTestChildren(result, position: position); + } + + void paint(PaintingContext context, Offset offset) { + if (child != null) + context.paintChild(child, offset.toPoint()); + } +} /// Lays the child out as if it was in the tree, but without painting anything, /// without making the child available for hit testing, and without taking any diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index b98ba838a0..1b50c2b4a9 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -861,8 +861,12 @@ class RenderSizeObserver extends RenderProxyBox { void performLayout() { Size oldSize = hasSize ? size : null; super.performLayout(); - if (oldSize != size) - onSizeChanged(size); + if (oldSize != size) { + // We make a copy of the Size object here because if we leak a _DebugSize + // object out of the render tree, we can get confused later if it comes + // back and gets set as the size property of a RenderBox. + onSizeChanged(new Size(size.width, size.height)); + } } } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 8e21acc985..c9ed90652a 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -346,6 +346,19 @@ class OverflowBox extends OneChildRenderObjectWidget { } } +class SizedOverflowBox extends OneChildRenderObjectWidget { + SizedOverflowBox({ Key key, this.size, Widget child }) + : super(key: key, child: child); + + final Size size; + + RenderSizedOverflowBox createRenderObject() => new RenderSizedOverflowBox(requestedSize: size); + + void updateRenderObject(RenderSizedOverflowBox renderObject, SizedOverflowBox oldWidget) { + renderObject.requestedSize = size; + } +} + class OffStage extends OneChildRenderObjectWidget { OffStage({ Key key, Widget child }) : super(key: key, child: child); diff --git a/packages/flutter/lib/src/widgets/enter_exit_transition.dart b/packages/flutter/lib/src/widgets/enter_exit_transition.dart new file mode 100644 index 0000000000..7b6dbee953 --- /dev/null +++ b/packages/flutter/lib/src/widgets/enter_exit_transition.dart @@ -0,0 +1,185 @@ +// 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 'dart:async'; + +import 'package:flutter/animation.dart'; + +import 'basic.dart'; +import 'framework.dart'; + +class SmoothlyResizingOverflowBox extends StatefulComponent { + SmoothlyResizingOverflowBox({ + Key key, + this.child, + this.size, + this.duration, + this.curve: Curves.linear + }) : super(key: key) { + assert(duration != null); + assert(curve != null); + } + + final Widget child; + final Size size; + final Duration duration; + final Curve curve; + + _SmoothlyResizingOverflowBoxState createState() => new _SmoothlyResizingOverflowBoxState(); +} + +class _SmoothlyResizingOverflowBoxState extends State { + ValuePerformance _size; + + void initState() { + super.initState(); + _size = new ValuePerformance( + variable: new AnimatedSizeValue(config.size, curve: config.curve), + duration: config.duration + )..addListener(() { + setState(() {}); + }); + } + + void didUpdateConfig(SmoothlyResizingOverflowBox oldConfig) { + _size.duration = config.duration; + _size.variable.curve = config.curve; + if (config.size != oldConfig.size) { + AnimatedSizeValue variable = _size.variable; + variable.begin = variable.value; + variable.end = config.size; + _size.progress = 0.0; + _size.play(); + } + } + + void dispose() { + _size.stop(); + super.dispose(); + } + + Widget build(BuildContext context) { + return new SizedOverflowBox( + size: _size.value, + child: config.child + ); + } +} + +class _Entry { + _Entry({ + this.child, + this.enterPerformance, + this.enterTransition + }); + + final Widget child; + final Performance enterPerformance; + final Widget enterTransition; + + Size childSize = Size.zero; + + Performance exitPerformance; + Widget exitTransition; + + Widget get currentTransition => exitTransition ?? enterTransition; + + void dispose() { + enterPerformance?.stop(); + exitPerformance?.stop(); + } +} + +typedef Widget TransitionBuilderCallback(PerformanceView performance, Widget child); + +class EnterExitTransition extends StatefulComponent { + EnterExitTransition({ + Key key, + this.child, + this.duration, + this.curve: Curves.linear, + this.onEnter, + this.onExit + }) : super(key: key) { + assert(child != null); + assert(duration != null); + assert(curve != null); + assert(onEnter != null); + assert(onExit != null); + } + + final Widget child; + final Duration duration; + final Curve curve; + final TransitionBuilderCallback onEnter; + final TransitionBuilderCallback onExit; + + _EnterExitTransitionState createState() => new _EnterExitTransitionState(); +} + +class _EnterExitTransitionState extends State { + final List<_Entry> _entries = new List<_Entry>(); + + void initState() { + super.initState(); + _entries.add(_createEnterTransition()); + } + + _Entry _createEnterTransition() { + Performance enterPerformance = new Performance(duration: config.duration)..play(); + return new _Entry( + child: config.child, + enterPerformance: enterPerformance, + enterTransition: config.onEnter(enterPerformance, new KeyedSubtree( + key: new GlobalKey(), + child: config.child + )) + ); + } + + Future _createExitTransition(_Entry entry) async { + Performance exitPerformance = new Performance(duration: config.duration); + entry + ..exitPerformance = exitPerformance + ..exitTransition = config.onExit(exitPerformance, entry.enterTransition); + await exitPerformance.play(); + if (!mounted) + return; + setState(() { + _entries.remove(entry); + }); + } + + void didUpdateConfig(EnterExitTransition oldConfig) { + if (config.child.key != oldConfig.child.key) { + _createExitTransition(_entries.last); + _entries.add(_createEnterTransition()); + } + } + + void dispose() { + for (_Entry entry in new List<_Entry>.from(_entries)) + entry.dispose(); + super.dispose(); + } + + Widget build(BuildContext context) { + return new SmoothlyResizingOverflowBox( + size: _entries.last.childSize, + duration: config.duration, + curve: config.curve, + child: new Stack(_entries.map((_Entry entry) { + return new SizeObserver( + key: new ObjectKey(entry), + onSizeChanged: (Size newSize) { + setState(() { + entry.childSize = newSize; + }); + }, + child: entry.currentTransition + ); + }).toList()) + ); + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 2ecb32b564..434ebf1258 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -11,6 +11,7 @@ export 'src/widgets/binding.dart'; export 'src/widgets/dismissable.dart'; export 'src/widgets/drag_target.dart'; export 'src/widgets/editable_text.dart'; +export 'src/widgets/enter_exit_transition.dart'; export 'src/widgets/focus.dart'; export 'src/widgets/framework.dart'; export 'src/widgets/gesture_detector.dart';