| // Copyright 2014 The Flutter 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/widgets.dart'; |
| |
| import 'constants.dart'; |
| import 'theme.dart'; |
| import 'theme_data.dart'; |
| |
| // Examples can assume: |
| // String userAvatarUrl; |
| |
| /// A circle that represents a user. |
| /// |
| /// Typically used with a user's profile image, or, in the absence of |
| /// such an image, the user's initials. A given user's initials should |
| /// always be paired with the same background color, for consistency. |
| /// |
| /// The [onBackgroundImageError] parameter must be null if the [backgroundImage] |
| /// is null. |
| /// |
| /// {@tool snippet} |
| /// |
| /// If the avatar is to have an image, the image should be specified in the |
| /// [backgroundImage] property: |
| /// |
| /// ```dart |
| /// CircleAvatar( |
| /// backgroundImage: NetworkImage(userAvatarUrl), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// The image will be cropped to have a circle shape. |
| /// |
| /// {@tool snippet} |
| /// |
| /// 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 |
| /// CircleAvatar( |
| /// backgroundColor: Colors.brown.shade800, |
| /// child: Text('AH'), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [Chip], for representing users or concepts in long form. |
| /// * [ListTile], which can combine an icon (such as a [CircleAvatar]) with |
| /// some text for a fixed height list entry. |
| /// * <https://material.io/design/components/chips.html#input-chips> |
| class CircleAvatar extends StatelessWidget { |
| /// Creates a circle that represents a user. |
| const CircleAvatar({ |
| Key key, |
| this.child, |
| this.backgroundColor, |
| this.backgroundImage, |
| this.onBackgroundImageError, |
| this.foregroundColor, |
| this.radius, |
| this.minRadius, |
| this.maxRadius, |
| }) : assert(radius == null || (minRadius == null && maxRadius == null)), |
| assert(backgroundImage != null || onBackgroundImageError == null), |
| super(key: key); |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// Typically a [Text] widget. If the [CircleAvatar] is to have an image, use |
| /// [backgroundImage] instead. |
| final Widget child; |
| |
| /// The color with which to fill the circle. Changing the background |
| /// color will cause the avatar to animate to the new color. |
| /// |
| /// If a [backgroundColor] is not specified, the theme's |
| /// [ThemeData.primaryColorLight] is used with dark foreground colors, and |
| /// [ThemeData.primaryColorDark] with light foreground colors. |
| final Color backgroundColor; |
| |
| /// The default text color for text in the circle. |
| /// |
| /// 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. |
| final Color foregroundColor; |
| |
| /// The background image of the circle. Changing the background |
| /// image will cause the avatar to animate to the new image. |
| /// |
| /// If the [CircleAvatar] is to have the user's initials, use [child] instead. |
| final ImageProvider backgroundImage; |
| |
| /// An optional error callback for errors emitted when loading |
| /// [backgroundImage]. |
| final ImageErrorListener onBackgroundImageError; |
| |
| /// The size of the avatar, expressed as the radius (half the diameter). |
| /// |
| /// 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]. |
| /// |
| /// 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). |
| final double radius; |
| |
| /// The minimum size of the avatar, expressed as the radius (half the |
| /// diameter). |
| /// |
| /// If [minRadius] is specified, then [radius] must not also be specified. |
| /// |
| /// Defaults to zero. |
| /// |
| /// 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. |
| final double minRadius; |
| |
| /// The maximum size of the avatar, expressed as the radius (half the |
| /// diameter). |
| /// |
| /// If [maxRadius] is specified, then [radius] must not also be specified. |
| /// |
| /// Defaults to [double.infinity]. |
| /// |
| /// 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. |
| final double maxRadius; |
| |
| // 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); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMediaQuery(context)); |
| final ThemeData theme = Theme.of(context); |
| TextStyle textStyle = theme.primaryTextTheme.subtitle1.copyWith(color: foregroundColor); |
| Color effectiveBackgroundColor = backgroundColor; |
| if (effectiveBackgroundColor == null) { |
| switch (ThemeData.estimateBrightnessForColor(textStyle.color)) { |
| case Brightness.dark: |
| effectiveBackgroundColor = theme.primaryColorLight; |
| break; |
| case Brightness.light: |
| effectiveBackgroundColor = theme.primaryColorDark; |
| break; |
| } |
| } else if (foregroundColor == null) { |
| switch (ThemeData.estimateBrightnessForColor(backgroundColor)) { |
| case Brightness.dark: |
| textStyle = textStyle.copyWith(color: theme.primaryColorLight); |
| break; |
| case Brightness.light: |
| textStyle = textStyle.copyWith(color: theme.primaryColorDark); |
| break; |
| } |
| } |
| final double minDiameter = _minDiameter; |
| final double maxDiameter = _maxDiameter; |
| return AnimatedContainer( |
| constraints: BoxConstraints( |
| minHeight: minDiameter, |
| minWidth: minDiameter, |
| maxWidth: maxDiameter, |
| maxHeight: maxDiameter, |
| ), |
| duration: kThemeChangeDuration, |
| decoration: BoxDecoration( |
| color: effectiveBackgroundColor, |
| image: backgroundImage != null |
| ? DecorationImage( |
| image: backgroundImage, |
| onError: onBackgroundImageError, |
| fit: BoxFit.cover, |
| ) |
| : null, |
| shape: BoxShape.circle, |
| ), |
| child: child == null |
| ? null |
| : Center( |
| child: MediaQuery( |
| // Need to ignore the ambient textScaleFactor here so that the |
| // text doesn't escape the avatar when the textScaleFactor is large. |
| data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), |
| child: IconTheme( |
| data: theme.iconTheme.copyWith(color: textStyle.color), |
| child: DefaultTextStyle( |
| style: textStyle, |
| child: child, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |