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

import 'package:flutter/widgets.dart';

import 'constants.dart';
import 'theme.dart';

10
// Examples can assume:
11
// late String userAvatarUrl;
12

Hixie's avatar
Hixie committed
13 14
/// A circle that represents a user.
///
15
/// Typically used with a user's profile image, or, in the absence of
Hixie's avatar
Hixie committed
16 17 18
/// such an image, the user's initials. A given user's initials should
/// always be paired with the same background color, for consistency.
///
19 20 21
/// If [foregroundImage] fails then [backgroundImage] is used. If
/// [backgroundImage] fails too, [backgroundColor] is used.
///
22 23
/// The [onBackgroundImageError] parameter must be null if the [backgroundImage]
/// is null.
24 25
/// The [onForegroundImageError] parameter must be null if the [foregroundImage]
/// is null.
26
///
27
/// {@tool snippet}
Ian Hickson's avatar
Ian Hickson committed
28
///
29 30 31 32
/// If the avatar is to have an image, the image should be specified in the
/// [backgroundImage] property:
///
/// ```dart
33 34
/// CircleAvatar(
///   backgroundImage: NetworkImage(userAvatarUrl),
35 36
/// )
/// ```
37
/// {@end-tool}
38 39 40
///
/// The image will be cropped to have a circle shape.
///
41
/// {@tool snippet}
42
///
43 44 45 46
/// If the avatar is to just have the user's initials, they are typically
/// provided using a [Text] widget as the [child] and a [backgroundColor]:
///
/// ```dart
47
/// CircleAvatar(
48
///   backgroundColor: Colors.brown.shade800,
49
///   child: const Text('AH'),
Ian Hickson's avatar
Ian Hickson committed
50
/// )
51
/// ```
52
/// {@end-tool}
53
///
54
/// See also:
55
///
56
///  * [Chip], for representing users or concepts in long form.
57 58
///  * [ListTile], which can combine an icon (such as a [CircleAvatar]) with
///    some text for a fixed height list entry.
59
///  * <https://material.io/design/components/chips.html#input-chips>
60
class CircleAvatar extends StatelessWidget {
61
  /// Creates a circle that represents a user.
62
  const CircleAvatar({
63
    Key? key,
Hans Muller's avatar
Hans Muller committed
64
    this.child,
Adam Barth's avatar
Adam Barth committed
65
    this.backgroundColor,
66
    this.backgroundImage,
67
    this.foregroundImage,
68
    this.onBackgroundImageError,
69
    this.onForegroundImageError,
70
    this.foregroundColor,
71 72 73
    this.radius,
    this.minRadius,
    this.maxRadius,
74
  }) : assert(radius == null || (minRadius == null && maxRadius == null)),
75
       assert(backgroundImage != null || onBackgroundImageError == null),
76
       assert(foregroundImage != null || onForegroundImageError== null),
77
       super(key: key);
Adam Barth's avatar
Adam Barth committed
78

79
  /// The widget below this widget in the tree.
80 81 82
  ///
  /// Typically a [Text] widget. If the [CircleAvatar] is to have an image, use
  /// [backgroundImage] instead.
83
  final Widget? child;
Hixie's avatar
Hixie committed
84 85 86

  /// The color with which to fill the circle. Changing the background
  /// color will cause the avatar to animate to the new color.
87
  ///
88 89 90
  /// If a [backgroundColor] is not specified, the theme's
  /// [ThemeData.primaryColorLight] is used with dark foreground colors, and
  /// [ThemeData.primaryColorDark] with light foreground colors.
91
  final Color? backgroundColor;
Hixie's avatar
Hixie committed
92

93 94
  /// The default text color for text in the circle.
  ///
95 96 97 98 99
  /// Defaults to the primary text theme color if no [backgroundColor] is
  /// specified.
  ///
  /// Defaults to [ThemeData.primaryColorLight] for dark background colors, and
  /// [ThemeData.primaryColorDark] for light background colors.
100
  final Color? foregroundColor;
101

102 103
  /// The background image of the circle. Changing the background
  /// image will cause the avatar to animate to the new image.
104
  ///
105 106
  /// Typically used as a fallback image for [foregroundImage].
  ///
107
  /// If the [CircleAvatar] is to have the user's initials, use [child] instead.
108
  final ImageProvider? backgroundImage;
109

110 111 112 113 114
  /// The foreground image of the circle.
  ///
  /// Typically used as profile image. For fallback use [backgroundImage].
  final ImageProvider? foregroundImage;

115 116
  /// An optional error callback for errors emitted when loading
  /// [backgroundImage].
117
  final ImageErrorListener? onBackgroundImageError;
118

119 120 121 122
  /// An optional error callback for errors emitted when loading
  /// [foregroundImage].
  final ImageErrorListener? onForegroundImageError;

123
  /// The size of the avatar, expressed as the radius (half the diameter).
124
  ///
125 126 127 128
  /// If [radius] is specified, then neither [minRadius] nor [maxRadius] may be
  /// specified. Specifying [radius] is equivalent to specifying a [minRadius]
  /// and [maxRadius], both with the value of [radius].
  ///
129 130 131 132 133 134
  /// If neither [minRadius] nor [maxRadius] are specified, defaults to 20
  /// logical pixels. This is the appropriate size for use with
  /// [ListTile.leading].
  ///
  /// Changes to the [radius] are animated (including changing from an explicit
  /// [radius] to a [minRadius]/[maxRadius] pair or vice versa).
135
  final double? radius;
Adam Barth's avatar
Adam Barth committed
136

137 138
  /// The minimum size of the avatar, expressed as the radius (half the
  /// diameter).
139
  ///
140
  /// If [minRadius] is specified, then [radius] must not also be specified.
141 142
  ///
  /// Defaults to zero.
143 144 145 146 147 148 149 150
  ///
  /// Constraint changes are animated, but size changes due to the environment
  /// itself changing are not. For example, changing the [minRadius] from 10 to
  /// 20 when the [CircleAvatar] is in an unconstrained environment will cause
  /// the avatar to animate from a 20 pixel diameter to a 40 pixel diameter.
  /// However, if the [minRadius] is 40 and the [CircleAvatar] has a parent
  /// [SizedBox] whose size changes instantaneously from 20 pixels to 40 pixels,
  /// the size will snap to 40 pixels instantly.
151
  final double? minRadius;
152

153 154
  /// The maximum size of the avatar, expressed as the radius (half the
  /// diameter).
155
  ///
156
  /// If [maxRadius] is specified, then [radius] must not also be specified.
157 158
  ///
  /// Defaults to [double.infinity].
159 160 161 162 163 164 165 166
  ///
  /// Constraint changes are animated, but size changes due to the environment
  /// itself changing are not. For example, changing the [maxRadius] from 10 to
  /// 20 when the [CircleAvatar] is in an unconstrained environment will cause
  /// the avatar to animate from a 20 pixel diameter to a 40 pixel diameter.
  /// However, if the [maxRadius] is 40 and the [CircleAvatar] has a parent
  /// [SizedBox] whose size changes instantaneously from 20 pixels to 40 pixels,
  /// the size will snap to 40 pixels instantly.
167
  final double? maxRadius;
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191

  // The default radius if nothing is specified.
  static const double _defaultRadius = 20.0;

  // The default min if only the max is specified.
  static const double _defaultMinRadius = 0.0;

  // The default max if only the min is specified.
  static const double _defaultMaxRadius = double.infinity;

  double get _minDiameter {
    if (radius == null && minRadius == null && maxRadius == null) {
      return _defaultRadius * 2.0;
    }
    return 2.0 * (radius ?? minRadius ?? _defaultMinRadius);
  }

  double get _maxDiameter {
    if (radius == null && minRadius == null && maxRadius == null) {
      return _defaultRadius * 2.0;
    }
    return 2.0 * (radius ?? maxRadius ?? _defaultMaxRadius);
  }

192
  @override
Adam Barth's avatar
Adam Barth committed
193
  Widget build(BuildContext context) {
194
    assert(debugCheckHasMediaQuery(context));
195
    final ThemeData theme = Theme.of(context);
196 197
    TextStyle textStyle = theme.primaryTextTheme.subtitle1!.copyWith(color: foregroundColor);
    Color? effectiveBackgroundColor = backgroundColor;
198
    if (effectiveBackgroundColor == null) {
199
      switch (ThemeData.estimateBrightnessForColor(textStyle.color!)) {
200 201 202 203 204 205 206 207
        case Brightness.dark:
          effectiveBackgroundColor = theme.primaryColorLight;
          break;
        case Brightness.light:
          effectiveBackgroundColor = theme.primaryColorDark;
          break;
      }
    } else if (foregroundColor == null) {
208
      switch (ThemeData.estimateBrightnessForColor(backgroundColor!)) {
209
        case Brightness.dark:
210
          textStyle = textStyle.copyWith(color: theme.primaryColorLight);
211 212
          break;
        case Brightness.light:
213
          textStyle = textStyle.copyWith(color: theme.primaryColorDark);
214 215 216
          break;
      }
    }
217 218
    final double minDiameter = _minDiameter;
    final double maxDiameter = _maxDiameter;
219 220
    return AnimatedContainer(
      constraints: BoxConstraints(
221 222 223 224 225
        minHeight: minDiameter,
        minWidth: minDiameter,
        maxWidth: maxDiameter,
        maxHeight: maxDiameter,
      ),
Adam Barth's avatar
Adam Barth committed
226
      duration: kThemeChangeDuration,
227
      decoration: BoxDecoration(
228
        color: effectiveBackgroundColor,
229
        image: backgroundImage != null
230
          ? DecorationImage(
231
              image: backgroundImage!,
232 233 234
              onError: onBackgroundImageError,
              fit: BoxFit.cover,
            )
235
          : null,
236
        shape: BoxShape.circle,
Adam Barth's avatar
Adam Barth committed
237
      ),
238 239 240 241 242 243 244 245 246 247
      foregroundDecoration: foregroundImage != null
          ? BoxDecoration(
              image: DecorationImage(
                image: foregroundImage!,
                onError: onForegroundImageError,
                fit: BoxFit.cover,
              ),
              shape: BoxShape.circle,
            )
          : null,
248 249
      child: child == null
          ? null
250 251
          : Center(
              child: MediaQuery(
252 253
                // Need to ignore the ambient textScaleFactor here so that the
                // text doesn't escape the avatar when the textScaleFactor is large.
254
                data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
255
                child: IconTheme(
256
                  data: theme.iconTheme.copyWith(color: textStyle.color),
257
                  child: DefaultTextStyle(
258
                    style: textStyle,
259
                    child: child!,
260 261 262 263
                  ),
                ),
              ),
            ),
Adam Barth's avatar
Adam Barth committed
264 265 266
    );
  }
}