blob: ff06621fb9ce2e576fec3a938d4a779f00e62799 [file] [log] [blame]
// 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,
),
),
),
),
);
}
}