| // 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:sky' as sky; |
| |
| import 'package:sky/animation/animated_value.dart'; |
| import 'package:sky/animation/animation_performance.dart'; |
| import 'package:sky/animation/curves.dart'; |
| import 'package:sky/widgets/basic.dart'; |
| import 'package:sky/widgets/transitions.dart'; |
| import 'package:sky/widgets/framework.dart'; |
| |
| const Duration _kCardDismissFadeout = const Duration(milliseconds: 200); |
| const Duration _kCardDismissResize = const Duration(milliseconds: 300); |
| final Interval _kCardDismissResizeInterval = new Interval(0.4, 1.0); |
| const double _kMinFlingVelocity = 700.0; |
| const double _kMinFlingVelocityDelta = 400.0; |
| const double _kFlingVelocityScale = 1.0 / 300.0; |
| const double _kDismissCardThreshold = 0.6; |
| |
| typedef void ResizedCallback(); |
| typedef void DismissedCallback(); |
| |
| class Dismissable extends StatefulComponent { |
| |
| Dismissable({ |
| Key key, |
| this.child, |
| this.onResized, |
| this.onDismissed |
| // TODO(hansmuller): direction |
| }) : super(key: key); |
| |
| Widget child; |
| ResizedCallback onResized; |
| DismissedCallback onDismissed; |
| |
| AnimationPerformance _fadePerformance; |
| AnimationPerformance _resizePerformance; |
| |
| Size _size; |
| double _dragX = 0.0; |
| bool _dragUnderway = false; |
| |
| void initState() { |
| _fadePerformance = new AnimationPerformance(duration: _kCardDismissFadeout); |
| } |
| |
| void _handleFadeCompleted() { |
| if (!_dragUnderway) |
| _startResizePerformance(); |
| } |
| |
| void syncConstructorArguments(Dismissable source) { |
| child = source.child; |
| onResized = source.onResized; |
| onDismissed = source.onDismissed; |
| } |
| |
| Point get _activeCardDragEndPoint { |
| if (!_isActive) |
| return Point.origin; |
| assert(_size != null); |
| return new Point(_dragX.sign * _size.width * _kDismissCardThreshold, 0.0); |
| } |
| |
| bool get _isActive { |
| return _size != null && (_dragUnderway || _fadePerformance.isAnimating); |
| } |
| |
| void _maybeCallOnResized() { |
| if (onResized != null) |
| onResized(); |
| } |
| |
| void _maybeCallOnDismissed() { |
| if (onDismissed != null) |
| onDismissed(); |
| } |
| |
| void _startResizePerformance() { |
| assert(_size != null); |
| assert(_fadePerformance != null); |
| assert(_fadePerformance.isCompleted); |
| assert(_resizePerformance == null); |
| |
| setState(() { |
| _resizePerformance = new AnimationPerformance() |
| ..duration = _kCardDismissResize |
| ..addListener(_handleResizeProgressChanged); |
| }); |
| } |
| |
| void _handleResizeProgressChanged() { |
| if (_resizePerformance.isCompleted) |
| _maybeCallOnDismissed(); |
| else |
| _maybeCallOnResized(); |
| } |
| |
| EventDisposition _handlePointerDown(sky.PointerEvent event) { |
| if (_fadePerformance.isAnimating) |
| return EventDisposition.processed; |
| |
| _dragUnderway = true; |
| _dragX = 0.0; |
| _fadePerformance.progress = 0.0; |
| return EventDisposition.processed; |
| } |
| |
| EventDisposition _handlePointerMove(sky.PointerEvent event) { |
| if (!_isActive) |
| return EventDisposition.ignored; |
| |
| if (_fadePerformance.isAnimating) |
| return EventDisposition.processed; |
| |
| double oldDragX = _dragX; |
| _dragX += event.dx; |
| if (oldDragX.sign != _dragX.sign) |
| setState(() {}); // Rebuild to update the new drag endpoint. |
| if (!_fadePerformance.isAnimating) |
| _fadePerformance.progress = _dragX.abs() / (_size.width * _kDismissCardThreshold); |
| return EventDisposition.processed; |
| } |
| |
| EventDisposition _handlePointerUpOrCancel(_) { |
| if (!_isActive) |
| return EventDisposition.ignored; |
| |
| if (_fadePerformance.isAnimating) |
| return EventDisposition.processed; |
| |
| _dragUnderway = false; |
| if (_fadePerformance.isCompleted) |
| _startResizePerformance(); |
| else if (!_fadePerformance.isAnimating) |
| _fadePerformance.reverse(); |
| return EventDisposition.processed; |
| } |
| |
| bool _isHorizontalFlingGesture(sky.GestureEvent event) { |
| double vx = event.velocityX.abs(); |
| double vy = event.velocityY.abs(); |
| return vx - vy > _kMinFlingVelocityDelta && vx > _kMinFlingVelocity; |
| } |
| |
| EventDisposition _handleFlingStart(sky.GestureEvent event) { |
| if (!_isActive) |
| return EventDisposition.ignored; |
| |
| _dragUnderway = false; |
| if (_isHorizontalFlingGesture(event)) { |
| _dragX = event.velocityX.sign; |
| if (_fadePerformance.isCompleted) |
| _startResizePerformance(); |
| else |
| _fadePerformance.fling(velocity: event.velocityX.abs() * _kFlingVelocityScale); |
| } else { |
| _fadePerformance.reverse(); |
| } |
| |
| return EventDisposition.processed; |
| } |
| |
| void _handleSizeChanged(Size newSize) { |
| setState(() { |
| _size = new Size.copy(newSize); |
| }); |
| } |
| |
| Widget build() { |
| if (_resizePerformance != null) { |
| AnimatedValue<double> dismissHeight = new AnimatedValue<double>( |
| _size.height, |
| end: 0.0, |
| curve: ease, |
| interval: _kCardDismissResizeInterval |
| ); |
| |
| return new SquashTransition( |
| performance: _resizePerformance, |
| direction: Direction.forward, |
| height: dismissHeight); |
| } |
| |
| return new Listener( |
| onPointerDown: _handlePointerDown, |
| onPointerMove: _handlePointerMove, |
| onPointerUp: _handlePointerUpOrCancel, |
| onPointerCancel: _handlePointerUpOrCancel, |
| onGestureFlingStart: _handleFlingStart, |
| child: new SizeObserver( |
| callback: _handleSizeChanged, |
| child: new FadeTransition( |
| performance: _fadePerformance, |
| onCompleted: _handleFadeCompleted, |
| opacity: new AnimatedValue<double>(1.0, end: 0.0), |
| child: new SlideTransition( |
| performance: _fadePerformance, |
| position: new AnimatedValue<Point>(Point.origin, end: _activeCardDragEndPoint), |
| child: child |
| ) |
| ) |
| ) |
| ); |
| } |
| } |