expand_icon.dart 4.11 KB
Newer Older
1 2 3
// 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.
4
import 'dart:math' as math;
5 6 7 8 9 10

import 'package:flutter/widgets.dart';

import 'colors.dart';
import 'debug.dart';
import 'icon_button.dart';
11
import 'icons.dart';
12
import 'material_localizations.dart';
13 14 15
import 'theme.dart';

/// A widget representing a rotating expand/collapse button. The icon rotates
16
/// 180 degrees when pressed, then reverts the animation on a second press.
17 18
/// The underlying icon is [Icons.expand_more].
///
19 20 21 22
/// 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.
///
23 24 25 26 27
/// See [IconButton] for a more general implementation of a pressable button
/// with an icon.
class ExpandIcon extends StatefulWidget {
  /// Creates an [ExpandIcon] with the given padding, and a callback that is
  /// triggered when the icon is pressed.
28
  const ExpandIcon({
29
    Key key,
30 31
    this.isExpanded = false,
    this.size = 24.0,
32
    @required this.onPressed,
33
    this.padding = const EdgeInsets.all(8.0),
34 35 36 37
  }) : assert(isExpanded != null),
       assert(size != null),
       assert(padding != null),
       super(key: key);
38

39 40 41 42 43 44
  /// 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;

45 46 47 48 49 50
  /// 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
51
  /// between expanded and collapsed. The value passed to the current state.
52 53 54 55
  ///
  /// If this is set to null, the button will be disabled.
  final ValueChanged<bool> onPressed;

56
  /// The padding around the icon. The entire padded icon will react to input
57 58 59
  /// gestures.
  ///
  /// This property must not be null. It defaults to 8.0 padding on all sides.
60
  final EdgeInsetsGeometry padding;
61 62

  @override
63
  _ExpandIconState createState() => _ExpandIconState();
64 65
}

66
class _ExpandIconState extends State<ExpandIcon> with SingleTickerProviderStateMixin {
67 68 69
  AnimationController _controller;
  Animation<double> _iconTurns;

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

73 74 75
  @override
  void initState() {
    super.initState();
76
    _controller = AnimationController(duration: kThemeAnimationDuration, vsync: this);
77
    _iconTurns = _controller.drive(_iconTurnTween);
78 79 80 81
    // If the widget is initially expanded, rotate the icon without animating it.
    if (widget.isExpanded) {
      _controller.value = math.pi;
    }
82 83 84 85 86 87 88 89
  }

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

90
  @override
91
  void didUpdateWidget(ExpandIcon oldWidget) {
92
    super.didUpdateWidget(oldWidget);
93 94
    if (widget.isExpanded != oldWidget.isExpanded) {
      if (widget.isExpanded) {
95
        _controller.forward();
96
      } else {
97
        _controller.reverse();
98 99 100
      }
    }
  }
101

102
  void _handlePressed() {
103 104
    if (widget.onPressed != null)
      widget.onPressed(widget.isExpanded);
105 106 107 108 109
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
110
    assert(debugCheckHasMaterialLocalizations(context));
111
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
112
    final ThemeData theme = Theme.of(context);
113 114
    final String onTapHint = widget.isExpanded ? localizations.expandedIconTapHint : localizations.collapsedIconTapHint;

115
    return Semantics(
116
      onTapHint: widget.onPressed == null ? null : onTapHint,
117
      child: IconButton(
118
        padding: widget.padding,
119
        color: theme.brightness == Brightness.dark ? Colors.white54 : Colors.black54,
120
        onPressed: widget.onPressed == null ? null : _handlePressed,
121
        icon: RotationTransition(
122
          turns: _iconTurns,
123
          child: const Icon(Icons.expand_more),
124 125
        ),
      ),
126 127 128
    );
  }
}