Where does the FocusScope widget create in the tree and we pass every context in it and it can request to any focus nodes. When we pass context to FocusScope it will start looking above the context and we never used the FocusScope widget in the code in the hierarchy where does it create and how does it resolves in the case of scaffold if we pass context that is above in the tree then it throws an exception then we use builder type of thing but in FocusScope why it doesn't throw an error?
Here is the example for the FocusScope
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// A demonstration pane.
///
/// This is just a separate widget to simplify the example.
class Pane extends StatelessWidget {
const Pane({
Key? key,
required this.focusNode,
this.onPressed,
required this.backgroundColor,
required this.icon,
this.child,
}) : super(key: key);
final FocusNode focusNode;
final VoidCallback? onPressed;
final Color backgroundColor;
final Widget icon;
final Widget? child;
#override
Widget build(BuildContext context) {
return Material(
color: backgroundColor,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Center(
child: child,
),
Align(
alignment: Alignment.topLeft,
child: IconButton(
autofocus: true,
focusNode: focusNode,
onPressed: onPressed,
icon: icon,
),
),
],
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool backdropIsVisible = false;
FocusNode backdropNode = FocusNode(debugLabel: 'Close Backdrop Button');
FocusNode foregroundNode = FocusNode(debugLabel: 'Option Button');
#override
void dispose() {
super.dispose();
backdropNode.dispose();
foregroundNode.dispose();
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final Size stackSize = constraints.biggest;
return Stack(
fit: StackFit.expand,
// The backdrop is behind the front widget in the Stack, but the widgets
// would still be active and traversable without the FocusScope.
children: <Widget>[
// TRY THIS: Try removing this FocusScope entirely to see how it affects
// the behavior. Without this FocusScope, the "ANOTHER BUTTON TO FOCUS"
// button, and the IconButton in the backdrop Pane would be focusable
// even when the backdrop wasn't visible.
FocusScope(
// TRY THIS: Try commenting out this line. Notice that the focus
// starts on the backdrop and is stuck there? It seems like the app is
// non-responsive, but it actually isn't. This line makes sure that
// this focus scope and its children can't be focused when they're not
// visible. It might help to make the background color of the
// foreground pane semi-transparent to see it clearly.
canRequestFocus: backdropIsVisible,
child: Pane(
icon: const Icon(Icons.close),
focusNode: backdropNode,
backgroundColor: Colors.lightBlue,
onPressed: () => setState(() => backdropIsVisible = false),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// This button would be not visible, but still focusable from
// the foreground pane without the FocusScope.
ElevatedButton(
onPressed: () => debugPrint('You pressed the other button!'),
child: const Text('ANOTHER BUTTON TO FOCUS'),
),
DefaultTextStyle(
style: Theme.of(context).textTheme.headline2!,
child: const Text('BACKDROP')),
],
),
),
),
AnimatedPositioned(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300),
top: backdropIsVisible ? stackSize.height * 0.9 : 0.0,
width: stackSize.width,
height: stackSize.height,
onEnd: () {
if (backdropIsVisible) {
backdropNode.requestFocus();
} else {
foregroundNode.requestFocus();
}
},
child: Pane(
icon: const Icon(Icons.menu),
focusNode: foregroundNode,
// TRY THIS: Try changing this to Colors.green.withOpacity(0.8) to see for
// yourself that the hidden components do/don't get focus.
backgroundColor: Colors.green,
onPressed: backdropIsVisible
? null
: () => setState(() => backdropIsVisible = true),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.headline2!,
child: const Text('FOREGROUND')),
),
),
],
);
}
#override
Widget build(BuildContext context) {
// Use a LayoutBuilder so that we can base the size of the stack on the size
// of its parent.
return LayoutBuilder(builder: _buildStack);
}
}
Related
I am pretty new to OOP and Flutter. I created the following widget and want to call the widget function when creating it inside another stateless Widget.
My plan is to create the widget as a child and then use the defined variable widgetState to check if it is true and if it is, it should call the void printSample() function. The conditional check shout be triggered by onTap of the widget (InkWell).
Widget with the Function:
class WhiteCard extends StatelessWidget {
final widgetState = true;
final VoidCallback onTap;
final Color switchColor = Colors.red;
void printSample() {
print("Sample text");
}
const WhiteCard({
Key? key,
required this.onTap,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
width: 320,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Colors.white,
),
child: Center(
child: ScreenController.isLargeScreen(context)
? Text("large")
: ScreenController.isMediumScreen(context)
? Text("medium")
: Text("small"),
),
),
);
}
}
Widget that calls the WhiteCard()
class LargeScreen extends StatelessWidget {
const LargeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Expanded(
child: Container(
color: Colors.orangeAccent,
child: Center(
child: WhiteCard(
onTap: () {
CALL THE FUNCTION FROM WIDGET);
},
),
),
),
);
}
}
You can also define a function to leave the definition of object cleaner:
class LargeScreen extends StatelessWidget {
const LargeScreen({Key? key}) : super(key: key);
onTapFunction() {
// write he whatever you need to do when taping widget
}
#override
Widget build(BuildContext context) {
return Expanded(
child: Container(
color: Colors.orangeAccent,
child: Center(
child: WhiteCard(
onTap: onTapFunction,
),
),
),
);
}
}
So if you need to call the onTapFunction from elsewhere, you can.
Well if I understood your question correctly then your onTap should be something like this:
onTap: () {
if (widgetState) {
printSample();
}
Good luck, Feel free to respond if you have queries
I have two screens where the second screen is pushed above the first with Navigator.push() and the second screen is partial transparent. I want to display a SnackBar, but it isn't really visible. It looks like the ScaffoldMessenger chooses the wrong of the two Scaffolds to attach the Snackbar. This leads to the effect that the SnackBar collides with the TextInput and it is also not fully visible. But this bad behavior is only the case as long as the soft keyboard is open. If the keyboard is closed, everything works fine. It seems like the open keyboard tells the ScaffoldMessenger to choose the Scaffold from the second screen to display the SnackBar.
How can I achieve that the SnackBar is shown normally in the sense of is attached to the Scaffold of screen 2 while the keyboard is open? The expected behavior is that the Snackbar isn't displayed transparent.
Keyboard open -> SnackBar is attached to Scaffold of screen 1 -> Bad
Keyboard closed -> SnackBar is attached to Scaffold of screen 2 -> Good
GIF showing the complete workflow
My code (fully executable)
import 'dart:io';
import 'package:keyboard_utils/keyboard_listener.dart';
import 'package:keyboard_utils/keyboard_utils.dart';
import 'package:flutter/material.dart' hide KeyboardListener;
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[const Text('You have pushed the button this many times:'),
Text('$_counter', style: Theme.of(context).textTheme.headline4),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(PageRouteBuilder(
opaque: false, // push route with transparency
pageBuilder: (context, animation, secondaryAnimation) => const Screen2(),
));
},
child: const Text('navigate'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _counter++),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class Screen2 extends StatefulWidget {
const Screen2({Key? key}) : super(key: key);
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
final _keyboardUtils = KeyboardUtils();
late int _idKeyboardListener;
final focusNode = FocusNode();
bool isEmojiKeyboardVisible = false;
bool isKeyboardVisible = false;
double _keyboardHeight = 300;
#override
void initState() {
super.initState();
_idKeyboardListener = _keyboardUtils.add(
listener: KeyboardListener(
willHideKeyboard: () {
if (isKeyboardVisible) {
isKeyboardVisible = false;
isEmojiKeyboardVisible = false;
}
setState(() {}); // show correct Icon in IconButton
},
willShowKeyboard: (double keyboardHeight) async {
if (Platform.isAndroid) {
_keyboardHeight = keyboardHeight + WidgetsBinding.instance.window.viewPadding.top / WidgetsBinding.instance.window.devicePixelRatio;
} else {
_keyboardHeight = keyboardHeight;
}
isKeyboardVisible = true;
isEmojiKeyboardVisible = true;
setState(() {});
},
)
);
}
#override
void dispose() {
_keyboardUtils.unsubscribeListener(subscribingId: _idKeyboardListener);
if (_keyboardUtils.canCallDispose()) {
_keyboardUtils.dispose();
}
focusNode.dispose();
super.dispose();
}
Future<void> onEmojiButtonPressed() async {
if(isEmojiKeyboardVisible){
if(isKeyboardVisible){
FocusManager.instance.primaryFocus?.unfocus();
isKeyboardVisible = false;
} else {
focusNode.unfocus();
await Future<void>.delayed(const Duration(milliseconds: 1));
if(!mounted) return;
FocusScope.of(context).requestFocus(focusNode);
}
} else {
assert(!isKeyboardVisible);
setState(() {
isEmojiKeyboardVisible = true;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold( // wrapping with ScaffoldMessenger does NOT fix this bug
backgroundColor: Colors.white.withOpacity(0.5),
resizeToAvoidBottomInset: false,
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Expanded(
child: Container(
height: 200,
),
),
Row(
children: [
IconButton(
icon: Icon(isKeyboardVisible || !isEmojiKeyboardVisible ? Icons.emoji_emotions_outlined : Icons.keyboard_rounded),
onPressed: onEmojiButtonPressed,
),
Expanded(
child: TextField(
focusNode: focusNode,
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A snack!'))),
),
],
),
],
),
),
),
Offstage(
offstage: !isEmojiKeyboardVisible,
child: SizedBox(
height: _keyboardHeight,
child: Container(color: Colors.red),
),
),
],
),
),
);
}
}
Dependencies
keyboard_utils: ^1.3.4
What I've tried
I tried to wrap the Scaffold of Screen2 with a ScaffoldMessenger. This doesn't fix my problem. In that case, no SnackBar was shown at all if the keyboard was open.
Edit: I also created an GitHub issue for that but I don't expect an answer soon: https://github.com/flutter/flutter/issues/105406#issuecomment-1147194647
Edit 2: A workaround for this issue is to use SnackBarBehaviod.floating and a bottom margin, for example:
SnackBar(
content: Text('A snack!'),
margin: EdgeInsets.only(bottom: 350.0),
behavior: SnackBarBehavior.floating,
)
But this is not a satisfying solution.
How to clip Stack children within it's size.
In this image there are 3 grid-Items using orange color and every item using InkWell to use hover-Method to Align on Stack. While hover:false the Pop PoP Widget won't be visible to the UI. With align property it works, but as you can see the Right Top GridItem's item:2 pop POp widget is visible outside the Stack<Griditem> and I want to make it invisible outside the stack. I've tested using clipBehavior: with every Clip enums.
I want to hide the Pop POp widget while it is outside the Stack and yes I need this pop-up effect.
For Flutter web and I'm using Flutter V2.5.2
Current Layout with Issue
Full Code to reproduce the issue
import 'package:flutter/material.dart';
void main() => runApp(
const MaterialApp(
home: Appp(),
),
);
class Appp extends StatelessWidget {
const Appp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const BodyX();
}
}
class BodyX extends StatelessWidget {
const BodyX({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: LayoutBuilder(
builder: (context, constraints) {
return GridView.count(
crossAxisCount: 2,
children: [
...List.generate(
3,
(index) => GridItem(
key: UniqueKey(),
maxWidth: constraints.maxWidth / 2,
),
),
],
);
},
));
}
}
class GridItem extends StatefulWidget {
const GridItem({
Key? key,
required this.maxWidth,
}) : super(key: key);
final double maxWidth;
#override
State<GridItem> createState() => _AppXState();
}
class _AppXState extends State<GridItem> {
bool _isHovered = false;
#override
Widget build(BuildContext context) {
print("ItemWidth : ${widget.maxWidth}");
return SizedBox(
//though it wont effect here,
// just finding the size of Grid because it will 1x1
width: widget.maxWidth,
height: widget.maxWidth,
child: InkWell(
onTap: () {},
hoverColor: Colors.black,
onHover: (value) {
setState(() {
_isHovered = value;
});
},
child: Stack(
clipBehavior: Clip.antiAliasWithSaveLayer,
children: [
Container(
color: Colors.deepOrange.withOpacity(.2),
),
AnimatedAlign(
alignment: Alignment(0, _isHovered ? .7 : 2),
child: Container(
padding: const EdgeInsets.all(22),
color: Colors.greenAccent,
child: const Text(
"Pop POp",
),
),
duration: const Duration(
milliseconds: 200,
),
)
],
),
),
);
}
}
If you don't want a Widget to draw beyond its layout size, you can use ClipRect to clip it.
In your case, you can wrap ClipRect on your Stack, like so:
ClipRect(
child: Stack(
children: ...
),
)
Further more, you can use ClipRRect to clip a rounded rectangle shape (circular border) or ClipPath to clip a custom shape, like a triangle. You can read more about these widgets in the official docs.
When I unfocus on a TextField with existing Text then refocus on it again, the cursor always shows up at the start position, not at the tapped position. So I apply the logic of unfocusing from flutter_keyboard_visibility package - KeyboardDismissOnTap Widget and it still doesn't work. But when I wrapped my widget with this widget, it works the way I wanted to.
What's the difference if I wrap my widget with this KeyboardDismissOnTap Widget and when I use the same logic of that widget to my widget? I think It's basically the same, but it's not working the same way.
KeyboardDismissOnTap
class KeyboardDismissOnTap extends StatelessWidget {
const KeyboardDismissOnTap({Key? key, required this.child}) : super(key: key);
final Widget child;
void _hideKeyboard(BuildContext context) {
final currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.hasFocus) {
FocusManager.instance.primaryFocus?.unfocus();
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _hideKeyboard(context),
child: child,
);
}
}
MyPage - copied the logic of KeyboardDismissOnTap. Refocusing not working as intended, cursor still shows at start of existing text
class Mypage extends StatelessWidget {
const Mypage({Key? key}) : super(key: key);
void _hideKeyboard(BuildContext context) {
final currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.hasFocus) {
FocusManager.instance.primaryFocus?.unfocus();
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _hideKeyboard(context),
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: TextEditingController()..text = 'Existing Text Here',
),
TextButton(
onPressed: () {
//Unfocus function here
}
},
child: Text('Unfocus'))
],
),
),
),
);
}
}
MyPage - wrapped with KeyboardDismissOnTap. Refocusing works correctly, cursor shows at the right position where you tapped it.
class Mypage extends StatelessWidget {
const Mypage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return KeyboardDismissOnTap(
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: TextEditingController()..text = 'Existing Text Here',
),
TextButton(
onPressed: () {
if (!FocusScope.of(context).hasPrimaryFocus && FocusScope.of(context).hasFocus) {
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Text('Unfocus'))
],
),
),
),
);
}
}
I tried to add text editing format option like in the gmail app. But when highlight the text there' is not a format option. Is it possible to handle selecting alert? (Copy/cut/paste). Or is there a way to add format bar like gmail?
TextField(
controller: _categoryController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Enter Category Name",
),
),
I added screenshot and gif files to better understanding my question.
Selecting option on my Flutter application
Selecting option on Gmail App
Output:
You can check the code below:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _controller = new TextEditingController();
final _textfieldFocusNode = new FocusNode();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.all(20.0),
child: GestureDetector(
// intercept all pointer calls
behavior: HitTestBehavior.opaque,
onTap: () {
FocusScope.of(context).requestFocus(_textfieldFocusNode);
},
onLongPress: () {
showMenu(
context: context,
// TODO: Position dynamically based on cursor or textfield
position: RelativeRect.fromLTRB(0.0, 300.0, 300.0, 0.0),
items: [
PopupMenuItem(
child: Row(
children: <Widget>[
// TODO: Dynamic items / handle click
PopupMenuItem(
child: Text(
"Paste",
style: Theme.of(context)
.textTheme
.body2
.copyWith(color: Colors.red),
),
),
PopupMenuItem(
child: Text("Select All"),
),
],
),
),
],
);
},
child: IgnorePointer(
// ensures textfield doesn't overrule GestureDetector
child: TextField(
focusNode: _textfieldFocusNode,
controller: _controller,
),
),
),
)
],
),
),
);
}
}
You can use selectionControls parameter to customize text selection menu:
TextField(selectionControls: MyMaterialTextSelectionControls()),
and implement delegate class like here:
class MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
// Padding between the toolbar and the anchor.
static const double _kToolbarContentDistanceBelow = 10.0;
static const double _kToolbarContentDistance = 8.0;
/// Builder for material-style copy/paste text selection toolbar.
#override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset selectionMidpoint,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ClipboardStatusNotifier clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) {
final TextSelectionPoint startTextSelectionPoint = endpoints[0];
final TextSelectionPoint endTextSelectionPoint =
endpoints.length > 1 ? endpoints[1] : endpoints[0];
final Offset anchorAbove = Offset(
globalEditableRegion.left + selectionMidpoint.dx,
globalEditableRegion.top +
startTextSelectionPoint.point.dy -
textLineHeight -
_kToolbarContentDistance,
);
final Offset anchorBelow = Offset(
globalEditableRegion.left + selectionMidpoint.dx,
globalEditableRegion.top +
endTextSelectionPoint.point.dy +
_kToolbarContentDistanceBelow,
);
final value = delegate.textEditingValue;
return MyTextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
clipboardStatus: clipboardStatus,
handleCustomButton: () {
print(value.selection.textInside(value.text));
delegate.hideToolbar();
},
);
}
}
class MyTextSelectionToolbar extends StatelessWidget {
const MyTextSelectionToolbar({
Key? key,
required this.anchorAbove,
required this.anchorBelow,
required this.clipboardStatus,
required this.handleCustomButton,
}) : super(key: key);
final Offset anchorAbove;
final Offset anchorBelow;
final ClipboardStatusNotifier clipboardStatus;
final VoidCallback? handleCustomButton;
#override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final List<_TextSelectionToolbarItemData> items =
<_TextSelectionToolbarItemData>[
_TextSelectionToolbarItemData(
onPressed: handleCustomButton ?? () {},
label: 'Custom button',
),
];
int childIndex = 0;
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
toolbarBuilder: (BuildContext context, Widget child) =>
Container(color: Colors.pink, child: child),
children: items
.map((_TextSelectionToolbarItemData itemData) =>
TextSelectionToolbarTextButton(
padding: TextSelectionToolbarTextButton.getPadding(
childIndex++, items.length),
onPressed: itemData.onPressed,
child: Text(itemData.label),
))
.toList(),
);
}
}
class _TextSelectionToolbarItemData {
const _TextSelectionToolbarItemData({
required this.label,
required this.onPressed,
});
final String label;
final VoidCallback onPressed;
}
Or checkout this one text_selection_controls
You can use Selectable Widget. click here