| 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 | import 'package:flutter/material.dart'; |
| 6 | |
| 7 | /// Flutter code sample for [ListenableBuilder]. |
| 8 | |
| 9 | void main() => runApp(const ListenableBuilderExample()); |
| 10 | |
| 11 | /// This widget listens for changes in the focus state of the subtree defined by |
| 12 | /// its [child] widget, changing the border and color of the container it is in |
| 13 | /// when it has focus. |
| 14 | /// |
| 15 | /// A [FocusListenerContainer] swaps out the [BorderSide] of a border around the |
| 16 | /// child widget with [focusedSide], and the background color with |
| 17 | /// [focusedColor], when a widget that is a descendant of this widget has focus. |
| 18 | class FocusListenerContainer extends StatefulWidget { |
| 19 | const FocusListenerContainer({ |
| 20 | super.key, |
| 21 | this.border, |
| 22 | this.padding, |
| 23 | this.focusedSide, |
| 24 | this.focusedColor = Colors.black12, |
| 25 | required this.child, |
| 26 | }); |
| 27 | |
| 28 | /// This is the border that will be used when not focused, and which defines |
| 29 | /// all the attributes except for the [OutlinedBorder.side] when focused. |
| 30 | final OutlinedBorder? border; |
| 31 | |
| 32 | /// This is the [BorderSide] that will be used for [border] when the [child] |
| 33 | /// subtree is focused. |
| 34 | final BorderSide? focusedSide; |
| 35 | |
| 36 | /// This is the [Color] that will be used as the fill color for the background |
| 37 | /// of the [child] when a descendant widget is focused. |
| 38 | final Color? focusedColor; |
| 39 | |
| 40 | /// The padding around the inside of the container. |
| 41 | final EdgeInsetsGeometry? padding; |
| 42 | |
| 43 | /// This is defines the subtree to listen to for focus changes. |
| 44 | final Widget child; |
| 45 | |
| 46 | @override |
| 47 | State<FocusListenerContainer> createState() => _FocusListenerContainerState(); |
| 48 | } |
| 49 | |
| 50 | class _FocusListenerContainerState extends State<FocusListenerContainer> { |
| 51 | final FocusNode _focusNode = FocusNode(); |
| 52 | |
| 53 | @override |
| 54 | void dispose() { |
| 55 | _focusNode.dispose(); |
| 56 | super.dispose(); |
| 57 | } |
| 58 | |
| 59 | @override |
| 60 | Widget build(BuildContext context) { |
| 61 | final OutlinedBorder effectiveBorder = widget.border ?? const RoundedRectangleBorder(); |
| 62 | return ListenableBuilder( |
| 63 | listenable: _focusNode, |
| 64 | child: Focus( |
| 65 | focusNode: _focusNode, |
| 66 | skipTraversal: true, |
| 67 | canRequestFocus: false, |
| 68 | child: widget.child, |
| 69 | ), |
| 70 | builder: (BuildContext context, Widget? child) { |
| 71 | return Container( |
| 72 | padding: widget.padding, |
| 73 | decoration: ShapeDecoration( |
| 74 | color: _focusNode.hasFocus ? widget.focusedColor : null, |
| 75 | shape: effectiveBorder.copyWith(side: _focusNode.hasFocus ? widget.focusedSide : null), |
| 76 | ), |
| 77 | child: child, |
| 78 | ); |
| 79 | }, |
| 80 | ); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | class MyField extends StatefulWidget { |
| 85 | const MyField({super.key, required this.label}); |
| 86 | |
| 87 | final String label; |
| 88 | |
| 89 | @override |
| 90 | State<MyField> createState() => _MyFieldState(); |
| 91 | } |
| 92 | |
| 93 | class _MyFieldState extends State<MyField> { |
| 94 | final TextEditingController controller = TextEditingController(); |
| 95 | |
| 96 | @override |
| 97 | Widget build(BuildContext context) { |
| 98 | return Row( |
| 99 | children: <Widget>[ |
| 100 | Expanded(child: Text(widget.label)), |
| 101 | Expanded( |
| 102 | flex: 2, |
| 103 | child: TextField( |
| 104 | controller: controller, |
| 105 | onEditingComplete: () { |
| 106 | debugPrint('Field ${widget.label} changed to ${controller.value}' ); |
| 107 | }, |
| 108 | ), |
| 109 | ), |
| 110 | ], |
| 111 | ); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | class ListenableBuilderExample extends StatelessWidget { |
| 116 | const ListenableBuilderExample({super.key}); |
| 117 | |
| 118 | @override |
| 119 | Widget build(BuildContext context) { |
| 120 | return MaterialApp( |
| 121 | home: Scaffold( |
| 122 | appBar: AppBar(title: const Text('ListenableBuilder Example' )), |
| 123 | body: Center( |
| 124 | child: SizedBox( |
| 125 | width: 300, |
| 126 | child: Padding( |
| 127 | padding: const EdgeInsets.all(8.0), |
| 128 | child: Column( |
| 129 | mainAxisAlignment: MainAxisAlignment.center, |
| 130 | children: <Widget>[ |
| 131 | const Padding( |
| 132 | padding: EdgeInsets.only(bottom: 8), |
| 133 | child: MyField(label: 'Company' ), |
| 134 | ), |
| 135 | FocusListenerContainer( |
| 136 | padding: const EdgeInsets.all(8), |
| 137 | border: const RoundedRectangleBorder( |
| 138 | side: BorderSide(strokeAlign: BorderSide.strokeAlignOutside), |
| 139 | borderRadius: BorderRadius.all(Radius.circular(5)), |
| 140 | ), |
| 141 | // The border side will get wider when the subtree has focus. |
| 142 | focusedSide: const BorderSide( |
| 143 | width: 4, |
| 144 | strokeAlign: BorderSide.strokeAlignOutside, |
| 145 | ), |
| 146 | // The container background will change color to this when |
| 147 | // the subtree has focus. |
| 148 | focusedColor: Colors.blue.shade50, |
| 149 | child: const Column( |
| 150 | crossAxisAlignment: CrossAxisAlignment.start, |
| 151 | children: <Widget>[ |
| 152 | Text('Owner:' ), |
| 153 | MyField(label: 'First Name' ), |
| 154 | MyField(label: 'Last Name' ), |
| 155 | ], |
| 156 | ), |
| 157 | ), |
| 158 | ], |
| 159 | ), |
| 160 | ), |
| 161 | ), |
| 162 | ), |
| 163 | ), |
| 164 | ); |
| 165 | } |
| 166 | } |
| 167 | |