// 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/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'constants.dart'; const Duration _kToggleDuration = const Duration(milliseconds: 200); final Tween<double> _kRadialReactionRadiusTween = new Tween<double>(begin: 0.0, end: kRadialReactionRadius); /// A base class for material style toggleable controls with toggle animations. /// /// This class handles storing the current value, dispatching ValueChanged on a /// tap gesture and driving a changed animation. Subclasses are responsible for /// painting. abstract class RenderToggleable extends RenderConstrainedBox { /// Creates a toggleable render object. /// /// The [value], [activeColor], and [inactiveColor] arguments must not be /// null. RenderToggleable({ @required bool value, Size size, @required Color activeColor, @required Color inactiveColor, ValueChanged<bool> onChanged, @required TickerProvider vsync, }) : assert(value != null), assert(activeColor != null), assert(inactiveColor != null), assert(vsync != null), _value = value, _activeColor = activeColor, _inactiveColor = inactiveColor, _onChanged = onChanged, _vsync = vsync, super(additionalConstraints: new BoxConstraints.tight(size)) { _tap = new TapGestureRecognizer() ..onTapDown = _handleTapDown ..onTap = _handleTap ..onTapUp = _handleTapUp ..onTapCancel = _handleTapCancel; _positionController = new AnimationController( duration: _kToggleDuration, value: value ? 1.0 : 0.0, vsync: vsync, ); _position = new CurvedAnimation( parent: _positionController, curve: Curves.linear, )..addListener(markNeedsPaint) ..addStatusListener(_handlePositionStateChanged); _reactionController = new AnimationController( duration: kRadialReactionDuration, vsync: vsync, ); _reaction = new CurvedAnimation( parent: _reactionController, curve: Curves.fastOutSlowIn, )..addListener(markNeedsPaint); } /// Used by subclasses to manipulate the visual value of the control. /// /// Some controls respond to user input by updating their visual value. For /// example, the thumb of a switch moves from one position to another when /// dragged. These controls manipulate this animation controller to update /// their [position] and eventually trigger an [onChanged] callback when the /// animation reaches either 0.0 or 1.0. @protected AnimationController get positionController => _positionController; AnimationController _positionController; /// The visual value of the control. /// /// When the control is inactive, the [value] is false and this animation has /// the value 0.0. When the control is active, the value is true and this /// animation has the value 1.0. When the control is changing from inactive /// to active (or vice versa), [value] is the target value and this animation /// gradually updates from 0.0 to 1.0 (or vice versa). CurvedAnimation get position => _position; CurvedAnimation _position; /// Used by subclasses to control the radial reaction animation. /// /// Some controls have a radial ink reaction to user input. This animation /// controller can be used to start or stop these ink reactions. /// /// Subclasses should call [paintRadialReaction] to actually paint the radial /// reaction. @protected AnimationController get reactionController => _reactionController; AnimationController _reactionController; Animation<double> _reaction; /// The [TickerProvider] for the [AnimationController]s that run the animations. TickerProvider get vsync => _vsync; TickerProvider _vsync; set vsync(TickerProvider value) { assert(value != null); if (value == _vsync) return; _vsync = value; positionController.resync(vsync); reactionController.resync(vsync); } /// Whether this control is current "active" (checked, on, selected) or "inactive" (unchecked, off, not selected). /// /// When the value changes, this object starts the [positionController] and /// [position] animations to animate the visual appearance of the control to /// the new value. bool get value => _value; bool _value; set value(bool value) { assert(value != null); if (value == _value) return; _value = value; markNeedsSemanticsUpdate(onlyLocalUpdates: true); _position ..curve = Curves.easeIn ..reverseCurve = Curves.easeOut; if (value) _positionController.forward(); else _positionController.reverse(); } /// The color that should be used in the active state (i.e., when [value] is true). /// /// For example, a checkbox should use this color when checked. Color get activeColor => _activeColor; Color _activeColor; set activeColor(Color value) { assert(value != null); if (value == _activeColor) return; _activeColor = value; markNeedsPaint(); } /// The color that should be used in the inactive state (i.e., when [value] is false). /// /// For example, a checkbox should use this color when unchecked. Color get inactiveColor => _inactiveColor; Color _inactiveColor; set inactiveColor(Color value) { assert(value != null); if (value == _inactiveColor) return; _inactiveColor = value; markNeedsPaint(); } /// Called when the control changes value. /// /// If the control is tapped, [onChanged] is called immediately with the new /// value. If the control changes value due to an animation (see /// [positionController]), the callback is called when the animation /// completes. /// /// The control is considered interactive (see [isInteractive]) if this /// callback is non-null. If the callback is null, then the control is /// disabled, and non-interactive. A disabled checkbox, for example, is /// displayed using a grey color and its value cannot be changed. ValueChanged<bool> get onChanged => _onChanged; ValueChanged<bool> _onChanged; set onChanged(ValueChanged<bool> value) { if (value == _onChanged) return; final bool wasInteractive = isInteractive; _onChanged = value; if (wasInteractive != isInteractive) { markNeedsPaint(); markNeedsSemanticsUpdate(); } } /// Whether [value] of this control can be changed by user interaction. /// /// The control is considered interactive if the [onChanged] callback is /// non-null. If the callback is null, then the control is disabled, and /// non-interactive. A disabled checkbox, for example, is displayed using a /// grey color and its value cannot be changed. bool get isInteractive => onChanged != null; TapGestureRecognizer _tap; Offset _downPosition; @override void attach(PipelineOwner owner) { super.attach(owner); if (value) _positionController.forward(); else _positionController.reverse(); if (isInteractive) { switch (_reactionController.status) { case AnimationStatus.forward: _reactionController.forward(); break; case AnimationStatus.reverse: _reactionController.reverse(); break; case AnimationStatus.dismissed: case AnimationStatus.completed: // nothing to do break; } } } @override void detach() { _positionController.stop(); _reactionController.stop(); super.detach(); } void _handlePositionStateChanged(AnimationStatus status) { if (isInteractive) { if (status == AnimationStatus.completed && !_value) onChanged(true); else if (status == AnimationStatus.dismissed && _value) onChanged(false); } } void _handleTapDown(TapDownDetails details) { if (isInteractive) { _downPosition = globalToLocal(details.globalPosition); _reactionController.forward(); } } void _handleTap() { if (isInteractive) onChanged(!_value); } void _handleTapUp(TapUpDetails details) { _downPosition = null; if (isInteractive) _reactionController.reverse(); } void _handleTapCancel() { _downPosition = null; if (isInteractive) _reactionController.reverse(); } @override bool hitTestSelf(Offset position) => true; @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent && isInteractive) _tap.addPointer(event); } /// Used by subclasses to paint the radial ink reaction for this control. /// /// The reaction is painted on the given canvas at the given offset. The /// origin is the center point of the reaction (usually distinct from the /// point at which the user interacted with the control, which is handled /// automatically). void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) { if (!_reaction.isDismissed) { // TODO(abarth): We should have a different reaction color when position is zero. final Paint reactionPaint = new Paint()..color = activeColor.withAlpha(kRadialReactionAlpha); final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value); final double radius = _kRadialReactionRadiusTween.evaluate(_reaction); canvas.drawCircle(center + offset, radius, reactionPaint); } } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); config.isSemanticBoundary = isInteractive; if (isInteractive) config.addAction(SemanticsAction.tap, _handleTap); config.isChecked = _value; } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add(new FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true)); description.add(new FlagProperty('isInteractive', value: isInteractive, ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true)); } }