mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
fixes [Chip widget's avatar padding changing if label text is more than 1 line](https://github.com/flutter/flutter/issues/136892) ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; List<String> strings = [ 'hello good morning', 'hello good morning hello good morning', 'hello good morning hello good morning hello good morning' ]; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( 'avatarBoxConstraints: null \ndeleteIconBoxConstraints: null', textAlign: TextAlign.center), for (String string in strings) Padding( padding: const EdgeInsets.all(8.0), child: RawChip( label: Container( width: 150, color: Colors.amber, child: Text( string, maxLines: 3, overflow: TextOverflow.ellipsis, ), ), avatar: const Icon(Icons.settings), onDeleted: () {}, ), ), ], ), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( 'avatarBoxConstraints: BoxConstraints.tightForFinite() \ndeleteIconBoxConstraints: BoxConstraints.tightForFinite()', textAlign: TextAlign.center), for (String string in strings) Padding( padding: const EdgeInsets.all(8.0), child: RawChip( avatarBoxConstraints: const BoxConstraints.tightForFinite(), deleteIconBoxConstraints: const BoxConstraints.tightForFinite(), label: Container( width: 150, color: Colors.amber, child: Text( string, maxLines: 3, overflow: TextOverflow.ellipsis, ), ), avatar: const Icon(Icons.settings), onDeleted: () {}, ), ), ], ), ], ), ), ), ); } } ``` </details> ### Preview  # Example previews  
359 lines
11 KiB
Dart
359 lines
11 KiB
Dart
// 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/foundation.dart' show clampDouble;
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'chip.dart';
|
|
import 'chip_theme.dart';
|
|
import 'color_scheme.dart';
|
|
import 'colors.dart';
|
|
import 'debug.dart';
|
|
import 'icons.dart';
|
|
import 'material_state.dart';
|
|
import 'text_theme.dart';
|
|
import 'theme.dart';
|
|
import 'theme_data.dart';
|
|
|
|
/// A Material Design input chip.
|
|
///
|
|
/// Input chips represent a complex piece of information, such as an entity
|
|
/// (person, place, or thing) or conversational text, in a compact form.
|
|
///
|
|
/// Input chips can be made selectable by setting [onSelected], deletable by
|
|
/// setting [onDeleted], and pressable like a button with [onPressed]. They have
|
|
/// a [label], and they can have a leading icon (see [avatar]) and a trailing
|
|
/// icon ([deleteIcon]). Colors and padding can be customized.
|
|
///
|
|
/// Requires one of its ancestors to be a [Material] widget.
|
|
///
|
|
/// Input chips work together with other UI elements. They can appear:
|
|
///
|
|
/// * In a [Wrap] widget.
|
|
/// * In a horizontally scrollable list, for example configured such as a
|
|
/// [ListView] with [ListView.scrollDirection] set to [Axis.horizontal].
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This example shows how to create [InputChip]s with [onSelected] and
|
|
/// [onDeleted] callbacks. When the user taps the chip, the chip will be selected.
|
|
/// When the user taps the delete icon, the chip will be deleted.
|
|
///
|
|
/// ** See code in examples/api/lib/material/input_chip/input_chip.0.dart **
|
|
/// {@end-tool}
|
|
///
|
|
///
|
|
/// {@tool dartpad}
|
|
/// The following example shows how to generate [InputChip]s from
|
|
/// user text input. When the user enters a pizza topping in the text field,
|
|
/// the user is presented with a list of suggestions. When selecting one of the
|
|
/// suggestions, an [InputChip] is generated in the text field.
|
|
///
|
|
/// ** See code in examples/api/lib/material/input_chip/input_chip.1.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// ## Material Design 3
|
|
///
|
|
/// [InputChip] can be used for Input chips from Material Design 3.
|
|
/// If [ThemeData.useMaterial3] is true, then [InputChip]
|
|
/// will be styled to match the Material Design 3 specification for Input
|
|
/// chips.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Chip], a chip that displays information and can be deleted.
|
|
/// * [ChoiceChip], allows a single selection from a set of options. Choice
|
|
/// chips contain related descriptive text or categories.
|
|
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
|
|
/// * [ActionChip], represents an action related to primary content.
|
|
/// * [CircleAvatar], which shows images or initials of people.
|
|
/// * [Wrap], A widget that displays its children in multiple horizontal or
|
|
/// vertical runs.
|
|
/// * <https://material.io/design/components/chips.html>
|
|
class InputChip extends StatelessWidget
|
|
implements
|
|
ChipAttributes,
|
|
DeletableChipAttributes,
|
|
SelectableChipAttributes,
|
|
CheckmarkableChipAttributes,
|
|
DisabledChipAttributes,
|
|
TappableChipAttributes {
|
|
/// Creates an [InputChip].
|
|
///
|
|
/// The [onPressed] and [onSelected] callbacks must not both be specified at
|
|
/// the same time. When both [onPressed] and [onSelected] are null, the chip
|
|
/// will be disabled.
|
|
///
|
|
/// The [pressElevation] and [elevation] must be null or non-negative.
|
|
/// Typically, [pressElevation] is greater than [elevation].
|
|
const InputChip({
|
|
super.key,
|
|
this.avatar,
|
|
required this.label,
|
|
this.labelStyle,
|
|
this.labelPadding,
|
|
this.selected = false,
|
|
this.isEnabled = true,
|
|
this.onSelected,
|
|
this.deleteIcon,
|
|
this.onDeleted,
|
|
this.deleteIconColor,
|
|
this.deleteButtonTooltipMessage,
|
|
this.onPressed,
|
|
this.pressElevation,
|
|
this.disabledColor,
|
|
this.selectedColor,
|
|
this.tooltip,
|
|
this.side,
|
|
this.shape,
|
|
this.clipBehavior = Clip.none,
|
|
this.focusNode,
|
|
this.autofocus = false,
|
|
this.color,
|
|
this.backgroundColor,
|
|
this.padding,
|
|
this.visualDensity,
|
|
this.materialTapTargetSize,
|
|
this.elevation,
|
|
this.shadowColor,
|
|
this.surfaceTintColor,
|
|
this.iconTheme,
|
|
this.selectedShadowColor,
|
|
this.showCheckmark,
|
|
this.checkmarkColor,
|
|
this.avatarBorder = const CircleBorder(),
|
|
this.avatarBoxConstraints,
|
|
this.deleteIconBoxConstraints,
|
|
}) : assert(pressElevation == null || pressElevation >= 0.0),
|
|
assert(elevation == null || elevation >= 0.0);
|
|
|
|
@override
|
|
final Widget? avatar;
|
|
@override
|
|
final Widget label;
|
|
@override
|
|
final TextStyle? labelStyle;
|
|
@override
|
|
final EdgeInsetsGeometry? labelPadding;
|
|
@override
|
|
final bool selected;
|
|
@override
|
|
final bool isEnabled;
|
|
@override
|
|
final ValueChanged<bool>? onSelected;
|
|
@override
|
|
final Widget? deleteIcon;
|
|
@override
|
|
final VoidCallback? onDeleted;
|
|
@override
|
|
final Color? deleteIconColor;
|
|
@override
|
|
final String? deleteButtonTooltipMessage;
|
|
@override
|
|
final VoidCallback? onPressed;
|
|
@override
|
|
final double? pressElevation;
|
|
@override
|
|
final Color? disabledColor;
|
|
@override
|
|
final Color? selectedColor;
|
|
@override
|
|
final String? tooltip;
|
|
@override
|
|
final BorderSide? side;
|
|
@override
|
|
final OutlinedBorder? shape;
|
|
@override
|
|
final Clip clipBehavior;
|
|
@override
|
|
final FocusNode? focusNode;
|
|
@override
|
|
final bool autofocus;
|
|
@override
|
|
final MaterialStateProperty<Color?>? color;
|
|
@override
|
|
final Color? backgroundColor;
|
|
@override
|
|
final EdgeInsetsGeometry? padding;
|
|
@override
|
|
final VisualDensity? visualDensity;
|
|
@override
|
|
final MaterialTapTargetSize? materialTapTargetSize;
|
|
@override
|
|
final double? elevation;
|
|
@override
|
|
final Color? shadowColor;
|
|
@override
|
|
final Color? surfaceTintColor;
|
|
@override
|
|
final Color? selectedShadowColor;
|
|
@override
|
|
final bool? showCheckmark;
|
|
@override
|
|
final Color? checkmarkColor;
|
|
@override
|
|
final ShapeBorder avatarBorder;
|
|
@override
|
|
final IconThemeData? iconTheme;
|
|
@override
|
|
final BoxConstraints? avatarBoxConstraints;
|
|
@override
|
|
final BoxConstraints? deleteIconBoxConstraints;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
assert(debugCheckHasMaterial(context));
|
|
final ChipThemeData? defaults = Theme.of(context).useMaterial3
|
|
? _InputChipDefaultsM3(context, isEnabled, selected)
|
|
: null;
|
|
final Widget? resolvedDeleteIcon = deleteIcon
|
|
?? (Theme.of(context).useMaterial3 ? const Icon(Icons.clear, size: 18) : null);
|
|
return RawChip(
|
|
defaultProperties: defaults,
|
|
avatar: avatar,
|
|
label: label,
|
|
labelStyle: labelStyle,
|
|
labelPadding: labelPadding,
|
|
deleteIcon: resolvedDeleteIcon,
|
|
onDeleted: onDeleted,
|
|
deleteIconColor: deleteIconColor,
|
|
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
|
|
onSelected: onSelected,
|
|
onPressed: onPressed,
|
|
pressElevation: pressElevation,
|
|
selected: selected,
|
|
disabledColor: disabledColor,
|
|
selectedColor: selectedColor,
|
|
tooltip: tooltip,
|
|
side: side,
|
|
shape: shape,
|
|
clipBehavior: clipBehavior,
|
|
focusNode: focusNode,
|
|
autofocus: autofocus,
|
|
color: color,
|
|
backgroundColor: backgroundColor,
|
|
padding: padding,
|
|
visualDensity: visualDensity,
|
|
materialTapTargetSize: materialTapTargetSize,
|
|
elevation: elevation,
|
|
shadowColor: shadowColor,
|
|
surfaceTintColor: surfaceTintColor,
|
|
selectedShadowColor: selectedShadowColor,
|
|
showCheckmark: showCheckmark,
|
|
checkmarkColor: checkmarkColor,
|
|
isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null),
|
|
avatarBorder: avatarBorder,
|
|
iconTheme: iconTheme,
|
|
avatarBoxConstraints: avatarBoxConstraints,
|
|
deleteIconBoxConstraints: deleteIconBoxConstraints,
|
|
);
|
|
}
|
|
}
|
|
|
|
// BEGIN GENERATED TOKEN PROPERTIES - InputChip
|
|
|
|
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
|
// "END GENERATED" comments are generated from data in the Material
|
|
// Design token database by the script:
|
|
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
|
|
|
class _InputChipDefaultsM3 extends ChipThemeData {
|
|
_InputChipDefaultsM3(this.context, this.isEnabled, this.isSelected)
|
|
: super(
|
|
elevation: 0.0,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
|
|
showCheckmark: true,
|
|
);
|
|
|
|
final BuildContext context;
|
|
final bool isEnabled;
|
|
final bool isSelected;
|
|
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
|
late final TextTheme _textTheme = Theme.of(context).textTheme;
|
|
|
|
@override
|
|
TextStyle? get labelStyle => _textTheme.labelLarge?.copyWith(
|
|
color: isEnabled
|
|
? isSelected
|
|
? _colors.onSecondaryContainer
|
|
: _colors.onSurfaceVariant
|
|
: _colors.onSurface,
|
|
);
|
|
|
|
@override
|
|
MaterialStateProperty<Color?>? get color =>
|
|
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
|
if (states.contains(MaterialState.selected) && states.contains(MaterialState.disabled)) {
|
|
return _colors.onSurface.withOpacity(0.12);
|
|
}
|
|
if (states.contains(MaterialState.disabled)) {
|
|
return null;
|
|
}
|
|
if (states.contains(MaterialState.selected)) {
|
|
return _colors.secondaryContainer;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
@override
|
|
Color? get shadowColor => Colors.transparent;
|
|
|
|
@override
|
|
Color? get surfaceTintColor => Colors.transparent;
|
|
|
|
@override
|
|
Color? get checkmarkColor => isEnabled
|
|
? isSelected
|
|
? _colors.primary
|
|
: _colors.onSurfaceVariant
|
|
: _colors.onSurface;
|
|
|
|
@override
|
|
Color? get deleteIconColor => isEnabled
|
|
? isSelected
|
|
? _colors.onSecondaryContainer
|
|
: _colors.onSurfaceVariant
|
|
: _colors.onSurface;
|
|
|
|
@override
|
|
BorderSide? get side => !isSelected
|
|
? isEnabled
|
|
? BorderSide(color: _colors.outline)
|
|
: BorderSide(color: _colors.onSurface.withOpacity(0.12))
|
|
: const BorderSide(color: Colors.transparent);
|
|
|
|
@override
|
|
IconThemeData? get iconTheme => IconThemeData(
|
|
color: isEnabled
|
|
? isSelected
|
|
? _colors.primary
|
|
: _colors.onSurfaceVariant
|
|
: _colors.onSurface,
|
|
size: 18.0,
|
|
);
|
|
|
|
@override
|
|
EdgeInsetsGeometry? get padding => const EdgeInsets.all(8.0);
|
|
|
|
/// The label padding of the chip scales with the font size specified in the
|
|
/// [labelStyle], and the system font size settings that scale font sizes
|
|
/// globally.
|
|
///
|
|
/// The chip at effective font size 14.0 starts with 8px on each side and as
|
|
/// the font size scales up to closer to 28.0, the label padding is linearly
|
|
/// interpolated from 8px to 4px. Once the label has a font size of 2 or
|
|
/// higher, label padding remains 4px.
|
|
@override
|
|
EdgeInsetsGeometry? get labelPadding {
|
|
final double fontSize = labelStyle?.fontSize ?? 14.0;
|
|
final double fontSizeRatio = MediaQuery.textScalerOf(context).scale(fontSize) / 14.0;
|
|
return EdgeInsets.lerp(
|
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
|
const EdgeInsets.symmetric(horizontal: 4.0),
|
|
clampDouble(fontSizeRatio - 1.0, 0.0, 1.0),
|
|
)!;
|
|
}
|
|
}
|
|
|
|
// END GENERATED TOKEN PROPERTIES - InputChip
|