expand_icon.dart 6.44 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Ian Hickson's avatar
Ian Hickson committed
4

5 6
// @dart = 2.8

7
import 'dart:math' as math;
8 9 10 11 12 13

import 'package:flutter/widgets.dart';

import 'colors.dart';
import 'debug.dart';
import 'icon_button.dart';
14
import 'icons.dart';
15
import 'material_localizations.dart';
16 17 18
import 'theme.dart';

/// A widget representing a rotating expand/collapse button. The icon rotates
19
/// 180 degrees when pressed, then reverts the animation on a second press.
20 21
/// The underlying icon is [Icons.expand_more].
///
22 23 24 25
/// The expand icon does not include a semantic label for accessibility. In
/// order to be accessible it should be combined with a label using
/// [MergeSemantics]. This is done automatically by the [ExpansionPanel] widget.
///
26 27
/// See [IconButton] for a more general implementation of a pressable button
/// with an icon.
28 29 30 31
///
/// See also:
///
///  * https://material.io/design/iconography/system-icons.html
32 33 34
class ExpandIcon extends StatefulWidget {
  /// Creates an [ExpandIcon] with the given padding, and a callback that is
  /// triggered when the icon is pressed.
35
  const ExpandIcon({
36
    Key key,
37 38
    this.isExpanded = false,
    this.size = 24.0,
39
    @required this.onPressed,
40
    this.padding = const EdgeInsets.all(8.0),
41 42 43
    this.color,
    this.disabledColor,
    this.expandedColor,
44 45 46 47
  }) : assert(isExpanded != null),
       assert(size != null),
       assert(padding != null),
       super(key: key);
48

49 50 51 52 53 54
  /// Whether the icon is in an expanded state.
  ///
  /// Rebuilding the widget with a different [isExpanded] value will trigger
  /// the animation, but will not trigger the [onPressed] callback.
  final bool isExpanded;

55 56 57 58 59 60
  /// The size of the icon.
  ///
  /// This property must not be null. It defaults to 24.0.
  final double size;

  /// The callback triggered when the icon is pressed and the state changes
61
  /// between expanded and collapsed. The value passed to the current state.
62 63 64 65
  ///
  /// If this is set to null, the button will be disabled.
  final ValueChanged<bool> onPressed;

66
  /// The padding around the icon. The entire padded icon will react to input
67 68 69
  /// gestures.
  ///
  /// This property must not be null. It defaults to 8.0 padding on all sides.
70
  final EdgeInsetsGeometry padding;
71

72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

  /// The color of the icon.
  ///
  /// Defaults to [Colors.black54] when the theme's
  /// [ThemeData.brightness] is [Brightness.light] and to
  /// [Colors.white60] when it is [Brightness.dark]. This adheres to the
  /// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color)
  /// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application)
  final Color color;

  /// The color of the icon when it is disabled,
  /// i.e. if [onPressed] is null.
  ///
  /// Defaults to [Colors.black38] when the theme's
  /// [ThemeData.brightness] is [Brightness.light] and to
87
  /// [Colors.white38] when it is [Brightness.dark]. This adheres to the
88 89 90 91 92 93 94 95 96 97 98 99 100
  /// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color)
  /// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application)
  final Color disabledColor;

  /// The color of the icon when the icon is expanded.
  ///
  /// Defaults to [Colors.black54] when the theme's
  /// [ThemeData.brightness] is [Brightness.light] and to
  /// [Colors.white] when it is [Brightness.dark]. This adheres to the
  /// Material Design specifications for [icons](https://material.io/design/iconography/system-icons.html#color)
  /// and for [dark theme](https://material.io/design/color/dark-theme.html#ui-application)
  final Color expandedColor;

101
  @override
102
  _ExpandIconState createState() => _ExpandIconState();
103 104
}

105
class _ExpandIconState extends State<ExpandIcon> with SingleTickerProviderStateMixin {
106 107 108
  AnimationController _controller;
  Animation<double> _iconTurns;

109 110 111
  static final Animatable<double> _iconTurnTween = Tween<double>(begin: 0.0, end: 0.5)
    .chain(CurveTween(curve: Curves.fastOutSlowIn));

112 113 114
  @override
  void initState() {
    super.initState();
115
    _controller = AnimationController(duration: kThemeAnimationDuration, vsync: this);
116
    _iconTurns = _controller.drive(_iconTurnTween);
117 118 119 120
    // If the widget is initially expanded, rotate the icon without animating it.
    if (widget.isExpanded) {
      _controller.value = math.pi;
    }
121 122 123 124 125 126 127 128
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

129
  @override
130
  void didUpdateWidget(ExpandIcon oldWidget) {
131
    super.didUpdateWidget(oldWidget);
132 133
    if (widget.isExpanded != oldWidget.isExpanded) {
      if (widget.isExpanded) {
134
        _controller.forward();
135
      } else {
136
        _controller.reverse();
137 138 139
      }
    }
  }
140

141
  void _handlePressed() {
142 143
    if (widget.onPressed != null)
      widget.onPressed(widget.isExpanded);
144 145
  }

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
  /// Default icon colors and opacities for when [Theme.brightness] is set to
  /// [Brightness.light] are based on the
  /// [Material Design system icon specifications](https://material.io/design/iconography/system-icons.html#color).
  /// Icon colors and opacities for [Brightness.dark] are based on the
  /// [Material Design dark theme specifications](https://material.io/design/color/dark-theme.html#ui-application)
  Color get _iconColor {
    if (widget.isExpanded && widget.expandedColor != null) {
      return widget.expandedColor;
    }

    if (widget.color != null) {
      return widget.color;
    }

    switch(Theme.of(context).brightness) {
      case Brightness.light:
        return Colors.black54;
      case Brightness.dark:
        return Colors.white60;
    }

    assert(false);
    return null;
  }

171 172 173
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
174
    assert(debugCheckHasMaterialLocalizations(context));
175 176 177
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
    final String onTapHint = widget.isExpanded ? localizations.expandedIconTapHint : localizations.collapsedIconTapHint;

178
    return Semantics(
179
      onTapHint: widget.onPressed == null ? null : onTapHint,
180
      child: IconButton(
181
        padding: widget.padding,
182
        iconSize: widget.size,
183 184
        color: _iconColor,
        disabledColor: widget.disabledColor,
185
        onPressed: widget.onPressed == null ? null : _handlePressed,
186
        icon: RotationTransition(
187
          turns: _iconTurns,
188
          child: const Icon(Icons.expand_more),
189 190
        ),
      ),
191 192 193
    );
  }
}