Gesture detection: SelectableText inside ElevatedButton - flutter

I have a problem in Flutter Web with components that are Clickable (ElevatedButton, GestureDetector, etc) and at the same time contains a SelectableText widget inside.
The main problem is that a single click on top of the SelectableText does not trigger the onPressed button callback. Instead, it looks like 'initiates' a text selection.
From my point of view, text selection should only be initialized if a double tap, drag start or long tap happens on the text, but not on single tap.
A very simplified version of my problem:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Center(
child: ElevatedButton(
onPressed: () {
print('onTap');
},
child: Container(
padding: const EdgeInsets.all(36),
child: const SelectableText(
'Select me, but I also want to be clickable!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
),
),
),
),
),
);
}
}
Note the padding between the ElevatedButton and the SelectableText
Any tips? ideas?
Thanks in advance

I understand you are trying your best to mimic the "native web experience" where all texts are selectable, and like you said, "tap and drag start are different", but their differences might be as little as 1 pixel.
If you look at this web page (stack overflow), my user name is clickable, try to drag and select it, you can't. Now scroll up, below your question, the tags are clickable, try to drag and select on the word "flutter", you can't...
So honestly, for those texts on buttons, maybe just don't let them be selectable :D
But if you insist, there's onTap event on SelectableText widget as well, so you can fire the same callback as the button, so it doesn't matter where user clicked.

Related

Programmatically changing TextSpans in the selected text of a SelectableText.rich

I have a SelectableText.rich widget in flutter where the usecase requires each word within the text to be its own TextSpan.
The app currently parses the text into words and sets up the list of TextSpans. I want the user to be able to select text and then trigger a modification to the styling of all TextSpans touched by the selection.
For example, let’s say the SelectableText.rich has the following text where each word is its own TextSpan:
The boy went to the store. He went to the store every day.
If the user selects “went to the store” in the first sentence and then taps on a ‘bold’ button, how can I identify which spans should be affected? Note that “went to the store” is in the source text twice but only the first instance should be modified (so I can’t simply search the source text).
Here's a stripped-down example (using only the first sentence):
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RichText Selection Issue',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Selectable Text Problem')),
body: Padding(
padding: const EdgeInsets.all(30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SelectableText.rich(
TextSpan(text: 'The ', style: TextStyle(fontSize: 20), children: [
TextSpan(text: 'boy '),
TextSpan(text: 'went '),
TextSpan(text: 'to '),
TextSpan(text: 'the '),
TextSpan(text: 'store '),
TextSpan(text: '.'),
])),
const SizedBox(height: 20),
// Add button to trigger selection
ElevatedButton(
onPressed: () {},
child: const Text('Select'),
),
],
),
),
);
}
}
What would I do in the ElevatedButton's onPressed callback to trigger a rebuild that modified the style associated with the TextSpans for 'went to the store'? I can't take the example further than this because I don't know how to associate the selected text with the spans (which obviously would need 'style' properties and presumably some sort of state logic relating to whether or not they should be rendered as bold).
Any suggestions on how this could be handled (or pointers to any online articles covering this sort of topic)?
Thanks in advance!
Note : For the purposes of this question, please ignore the fact that the user could select partial words using the SelectableText widget. I haven’t thought through that issue yet (the objective would be that a partially selected word would be treated as the full word).
Edited to add an example.

Howto hide the keyboard in flutter when tapping outside of the TextField without triggering any other actions?

Hi I have the following (simplified) code:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).focusedChild?.unfocus(),
child: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
print("Button pressed!");
},
child: Text("Click me"),
),
TextField(),
],
),
),
);
}
}
This code results in the following UI:
When I click on the blue button, "Button pressed!" gets printed and the keyboard does not disappear.
The problem here, is that this behavior is not what I want. I want that the keyboard disappears when I click somewhere outside of the TextField, and that the button does not get triggered, even when I directly click on it. So for example if I click on the button just the keyboard should disappear without any other action/side effect (nothing gets printed in this case). But it should still be possible to interact with the TextField normally.
Note: Disabling the button is not a good option since in my real case scenario the page is build out of a lot complex widgets and disabling them is really complicated.
Already stuck there for a while now. Hope you can help me :)
Wrap the Scaffold with Gesture detector and on tap unfocus the focus scope
example
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: const Scaffold(),
);
}
}
Wrap your Scaffold Widget in a Button it can be Gesture Detector or Inkwell. Then add unfocus method in it.
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: const Scaffold(
body: TextFormField(
decoration: OutlineInputBorder()),
);
I also needed to hide the keyboard when I clicked on a date picker (another widget)
During my experience, with GestureDetector keyboard hid itself when I clicked somewhere that is not a widget. It would not hide itself otherwise.
Simply changing GestureDetector
to
Listener and adding
onPointerDown: (PointerDownEvent event) => FocusManager.instance.primaryFocus?.unfocus(), gave me the solution.
So, in your instance, what you should do is, wrap the Button "Click me" with a Listener and give it onPointerDown ,that should hide your keyboard whenever you press the button.

Flutter photo_view package gestures not working properly

Hi I am a flutter mobile developer and I tried to use this package: https://pub.dev/packages/photo_view/versions for an offline map. I tried zoom and drag and other gestures. It seems working properly for zooming most of the time, but when I tried to drag the picture It has a less than 10 percent success to move it. Here is my code, can somebody help me what coul'd I do wrong? In the package example It seems like It's working properly.
class OfflineMapWidget extends StatefulWidget {
final Session session;
final bool connectedToNetwork;
const OfflineMapWidget(
{Key? key, required this.session, required this.connectedToNetwork})
: super(key: key);
#override
_OfflineMapWidgetState createState() => _OfflineMapWidgetState();
}
class _OfflineMapWidgetState extends State<OfflineMapWidget> {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: SafeArea(
child: Theme(
data: ThemeData(
brightness: Brightness.dark,
appBarTheme: AppBarTheme(
backgroundColor: Color.fromARGB(255, 65, 116, 131))),
child: Scaffold(
appBar: AppBar(
leading: MenuWidget(),
actions: [widget.connectedToNetwork ? QrPaymentButton(session: widget.session) : Container()],
),
body: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
color: Colors.white,
child: PhotoView(
imageProvider: AssetImage('assets/images/offline_map.png'),
),
),
),
),
),
);
}
}
Update: I found the problem. This Widget was in a ZoomDrawer (another pub dev package), and that Widget confused this one, so I need to find another solution. If somebody else uses this 2 package in one the problem is here. The solution is simple, you can disable drag in ZoomDrawer with the disableDragGesture property. If you don't want to disable it on all menu points you can check if you are in the mentioned menu point and disable only when this one is up.I write this one down in the comments as well so it will be more clear.
I found the problem. This Widget was in a ZoomDrawer (another pub dev package), and that Widget confused this one, so I need to find another solution. If somebody else uses this 2 package in one the problem is here. The solution is simple, you can disable drag in ZoomDrawer with the disableDragGesture property. If you don't want to disable it on all menu points you can check if you are in the mentioned menu point and disable only when this one is up.

Set default icon theme to cupertino flutter appbar

The app that im building requires me to have an AppBar with a leading back button. However I prefer the cupertino back button(iOS-style) for the leading icon instead of the default back button for android. I am aware that I can manually change the leading button of each AppBar by using an iconButton but i was wondering if there is any easy way to do this like a theme. Any help appreciated.
Instead of using MaterialApp as your root widget you can use CupertinoApp to do the same, assuming that the above changing of the AppBar is needed for each screen in your app. This will automatically set the icon as you require
Here is a simple example to help you
Root Widget or starting point of the app
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 CupertinoApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
Then using a CupertinoPageScaffold where you want the CupertinoNavigationBar (I mean your appbar with ios icons) with the chevron icon like ios
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
// Try removing opacity to observe the lack of a blur effect and of sliding content.
automaticallyImplyLeading: true // This will decide if the leading icon comes in by default
backgroundColor: CupertinoColors.systemGrey.withOpacity(0.5),
middle: const Text('Sample Code'),
),
child: Column(
children: <Widget>[
Container(height: 50, color: CupertinoColors.systemRed),
Container(height: 50, color: CupertinoColors.systemGreen),
Container(height: 50, color: CupertinoColors.systemBlue),
Container(height: 50, color: CupertinoColors.systemYellow),
],
),
);
Facing a relatively similar problem I used the builder property, which it should work with any App like :
CupertinoApp(
builder: (_, child) => IconTheme(
data: IconThemeData(
size: 15,
color: const Color(0xffffffff),
),
child: child,
),
)
My problem was with the default icon color and size but you can use AppBarTheme or any similar widget to achieve what you want.
This may help you override default value with majority of the lacking theme delegates when working with the cupertino family (It's not yet mature like the material but I can see the constant and rapid effort and the future of it).

How to align widget to another widget in Flutter?

I have a RaisedButton widget inside of a Center widget as one of the widgets in a Column of widgets. I want to add a CircularProgressIndicator to the right side of this button and show it when the button is pressed. Yet I want to leave the button centred when the progress bar is shown. In other words I want the button always be in the center and the progress bar aligned to this button.
I tried to use a Row here but this pushes the button and it becomes not centred any more.
EDIT1: Looking at the result of the solution provided by #Anil Chauhan (thanks for the answer):
Like I said before that I tried to use Row like he did, the problem is that in this case the button is not in the centred in the screen and is pushed by the progress bar. And I need it to stay in the middle of it's row.
EDIT2: #Anil Chauhan edited answer now works for a specific case in which the button is predetermined size. But if the size of the button is changed based on the language of the text (in apps that have several languages) this solution will not work.
This is the reason the question I asked is: "How to align widget to another widget". Because if I could that I don't have to worry about the button text size any more.
What would be the right way to handle this in Flutter?
class MyPage extends StatefulWidget {
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
bool _showIndicator = false;
void _onButtonClicked() {
setState(() {
_showIndicator = !_showIndicator;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
children: <Widget>[
const Expanded(child: SizedBox()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: RaisedButton(
child: Text("I am Too Big"),
onPressed: _onButtonClicked,
),
),
Expanded(
child: _showIndicator
? const Align(
alignment: Alignment.centerLeft,
child: CircularProgressIndicator(),
)
: const SizedBox(),
),
],
),
),
);
}
}
Here is my explanation:
The RaisedButton size is depends on its child. If you add it to Row it will automatically align to left(or start).
Expanded widget will fill the remaining space in Flex widget(Row & Column are child classes of Flex). If you add more than one Expanded widgets, it will split equally. So I added two Expanded to both the side of button to make it center.
Now We should give child for Expanded Widget.
For the first one(left) we don't have anything to display so I added SizedBox.
For the second one(right) we need CircularProgressIndicator. so I added it.
The Expanded widget will try to make its child to fill the space inside of it. So the CircularProgressIndicator will become Ellipse shaped. We can avoid this by using Align Widget.
Try this:
Updated:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MyAppOne(),
);
}
}
class MyAppOne extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyAppOne>{
bool show = false;
#override
Widget build(BuildContext context){
return Scaffold(
body: Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
alignment: Alignment.center,
child: RaisedButton(
onPressed: () {
setState(() {
show =!show;
});
},
child: Text('Show'),
),
),
Positioned(
right: MediaQuery.of(context).size.width * .20,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(10.0),
child: show ? CircularProgressIndicator() : Container(),
),
)
],
)
);
}
}
Flutter's Column and Row widgets have two convenient properties called mainAxisAlignment and crossAxisAlignment. I assume since you're using a Column and want the CircularProgressIndicator to the right of the button, you might be want to use crossAxisAlignment since the cross-axis of a Column is along the horizontal.
If possible, please share your code for better understanding and support of the issue.