| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'package:flutter/material.dart'; |
| 6 | /// |
| 7 | /// @docImport 'app.dart'; |
| 8 | /// @docImport 'image_icon.dart'; |
| 9 | library; |
| 10 | |
| 11 | import 'package:flutter/foundation.dart'; |
| 12 | import 'package:flutter/rendering.dart'; |
| 13 | |
| 14 | import 'basic.dart'; |
| 15 | import 'debug.dart'; |
| 16 | import 'framework.dart'; |
| 17 | import 'icon_data.dart'; |
| 18 | import 'icon_theme.dart'; |
| 19 | import 'icon_theme_data.dart'; |
| 20 | import 'media_query.dart'; |
| 21 | |
| 22 | /// A graphical icon widget drawn with a glyph from a font described in |
| 23 | /// an [IconData] such as material's predefined [IconData]s in [Icons]. |
| 24 | /// |
| 25 | /// Icons are not interactive. For an interactive icon, consider material's |
| 26 | /// [IconButton]. |
| 27 | /// |
| 28 | /// There must be an ambient [Directionality] widget when using [Icon]. |
| 29 | /// Typically this is introduced automatically by the [WidgetsApp] or |
| 30 | /// [MaterialApp]. |
| 31 | /// |
| 32 | /// This widget assumes that the rendered icon is squared. Non-squared icons may |
| 33 | /// render incorrectly. |
| 34 | /// |
| 35 | /// {@tool snippet} |
| 36 | /// |
| 37 | /// This example shows how to create a [Row] of [Icon]s in different colors and |
| 38 | /// sizes. The first [Icon] uses a [semanticLabel] to announce in accessibility |
| 39 | /// modes like TalkBack and VoiceOver. |
| 40 | /// |
| 41 | ///  |
| 42 | /// |
| 43 | /// ```dart |
| 44 | /// const Row( |
| 45 | /// mainAxisAlignment: MainAxisAlignment.spaceAround, |
| 46 | /// children: <Widget>[ |
| 47 | /// Icon( |
| 48 | /// Icons.favorite, |
| 49 | /// color: Colors.pink, |
| 50 | /// size: 24.0, |
| 51 | /// semanticLabel: 'Text to announce in accessibility modes', |
| 52 | /// ), |
| 53 | /// Icon( |
| 54 | /// Icons.audiotrack, |
| 55 | /// color: Colors.green, |
| 56 | /// size: 30.0, |
| 57 | /// ), |
| 58 | /// Icon( |
| 59 | /// Icons.beach_access, |
| 60 | /// color: Colors.blue, |
| 61 | /// size: 36.0, |
| 62 | /// ), |
| 63 | /// ], |
| 64 | /// ) |
| 65 | /// ``` |
| 66 | /// {@end-tool} |
| 67 | /// |
| 68 | /// See also: |
| 69 | /// |
| 70 | /// * [IconButton], for interactive icons. |
| 71 | /// * [Icons], for the list of available Material Icons for use with this class. |
| 72 | /// * [IconTheme], which provides ambient configuration for icons. |
| 73 | /// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s. |
| 74 | class Icon extends StatelessWidget { |
| 75 | /// Creates an icon. |
| 76 | const Icon( |
| 77 | this.icon, { |
| 78 | super.key, |
| 79 | this.size, |
| 80 | this.fill, |
| 81 | this.weight, |
| 82 | this.grade, |
| 83 | this.opticalSize, |
| 84 | this.color, |
| 85 | this.shadows, |
| 86 | this.semanticLabel, |
| 87 | this.textDirection, |
| 88 | this.applyTextScaling, |
| 89 | this.blendMode, |
| 90 | this.fontWeight, |
| 91 | }) : assert(fill == null || (0.0 <= fill && fill <= 1.0)), |
| 92 | assert(weight == null || (0.0 < weight)), |
| 93 | assert(opticalSize == null || (0.0 < opticalSize)); |
| 94 | |
| 95 | /// The icon to display. The available icons are described in [Icons]. |
| 96 | /// |
| 97 | /// The icon can be null, in which case the widget will render as an empty |
| 98 | /// space of the specified [size]. |
| 99 | final IconData? icon; |
| 100 | |
| 101 | /// The size of the icon in logical pixels. |
| 102 | /// |
| 103 | /// Icons occupy a square with width and height equal to size. |
| 104 | /// |
| 105 | /// Defaults to the nearest [IconTheme]'s [IconThemeData.size]. |
| 106 | /// |
| 107 | /// If this [Icon] is being placed inside an [IconButton], then use |
| 108 | /// [IconButton.iconSize] instead, so that the [IconButton] can make the splash |
| 109 | /// area the appropriate size as well. The [IconButton] uses an [IconTheme] to |
| 110 | /// pass down the size to the [Icon]. |
| 111 | final double? size; |
| 112 | |
| 113 | /// The fill for drawing the icon. |
| 114 | /// |
| 115 | /// Requires the underlying icon font to support the `FILL` [FontVariation] |
| 116 | /// axis, otherwise has no effect. Variable font filenames often indicate |
| 117 | /// the supported axes. Must be between 0.0 (unfilled) and 1.0 (filled), |
| 118 | /// inclusive. |
| 119 | /// |
| 120 | /// Can be used to convey a state transition for animation or interaction. |
| 121 | /// |
| 122 | /// Defaults to nearest [IconTheme]'s [IconThemeData.fill]. |
| 123 | /// |
| 124 | /// See also: |
| 125 | /// * [weight], for controlling stroke weight. |
| 126 | /// * [grade], for controlling stroke weight in a more granular way. |
| 127 | /// * [opticalSize], for controlling optical size. |
| 128 | final double? fill; |
| 129 | |
| 130 | /// The stroke weight for drawing the icon. |
| 131 | /// |
| 132 | /// Requires the underlying icon font to support the `wght` [FontVariation] |
| 133 | /// axis, otherwise has no effect. Variable font filenames often indicate |
| 134 | /// the supported axes. Must be greater than 0. |
| 135 | /// |
| 136 | /// Defaults to nearest [IconTheme]'s [IconThemeData.weight]. |
| 137 | /// |
| 138 | /// See also: |
| 139 | /// * [fill], for controlling fill. |
| 140 | /// * [grade], for controlling stroke weight in a more granular way. |
| 141 | /// * [opticalSize], for controlling optical size. |
| 142 | /// * https://fonts.google.com/knowledge/glossary/weight_axis |
| 143 | final double? weight; |
| 144 | |
| 145 | /// The grade (granular stroke weight) for drawing the icon. |
| 146 | /// |
| 147 | /// Requires the underlying icon font to support the `GRAD` [FontVariation] |
| 148 | /// axis, otherwise has no effect. Variable font filenames often indicate |
| 149 | /// the supported axes. Can be negative. |
| 150 | /// |
| 151 | /// Grade and [weight] both affect a symbol's stroke weight (thickness), but |
| 152 | /// grade has a smaller impact on the size of the symbol. |
| 153 | /// |
| 154 | /// Grade is also available in some text fonts. One can match grade levels |
| 155 | /// between text and symbols for a harmonious visual effect. For example, if |
| 156 | /// the text font has a -25 grade value, the symbols can match it with a |
| 157 | /// suitable value, say -25. |
| 158 | /// |
| 159 | /// Defaults to nearest [IconTheme]'s [IconThemeData.grade]. |
| 160 | /// |
| 161 | /// See also: |
| 162 | /// * [fill], for controlling fill. |
| 163 | /// * [weight], for controlling stroke weight in a less granular way. |
| 164 | /// * [opticalSize], for controlling optical size. |
| 165 | /// * https://fonts.google.com/knowledge/glossary/grade_axis |
| 166 | final double? grade; |
| 167 | |
| 168 | /// The optical size for drawing the icon. |
| 169 | /// |
| 170 | /// Requires the underlying icon font to support the `opsz` [FontVariation] |
| 171 | /// axis, otherwise has no effect. Variable font filenames often indicate |
| 172 | /// the supported axes. Must be greater than 0. |
| 173 | /// |
| 174 | /// For an icon to look the same at different sizes, the stroke weight |
| 175 | /// (thickness) must change as the icon size scales. Optical size offers a way |
| 176 | /// to automatically adjust the stroke weight as icon size changes. |
| 177 | /// |
| 178 | /// Defaults to nearest [IconTheme]'s [IconThemeData.opticalSize]. |
| 179 | /// |
| 180 | /// See also: |
| 181 | /// * [fill], for controlling fill. |
| 182 | /// * [weight], for controlling stroke weight. |
| 183 | /// * [grade], for controlling stroke weight in a more granular way. |
| 184 | /// * https://fonts.google.com/knowledge/glossary/optical_size_axis |
| 185 | final double? opticalSize; |
| 186 | |
| 187 | /// The color to use when drawing the icon. |
| 188 | /// |
| 189 | /// Defaults to the nearest [IconTheme]'s [IconThemeData.color]. |
| 190 | /// |
| 191 | /// The color (whether specified explicitly here or obtained from the |
| 192 | /// [IconTheme]) will be further adjusted by the nearest [IconTheme]'s |
| 193 | /// [IconThemeData.opacity]. |
| 194 | /// |
| 195 | /// {@tool snippet} |
| 196 | /// Typically, a Material Design color will be used, as follows: |
| 197 | /// |
| 198 | /// ```dart |
| 199 | /// Icon( |
| 200 | /// Icons.widgets, |
| 201 | /// color: Colors.blue.shade400, |
| 202 | /// ) |
| 203 | /// ``` |
| 204 | /// {@end-tool} |
| 205 | final Color? color; |
| 206 | |
| 207 | /// A list of [Shadow]s that will be painted underneath the icon. |
| 208 | /// |
| 209 | /// Multiple shadows are supported to replicate lighting from multiple light |
| 210 | /// sources. |
| 211 | /// |
| 212 | /// Shadows must be in the same order for [Icon] to be considered as |
| 213 | /// equivalent as order produces differing transparency. |
| 214 | /// |
| 215 | /// Defaults to the nearest [IconTheme]'s [IconThemeData.shadows]. |
| 216 | final List<Shadow>? shadows; |
| 217 | |
| 218 | /// Semantic label for the icon. |
| 219 | /// |
| 220 | /// Announced by assistive technologies (e.g TalkBack/VoiceOver). |
| 221 | /// This label does not show in the UI. |
| 222 | /// |
| 223 | /// * [SemanticsProperties.label], which is set to [semanticLabel] in the |
| 224 | /// underlying [Semantics] widget. |
| 225 | final String? semanticLabel; |
| 226 | |
| 227 | /// The text direction to use for rendering the icon. |
| 228 | /// |
| 229 | /// If this is null, the ambient [Directionality] is used instead. |
| 230 | /// |
| 231 | /// Some icons follow the reading direction. For example, "back" buttons point |
| 232 | /// left in left-to-right environments and right in right-to-left |
| 233 | /// environments. Such icons have their [IconData.matchTextDirection] field |
| 234 | /// set to true, and the [Icon] widget uses the [textDirection] to determine |
| 235 | /// the orientation in which to draw the icon. |
| 236 | /// |
| 237 | /// This property has no effect if the [icon]'s [IconData.matchTextDirection] |
| 238 | /// field is false, but for consistency a text direction value must always be |
| 239 | /// specified, either directly using this property or using [Directionality]. |
| 240 | final TextDirection? textDirection; |
| 241 | |
| 242 | /// Whether to scale the size of this widget using the ambient [MediaQuery]'s [TextScaler]. |
| 243 | /// |
| 244 | /// This is specially useful when you have an icon associated with a text, as |
| 245 | /// scaling the text without scaling the icon would result in a confusing |
| 246 | /// interface. |
| 247 | /// |
| 248 | /// Defaults to the nearest [IconTheme]'s |
| 249 | /// [IconThemeData.applyTextScaling]. |
| 250 | final bool? applyTextScaling; |
| 251 | |
| 252 | /// The [BlendMode] to apply to the foreground of the icon. |
| 253 | /// |
| 254 | /// Defaults to [BlendMode.srcOver] |
| 255 | final BlendMode? blendMode; |
| 256 | |
| 257 | /// The typeface thickness to use when painting the text (e.g., bold). |
| 258 | final FontWeight? fontWeight; |
| 259 | |
| 260 | @override |
| 261 | Widget build(BuildContext context) { |
| 262 | assert(this.textDirection != null || debugCheckHasDirectionality(context)); |
| 263 | final TextDirection textDirection = this.textDirection ?? Directionality.of(context); |
| 264 | |
| 265 | final IconThemeData iconTheme = IconTheme.of(context); |
| 266 | |
| 267 | final bool applyTextScaling = this.applyTextScaling ?? iconTheme.applyTextScaling ?? false; |
| 268 | |
| 269 | final double tentativeIconSize = size ?? iconTheme.size ?? kDefaultFontSize; |
| 270 | |
| 271 | final double iconSize = applyTextScaling |
| 272 | ? MediaQuery.textScalerOf(context).scale(tentativeIconSize) |
| 273 | : tentativeIconSize; |
| 274 | |
| 275 | final double? iconFill = fill ?? iconTheme.fill; |
| 276 | |
| 277 | final double? iconWeight = weight ?? iconTheme.weight; |
| 278 | |
| 279 | final double? iconGrade = grade ?? iconTheme.grade; |
| 280 | |
| 281 | final double? iconOpticalSize = opticalSize ?? iconTheme.opticalSize; |
| 282 | |
| 283 | final List<Shadow>? iconShadows = shadows ?? iconTheme.shadows; |
| 284 | |
| 285 | final IconData? icon = this.icon; |
| 286 | if (icon == null) { |
| 287 | return Semantics( |
| 288 | label: semanticLabel, |
| 289 | child: SizedBox(width: iconSize, height: iconSize), |
| 290 | ); |
| 291 | } |
| 292 | |
| 293 | final double iconOpacity = iconTheme.opacity ?? 1.0; |
| 294 | Color? iconColor = color ?? iconTheme.color!; |
| 295 | Paint? foreground; |
| 296 | if (iconOpacity != 1.0) { |
| 297 | iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity); |
| 298 | } |
| 299 | if (blendMode != null) { |
| 300 | foreground = Paint() |
| 301 | ..blendMode = blendMode! |
| 302 | ..color = iconColor; |
| 303 | // Cannot provide both a color and a foreground. |
| 304 | iconColor = null; |
| 305 | } |
| 306 | |
| 307 | final TextStyle fontStyle = TextStyle( |
| 308 | fontVariations: <FontVariation>[ |
| 309 | if (iconFill != null) FontVariation('FILL' , iconFill), |
| 310 | if (iconWeight != null) FontVariation('wght' , iconWeight), |
| 311 | if (iconGrade != null) FontVariation('GRAD' , iconGrade), |
| 312 | if (iconOpticalSize != null) FontVariation('opsz' , iconOpticalSize), |
| 313 | ], |
| 314 | inherit: false, |
| 315 | color: iconColor, |
| 316 | fontSize: iconSize, |
| 317 | fontFamily: icon.fontFamily, |
| 318 | fontWeight: fontWeight, |
| 319 | package: icon.fontPackage, |
| 320 | fontFamilyFallback: icon.fontFamilyFallback, |
| 321 | shadows: iconShadows, |
| 322 | height: |
| 323 | 1.0, // Makes sure the font's body is vertically centered within the iconSize x iconSize square. |
| 324 | leadingDistribution: TextLeadingDistribution.even, |
| 325 | foreground: foreground, |
| 326 | ); |
| 327 | |
| 328 | Widget iconWidget = RichText( |
| 329 | overflow: TextOverflow.visible, // Never clip. |
| 330 | textDirection: textDirection, // Since we already fetched it for the assert... |
| 331 | text: TextSpan(text: String.fromCharCode(icon.codePoint), style: fontStyle), |
| 332 | ); |
| 333 | |
| 334 | if (icon.matchTextDirection) { |
| 335 | switch (textDirection) { |
| 336 | case TextDirection.rtl: |
| 337 | iconWidget = Transform( |
| 338 | transform: Matrix4.identity()..scaleByDouble(-1.0, 1.0, 1.0, 1), |
| 339 | alignment: Alignment.center, |
| 340 | transformHitTests: false, |
| 341 | child: iconWidget, |
| 342 | ); |
| 343 | case TextDirection.ltr: |
| 344 | break; |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | return Semantics( |
| 349 | label: semanticLabel, |
| 350 | child: ExcludeSemantics( |
| 351 | child: SizedBox( |
| 352 | width: iconSize, |
| 353 | height: iconSize, |
| 354 | child: Center(child: iconWidget), |
| 355 | ), |
| 356 | ), |
| 357 | ); |
| 358 | } |
| 359 | |
| 360 | @override |
| 361 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 362 | super.debugFillProperties(properties); |
| 363 | properties.add(IconDataProperty('icon' , icon, ifNull: '<empty>' , showName: false)); |
| 364 | properties.add(DoubleProperty('size' , size, defaultValue: null)); |
| 365 | properties.add(DoubleProperty('fill' , fill, defaultValue: null)); |
| 366 | properties.add(DoubleProperty('weight' , weight, defaultValue: null)); |
| 367 | properties.add(DoubleProperty('grade' , grade, defaultValue: null)); |
| 368 | properties.add(DoubleProperty('opticalSize' , opticalSize, defaultValue: null)); |
| 369 | properties.add(ColorProperty('color' , color, defaultValue: null)); |
| 370 | properties.add(IterableProperty<Shadow>('shadows' , shadows, defaultValue: null)); |
| 371 | properties.add(StringProperty('semanticLabel' , semanticLabel, defaultValue: null)); |
| 372 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
| 373 | properties.add( |
| 374 | DiagnosticsProperty<bool>('applyTextScaling' , applyTextScaling, defaultValue: null), |
| 375 | ); |
| 376 | } |
| 377 | } |
| 378 | |