| // 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/material.dart'; |
| |
| /// Flutter code sample for [TextButton]. |
| |
| void main() { |
| runApp(const TextButtonExampleApp()); |
| } |
| |
| class TextButtonExampleApp extends StatefulWidget { |
| const TextButtonExampleApp({ super.key }); |
| |
| @override |
| State<TextButtonExampleApp> createState() => _TextButtonExampleAppState(); |
| } |
| |
| class _TextButtonExampleAppState extends State<TextButtonExampleApp> { |
| bool darkMode = false; |
| |
| @override |
| Widget build(BuildContext context) { |
| return MaterialApp( |
| themeMode: darkMode ? ThemeMode.dark : ThemeMode.light, |
| theme: ThemeData(brightness: Brightness.light), |
| darkTheme: ThemeData(brightness: Brightness.dark), |
| home: Scaffold( |
| body: Padding( |
| padding: const EdgeInsets.all(16), |
| child: TextButtonExample( |
| darkMode: darkMode, |
| updateDarkMode: (bool value) { |
| setState(() { darkMode = value; }); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class TextButtonExample extends StatefulWidget { |
| const TextButtonExample({ super.key, required this.darkMode, required this.updateDarkMode }); |
| |
| final bool darkMode; |
| final ValueChanged<bool> updateDarkMode; |
| |
| @override |
| State<TextButtonExample> createState() => _TextButtonExampleState(); |
| } |
| |
| class _TextButtonExampleState extends State<TextButtonExample> { |
| TextDirection textDirection = TextDirection.ltr; |
| ThemeMode themeMode = ThemeMode.light; |
| late final ScrollController scrollController; |
| Future<void>? currentAction; |
| |
| static const Widget verticalSpacer = SizedBox(height: 16); |
| static const Widget horizontalSpacer = SizedBox(width: 32); |
| |
| static const ImageProvider grassImage = NetworkImage( |
| 'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_grass.jpeg', |
| ); |
| static const ImageProvider defaultImage = NetworkImage( |
| 'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_default.png', |
| ); |
| static const ImageProvider hoveredImage = NetworkImage( |
| 'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_hovered.png', |
| ); |
| static const ImageProvider pressedImage = NetworkImage( |
| 'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_pressed.png', |
| ); |
| static const ImageProvider runningImage = NetworkImage( |
| 'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_end.png', |
| ); |
| |
| @override |
| void initState() { |
| scrollController = ScrollController(); |
| super.initState(); |
| } |
| |
| @override |
| void dispose() { |
| scrollController.dispose(); |
| super.dispose(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData theme = Theme.of(context); |
| final ColorScheme colorScheme = theme.colorScheme; |
| |
| // Adapt colors that are not part of the color scheme to |
| // the current dark/light mode. Used to define TextButton #7's |
| // gradients. |
| final (Color color1, Color color2, Color color3) = switch (colorScheme.brightness) { |
| Brightness.light => (Colors.blue.withOpacity(1.0), Colors.orange.withOpacity(1.0), Colors.yellow.withOpacity(1.0)), |
| Brightness.dark => (Colors.purple.withOpacity(1.0), Colors.cyan.withOpacity(1.0), Colors.yellow.withOpacity(1.0)), |
| }; |
| |
| // This gradient's appearance reflects the button's state. |
| // Always return a gradient decoration so that AnimatedContainer |
| // can interpolorate in between. Used by TextButton #7. |
| Decoration? statesToDecoration(Set<MaterialState> states) { |
| if (states.contains(MaterialState.pressed)) { |
| return BoxDecoration( |
| gradient: LinearGradient(colors: <Color>[color2, color2]), // solid fill |
| ); |
| } |
| return BoxDecoration( |
| gradient: LinearGradient( |
| colors: switch (states.contains(MaterialState.hovered)) { |
| true => <Color>[color1, color2], |
| false => <Color>[color2, color1], |
| }, |
| ), |
| ); |
| } |
| |
| // To make this method a little easier to read, the buttons that |
| // appear in the two columns to the right of the demo switches |
| // Card are broken out below. |
| |
| final List<Widget> columnOneButtons = <Widget>[ |
| TextButton( |
| onPressed: () {}, |
| child: const Text('Enabled'), |
| ), |
| verticalSpacer, |
| |
| const TextButton( |
| onPressed: null, |
| child: Text('Disabled'), |
| ), |
| verticalSpacer, |
| |
| TextButton.icon( |
| onPressed: () {}, |
| icon: const Icon(Icons.access_alarm), |
| label: const Text('TextButton.icon #1'), |
| ), |
| verticalSpacer, |
| |
| // Override the foreground and background colors. |
| // |
| // In this example, and most of the ones that follow, we're using |
| // the TextButton.styleFrom() convenience method to create a ButtonStyle. |
| // The styleFrom method is a little easier because it creates |
| // ButtonStyle MaterialStateProperty parameters for you. |
| // In this case, Specifying foregroundColor overrides the text, |
| // icon and overlay (splash and highlight) colors a little differently |
| // depending on the button's state. BackgroundColor is just the background |
| // color for all states. |
| TextButton.icon( |
| style: TextButton.styleFrom( |
| foregroundColor: colorScheme.onError, |
| backgroundColor: colorScheme.error, |
| ), |
| onPressed: () { }, |
| icon: const Icon(Icons.access_alarm), |
| label: const Text('TextButton.icon #2'), |
| ), |
| verticalSpacer, |
| |
| // Override the button's shape and its border. |
| // |
| // In this case we've specified a shape that has border - the |
| // RoundedRectangleBorder's side parameter. If the styleFrom |
| // side parameter was also specified, or if the TextButtonTheme |
| // defined above included a side parameter, then that would |
| // override the RoundedRectangleBorder's side. |
| TextButton( |
| style: TextButton.styleFrom( |
| shape: RoundedRectangleBorder( |
| borderRadius: const BorderRadius.all(Radius.circular(8)), |
| side: BorderSide( |
| color: colorScheme.primary, |
| width: 5, |
| ), |
| ), |
| ), |
| onPressed: () { }, |
| child: const Text('TextButton #3'), |
| ), |
| verticalSpacer, |
| |
| // Override overlay: the ink splash and highlight colors. |
| // |
| // The styleFrom method turns the specified overlayColor |
| // into a value MaterialStyleProperty<Color> ButtonStyle.overlay |
| // value that uses opacities depending on the button's state. |
| // If the overlayColor was Colors.transparent, no splash |
| // or highlights would be shown. |
| TextButton( |
| style: TextButton.styleFrom( |
| overlayColor: Colors.yellow, |
| ), |
| onPressed: () { }, |
| child: const Text('TextButton #4'), |
| ), |
| ]; |
| |
| final List<Widget> columnTwoButtons = <Widget>[ |
| // Override the foregroundBuilder: apply a ShaderMask. |
| // |
| // Apply a ShaderMask to the button's child. This kind of thing |
| // can be applied to one button easily enough by just wrapping the |
| // button's child directly. However to affect all buttons in this |
| // way you can specify a similar foregroundBuilder in a TextButton |
| // theme or the MaterialApp theme's ThemeData.textButtonTheme. |
| TextButton( |
| style: TextButton.styleFrom( |
| foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) { |
| return ShaderMask( |
| shaderCallback: (Rect bounds) { |
| return LinearGradient( |
| begin: Alignment.bottomCenter, |
| end: Alignment.topCenter, |
| colors: <Color>[ |
| colorScheme.primary, |
| colorScheme.onPrimary, |
| ], |
| ).createShader(bounds); |
| }, |
| blendMode: BlendMode.srcATop, |
| child: child, |
| ); |
| }, |
| ), |
| onPressed: () { }, |
| child: const Text('TextButton #5'), |
| ), |
| verticalSpacer, |
| |
| // Override the foregroundBuilder: add an underline. |
| // |
| // Add a border around button's child. In this case the |
| // border only appears when the button is hovered or pressed |
| // (if it's pressed it's always hovered too). Not that this |
| // border is different than the one specified with the styleFrom |
| // side parameter (or the ButtonStyle.side property). The foregroundBuilder |
| // is applied to a widget that contains the child and has already |
| // included the button's padding. It is unaffected by the button's shape. |
| // The styleFrom side parameter controls the button's outermost border and it |
| // outlines the button's shape. |
| TextButton( |
| style: TextButton.styleFrom( |
| foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) { |
| return DecoratedBox( |
| decoration: BoxDecoration( |
| border: states.contains(MaterialState.hovered) |
| ? Border(bottom: BorderSide(color: colorScheme.primary)) |
| : const Border(), // essentially "no border" |
| ), |
| child: child, |
| ); |
| }, |
| ), |
| onPressed: () { }, |
| child: const Text('TextButton #6'), |
| ), |
| verticalSpacer, |
| |
| // Override the backgroundBuilder to add a state specific gradient background |
| // and add an outline that only appears when the button is hovered or pressed. |
| // |
| // The gradient background decoration is computed by the statesToDecoration() |
| // method. The gradient flips horizontally when the button is hovered (watch |
| // closely). Because we want the outline to only appear when the button is hovered |
| // we can't use the styleFrom() side parameter, because that creates the same |
| // outline for all states. The ButtonStyle.copyWith() method is used to add |
| // a MaterialState<BorderSide?> property that does the right thing. |
| // |
| // The gradient background is translucent - all of the colors have opacity 0.5 - |
| // so the overlay's splash and highlight colors are visible even though they're |
| // drawn on the Material widget that's effectively behind the background. The |
| // border is also translucent, so if you look carefully, you'll see that the |
| // background - which is part of the button's Material but is drawn on top of the |
| // the background gradient - shows through the border. |
| TextButton( |
| onPressed: () {}, |
| style: TextButton.styleFrom( |
| overlayColor: color2, |
| backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) { |
| return AnimatedContainer( |
| duration: const Duration(milliseconds: 500), |
| decoration: statesToDecoration(states), |
| child: child, |
| ); |
| }, |
| ).copyWith( |
| side: MaterialStateProperty.resolveWith<BorderSide?>((Set<MaterialState> states) { |
| if (states.contains(MaterialState.hovered)) { |
| return BorderSide(width: 3, color: color3); |
| } |
| return null; // defer to the default |
| }), |
| ), |
| child: const Text('TextButton #7'), |
| ), |
| verticalSpacer, |
| |
| // Override the backgroundBuilder to add a grass image background. |
| // |
| // The image is clipped to the button's shape. We've included an Ink widget |
| // because the background image is opaque and would otherwise obscure the splash |
| // and highlight overlays that are painted on the button's Material widget |
| // by default. They're drawn on the Ink widget instead. The foreground color |
| // was overridden as well because white shows up a little better on the mottled |
| // green background. |
| TextButton( |
| onPressed: () {}, |
| style: TextButton.styleFrom( |
| foregroundColor: Colors.white, |
| backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) { |
| return Ink( |
| decoration: const BoxDecoration( |
| image: DecorationImage( |
| image: grassImage, |
| fit: BoxFit.cover, |
| ), |
| ), |
| child: child, |
| ); |
| }, |
| ), |
| child: const Text('TextButton #8'), |
| ), |
| verticalSpacer, |
| |
| // Override the foregroundBuilder to specify images for the button's pressed |
| // hovered and default states. We switch to an additional image while the |
| // button's callback is "running". |
| // |
| // This is an example of completely changing the default appearance of a button |
| // by specifying images for each state and by turning off the overlays by |
| // overlayColor: Colors.transparent. AnimatedContainer takes care of the |
| // fade in and out segues between images. |
| // |
| // This foregroundBuilder function ignores its child parameter. Unfortunately |
| // TextButton's child parameter is required, so we still have |
| // to provide one. |
| TextButton( |
| onPressed: () async { |
| // This is slightly complicated so that if the user presses the button |
| // while the current Future.delayed action is running, the currentAction |
| // flag is only reset to null after the _new_ action completes. |
| late final Future<void> thisAction; |
| thisAction = Future<void>.delayed(const Duration(seconds: 1), () { |
| if (currentAction == thisAction) { |
| setState(() { currentAction = null; }); |
| } |
| }); |
| setState(() { currentAction = thisAction; }); |
| }, |
| style: TextButton.styleFrom( |
| overlayColor: Colors.transparent, |
| foregroundBuilder: (BuildContext context, Set<WidgetState> states, Widget? child) { |
| late final ImageProvider image; |
| if (currentAction != null) { |
| image = runningImage; |
| } else if (states.contains(WidgetState.pressed)) { |
| image = pressedImage; |
| } else if (states.contains(WidgetState.hovered)) { |
| image = hoveredImage; |
| } else { |
| image = defaultImage; |
| } |
| return AnimatedContainer( |
| width: 64, |
| height: 64, |
| duration: const Duration(milliseconds: 300), |
| curve: Curves.fastOutSlowIn, |
| decoration: BoxDecoration( |
| image: DecorationImage( |
| image: image, |
| fit: BoxFit.contain, |
| ), |
| ), |
| ); |
| }, |
| ), |
| child: const Text('This child is not used'), |
| ), |
| ]; |
| |
| return Row( |
| children: <Widget> [ |
| // The dark/light and LTR/RTL switches. We use the updateDarkMode function |
| // provided by the parent TextButtonExampleApp to rebuild the MaterialApp |
| // in the appropriate dark/light ThemeMdoe. The directionality of the rest |
| // of the UI is controlled by the Directionality widget below, and the |
| // textDirection local state variable. |
| TextButtonExampleSwitches( |
| darkMode: widget.darkMode, |
| updateDarkMode: widget.updateDarkMode, |
| textDirection: textDirection, |
| updateRTL: (bool value) { |
| setState(() { |
| textDirection = value ? TextDirection.rtl : TextDirection.ltr; |
| }); |
| }, |
| ), |
| horizontalSpacer, |
| |
| Expanded( |
| child: Scrollbar( |
| controller: scrollController, |
| thumbVisibility: true, |
| child: SingleChildScrollView( |
| scrollDirection: Axis.horizontal, |
| controller: scrollController, |
| child: Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| Directionality( |
| textDirection: textDirection, |
| child: Column( |
| children: columnOneButtons, |
| ), |
| ), |
| horizontalSpacer, |
| |
| Directionality( |
| textDirection: textDirection, |
| child: Column( |
| children: columnTwoButtons |
| ), |
| ), |
| horizontalSpacer, |
| ], |
| ), |
| ), |
| ), |
| ), |
| ], |
| ); |
| } |
| } |
| |
| class TextButtonExampleSwitches extends StatelessWidget { |
| const TextButtonExampleSwitches({ |
| super.key, |
| required this.darkMode, |
| required this.updateDarkMode, |
| required this.textDirection, |
| required this.updateRTL |
| }); |
| |
| final bool darkMode; |
| final ValueChanged<bool> updateDarkMode; |
| final TextDirection textDirection; |
| final ValueChanged<bool> updateRTL; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Card( |
| child: Padding( |
| padding: const EdgeInsets.all(16), |
| child: IntrinsicWidth( |
| child: Column( |
| children: <Widget>[ |
| Row( |
| children: <Widget>[ |
| const Expanded(child: Text('Dark Mode')), |
| const SizedBox(width: 4), |
| Switch( |
| value: darkMode, |
| onChanged: updateDarkMode, |
| ), |
| ], |
| ), |
| const SizedBox(height: 16), |
| Row( |
| children: <Widget>[ |
| const Expanded(child: Text('RTL Text')), |
| const SizedBox(width: 4), |
| Switch( |
| value: textDirection == TextDirection.rtl, |
| onChanged: updateRTL, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| } |