Like Google Drive, can I create custom menu in Flutter Web application?.
Below the instruction how to implement working context menu called via mouse right button in flutter web app:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
// Prevent default event handler
document.onContextMenu.listen((event) => event.preventDefault());
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Center(
child: Listener(
child: Icon(
Icons.ac_unit,
size: 48.0,
),
onPointerDown: _onPointerDown,
),
),
);
}
/// Callback when mouse clicked on `Listener` wrapped widget.
Future<void> _onPointerDown(PointerDownEvent event) async {
// Check if right mouse button clicked
if (event.kind == PointerDeviceKind.mouse &&
event.buttons == kSecondaryMouseButton) {
final overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final menuItem = await showMenu<int>(
context: context,
items: [
PopupMenuItem(child: Text('Copy'), value: 1),
PopupMenuItem(child: Text('Cut'), value: 2),
],
position: RelativeRect.fromSize(
event.position & Size(48.0, 48.0), overlay.size));
// Check if menu item clicked
switch (menuItem) {
case 1:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Copy clicked'),
behavior: SnackBarBehavior.floating,
));
break;
case 2:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Cut clicked'),
behavior: SnackBarBehavior.floating));
break;
default:
}
}
}
}
The only thing is to do is correct positioning of left top corner of context menu.
Until the open issue is resolved, you can do the following in your main():
import 'dart:html';
void main() {
window.document.onContextMenu.listen((evt) => evt.preventDefault());
// ...
}
Here is the open issue for it: https://github.com/flutter/flutter/issues/31955
You can disable it for a webpage like this:
How do I disable right click on my web page?
You can also listen for Pointer Signal events and render the popup in Flutter:
https://medium.com/#crizantlai/flutter-handling-mouse-events-241108731537
Basically on web for example you would disable the default context menu, and show an Overlay in flutter when you receive the right click pointer signal.
Prevent default contextmenu
Add an oncontextmenu attribute to <html> tag in web/index.html:
<!DOCTYPE html>
<html oncontextmenu="event.preventDefault();">
<head>
...
See also: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#event_handler_attributes
This has the same effect as https://stackoverflow.com/a/64779321/16613821 (window.document is just the <html> tag), but without triggering "Avoid using web-only libraries outside Flutter web plugin packages." warning or using universal_html package.
NOTE: Hot reload won't work for this kind of change, but you can simply refresh(F5) browser.
Add your custom contextmenu
https://github.com/flutter/flutter/pull/74286 doesn't work well for your usecase
This should show up by default on desktop, but only when right clicking on EditableText-based widgets. Right clicking elsewhere does nothing, for now.
This is also purposely not customizable or reusable for now. It was a temporary solution that we plan to expand on.
In general, you can use GestureDetector.onSecondaryTap to detect user's right click.
Thanks for the inspiration BambinoUA. I decided to make my own cross platform class for this.
Works on iOS/Android/Web/Windows/Mac & Linux. Tested.
import 'package:bap/components/splash_effect.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart' as html;
class CrossPlatformClick extends StatefulWidget {
final Widget child;
/**
* Normal touch, tap, right click for platforms.
*/
final Function()? onNormalTap;
/**
* A list of menu items for right click or long press.
*/
final List<PopupMenuEntry<String>>? menuItems;
final Function(String? itemValue)? onMenuItemTapped;
const CrossPlatformClick({Key? key, required this.child, this.menuItems, this.onNormalTap, this.onMenuItemTapped}) : super(key: key);
#override
State<CrossPlatformClick> createState() => _CrossPlatformClickState();
}
class _CrossPlatformClickState extends State<CrossPlatformClick> {
/**
* We record this so that we can use long-press and location.
*/
PointerDownEvent? _lastEvent;
#override
Widget build(BuildContext context) {
final listener = Listener(
child: widget.child,
onPointerDown: (event) => _onPointerDown(context, event),
);
return SplashEffect(
isDisabled: widget.onNormalTap == null,
borderRadius: BorderRadius.zero,
onTap: widget.onNormalTap!,
child: listener,
onLongPress: () {
if (_lastEvent != null) {
_openMenu(context, _lastEvent!);
return;
}
if (kDebugMode) {
print("Last event was null, cannot open menu");
}
},
);
}
#override
void initState() {
super.initState();
html.document.onContextMenu.listen((event) => event.preventDefault());
}
/// Callback when mouse clicked on `Listener` wrapped widget.
Future<void> _onPointerDown(BuildContext context, PointerDownEvent event) async {
_lastEvent = event;
if (widget.menuItems == null) {
return;
}
// Check if right mouse button clicked
if (event.kind == PointerDeviceKind.mouse && event.buttons == kSecondaryMouseButton) {
return await _openMenu(context, event);
}
}
_openMenu(BuildContext context, PointerDownEvent event) async {
final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox;
final menuItem = await showMenu<String>(
context: context,
items: widget.menuItems ?? [],
position: RelativeRect.fromSize(event.position & Size(48.0, 48.0), overlay.size),
);
widget.onMenuItemTapped!(menuItem);
}
}
The class for standard splash effect touches
import 'package:flutter/material.dart';
class SplashEffect extends StatelessWidget {
final Widget child;
final Function() onTap;
final Function()? onLongPress;
final BorderRadius? borderRadius;
final bool isDisabled;
const SplashEffect({
Key? key,
required this.child,
required this.onTap,
this.isDisabled = false,
this.onLongPress,
this.borderRadius = const BorderRadius.all(Radius.circular(6)),
}) : super(key: key);
#override
Widget build(BuildContext context) {
if (isDisabled) {
return child;
}
return Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: borderRadius,
child: child,
onTap: onTap,
onLongPress: onLongPress,
),
);
}
}
And how to use it:
return CrossPlatformClick(
onNormalTap: onTapped,
menuItems: [
PopupMenuItem(child: Text('Copy Name', style: TextStyle(fontSize: 16)), value: "copied"),
],
onMenuItemTapped: (item) {
print("item tapped: " + (item ?? "-no-item"));
},
child:
Related
using keyboard like we do (ctl+a) to select and clear all text. how to achieve this in flutter web - specially inside the textfield
Any one have flutter web advance features list?
which are features different from flutter web and mobile ?
This is possible with actions, see the example from flutter docs:
https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// A text field that also has buttons to select all the text and copy the
/// selected text to the clipboard.
class CopyableTextField extends StatefulWidget {
const CopyableTextField({super.key, required this.title});
final String title;
#override
State<CopyableTextField> createState() => _CopyableTextFieldState();
}
class _CopyableTextFieldState extends State<CopyableTextField> {
late TextEditingController controller = TextEditingController();
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
ClearIntent: ClearAction(controller),
CopyIntent: CopyAction(controller),
SelectAllIntent: SelectAllAction(controller),
},
child: Builder(builder: (context) {
return Scaffold(
body: Center(
child: Row(
children: <Widget>[
const Spacer(),
Expanded(
child: TextField(controller: controller),
),
IconButton(
icon: const Icon(Icons.copy),
onPressed:
Actions.handler<CopyIntent>(context, const CopyIntent()),
),
IconButton(
icon: const Icon(Icons.select_all),
onPressed: Actions.handler<SelectAllIntent>(
context, const SelectAllIntent()),
),
const Spacer(),
],
),
),
);
}),
);
}
}
/// A ShortcutManager that logs all keys that it handles.
class LoggingShortcutManager extends ShortcutManager {
#override
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
final KeyEventResult result = super.handleKeypress(context, event);
if (result == KeyEventResult.handled) {
print('Handled shortcut $event in $context');
}
return result;
}
}
/// An ActionDispatcher that logs all the actions that it invokes.
class LoggingActionDispatcher extends ActionDispatcher {
#override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
super.invokeAction(action, intent, context);
return null;
}
}
/// An intent that is bound to ClearAction in order to clear its
/// TextEditingController.
class ClearIntent extends Intent {
const ClearIntent();
}
/// An action that is bound to ClearIntent that clears its
/// TextEditingController.
class ClearAction extends Action<ClearIntent> {
ClearAction(this.controller);
final TextEditingController controller;
#override
Object? invoke(covariant ClearIntent intent) {
controller.clear();
return null;
}
}
/// An intent that is bound to CopyAction to copy from its
/// TextEditingController.
class CopyIntent extends Intent {
const CopyIntent();
}
/// An action that is bound to CopyIntent that copies the text in its
/// TextEditingController to the clipboard.
class CopyAction extends Action<CopyIntent> {
CopyAction(this.controller);
final TextEditingController controller;
#override
Object? invoke(covariant CopyIntent intent) {
final String selectedString = controller.text.substring(
controller.selection.baseOffset,
controller.selection.extentOffset,
);
Clipboard.setData(ClipboardData(text: selectedString));
return null;
}
}
/// An intent that is bound to SelectAllAction to select all the text in its
/// controller.
class SelectAllIntent extends Intent {
const SelectAllIntent();
}
/// An action that is bound to SelectAllAction that selects all text in its
/// TextEditingController.
class SelectAllAction extends Action<SelectAllIntent> {
SelectAllAction(this.controller);
final TextEditingController controller;
#override
Object? invoke(covariant SelectAllIntent intent) {
controller.selection = controller.selection.copyWith(
baseOffset: 0,
extentOffset: controller.text.length,
affinity: controller.selection.affinity,
);
return null;
}
}
/// The top level application class.
///
/// Shortcuts defined here are in effect for the whole app,
/// although different widgets may fulfill them differently.
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String title = 'Shortcuts and Actions Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const ClearIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
const CopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
const SelectAllIntent(),
},
child: const CopyableTextField(title: title),
),
);
}
}
void main() => runApp(const MyApp());
I am working in Riverpod Auth flow boilerplate application.
I want to use common loading screen for all async function even login and logout. Currently I have AppState provider if Appstate loading i show loading screen. it's working fine for login but i wonder it’s good way or bad way.
Can i use this loading screen for all async task in the App?
AuthWidget:
class AuthWidget extends ConsumerWidget {
const AuthWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
AppState appState = ref.watch(appStateProvider);
if(appState.isLoading){
return const Center(child: CircularProgressIndicator(color: Colors.red),);
}
return appState.isAuthenticated ? const HomePage() : const SignIn();
}
}
AppState:
class AppState {
User? user;
bool isLoading;
bool isAuthenticated;
AppState(this.user, this.isLoading, this.isAuthenticated);
}
AuthRepository:
class AuthRepository extends StateNotifier<AppState>{
AuthRepository() : super(AppState(null,false,false));
Future<void> signIn()async {
state = AppState(null,true,false);
await Future.delayed(const Duration(seconds: 3));
User user = User(userName: 'FakeUser', email: 'user#gmail.com');
AppState appState = AppState(user, false, true);
state = appState;
}
}
final appStateProvider = StateNotifierProvider<AuthRepository,AppState>((ref){
return AuthRepository();
});
To answer your question : Yes you can.
The only thing I'd change here is the content of your AppState : I'd use a LoadingState dedicated to trigger your Loader instead.
Here is how I like to manage screens with a common loader in my apps.
1 - Create a LoadingState and provide it
final loadingStateProvider = ChangeNotifierProvider((ref) => LoadingState());
class LoadingState extends ChangeNotifier {
bool isLoading = false;
void startLoader() {
if (!isLoading) {
isLoading = true;
notifyListeners();
}
}
void stopLoader() {
if (isLoading) {
isLoading = false;
notifyListeners();
}
}
}
2 - Define a base page with the "common" loader
class LoadingContainer extends ConsumerWidget {
const LoadingContainer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(loadingStateProvider);
return Stack(
children: [
child,
if (state.isLoading)
const Center(child: CircularProgressIndicator())
else
const SizedBox(),
],
);
}
}
3 - Implement this widget whenever I need to handle loading datas.
return Scaffold(
backgroundColor: AppColor.blue,
body: LoadingContainer(
child: ...
And then I simply have to update my loadingStateProvider and it's isLoading value from a Controller or the Widget directly
If you want a centralized/common async calls, the InheritedWidget is ideal for that, you can just add a method and call it from anywhere down stream and because the call is offloaded with async, you can attach extra arguments and add usefull functionality such as a live update instead of relying on stuff like .then(). This example might not be as simple as FDuhen's but you can mix them together if you want to not use keys
AppState now is a widget and contains trigers that rely on global keys to rebuild the correct components, here i assumed that you actualy want to have an common overlay and not a loading screen widget, if not using a Navigator would be batter
Using keys is specially good if you end up implementing something this line, <token> been just a number that references a group of widgets
key: AppState.of(ctx).rebuild_on_triger(<token>)
class App_State_Data {
GlobalKey? page_key;
bool is_logged = false;
bool loading_overlay = false;
String loading_message = '';
}
class AppState extends InheritedWidget {
final App_State_Data _state;
bool get is_logged => _state.is_logged;
bool get should_overlay => _state.loading_overlay;
String get loading_message => _state.loading_message;
void page_rebuild() {
(_state.page_key!.currentState as _Page_Base).rebuild();
}
GlobalKey get page_key {
if (_state.page_key == null) {
_state.page_key = GlobalKey();
}
return _state.page_key!;
}
void place_overlay(String msg) {
_state.loading_message = msg;
_state.loading_overlay = true;
page_rebuild();
}
void clear_overlay() {
_state.loading_message = '';
_state.loading_overlay = false;
page_rebuild();
}
Future<void> triger_login(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 2));
_state.is_logged = true;
clear_overlay();
}
Future<void> triger_logout(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 1));
_state.is_logged = false;
clear_overlay();
}
AppState({Key? key, required Widget child})
: this._state = App_State_Data(),
super(key: key, child: child);
static AppState of(BuildContext ctx) {
final AppState? ret = ctx.dependOnInheritedWidgetOfExactType<AppState>();
assert(ret != null, 'No AppState found!');
return ret!;
}
#override
bool updateShouldNotify(AppState old) => true;
}
Here i added it as the topmost element making it like a global data class with is not necessary, you can split the state content and add just the necessary to where its needed
void main() => runApp(AppState(child: App()));
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
body: Page_Base(
key: AppState.of(ctx).page_key,
),
),
);
}
}
class Page_Base extends StatefulWidget {
final GlobalKey key;
const Page_Base({
required this.key,
}) : super(key: key);
#override
_Page_Base createState() => _Page_Base();
}
class _Page_Base extends State<Page_Base> {
Widget build_overlay(BuildContext ctx) {
return Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: Color(0xC09E9E9E),
child: Center(
child: Text(AppState.of(ctx).loading_message),
),
),
);
}
#override
Widget build(BuildContext ctx) {
return Stack(
children: [
AppState.of(ctx).is_logged ? Page_Home() : Page_Login(),
AppState.of(ctx).should_overlay ? build_overlay(ctx) : Material(),
],
);
}
void rebuild() {
// setState() is protected and can not be called
// from outside of the this. scope
setState(() => null);
}
}
Using AppState is the best part, just because the widget does not have to call more than 1 function and it will rebuild with the correct data on complition
class Page_Login extends StatelessWidget {
const Page_Login({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_login('Login'),
child: Container(
width: 200,
height: 200,
color: Colors.greenAccent,
child: Text('Page_Login'),
),
),
);
}
}
class Page_Home extends StatelessWidget {
const Page_Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_logout('Logout'),
child: Container(
width: 200,
height: 200,
color: Colors.blueAccent,
child: Text('Page_Home'),
),
),
);
}
}
Global loading indicator
If you want a centralized loading indicator to use in your whole app you could take advantage of Overlay's, which flutter already uses for dialogs, popups, bottom sheets etc. This way we don't introduce new widget in the widget tree.
If you only want to toggle between loading states you can use a StateProvider to handle the simple boolean value, else you could create a State/Change Notifier. This way you decouple your loading state from your AppState
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(const ProviderScope(child: MaterialApp(home: GlobalLoadingIndicator(child: Home()))));
// This widget should wrap your entire app, but be below MaterialApp in order to have access to the Overlay
class GlobalLoadingIndicator extends ConsumerStatefulWidget {
final Widget child;
const GlobalLoadingIndicator({required this.child, Key? key}) : super(key: key);
#override
ConsumerState createState() => _GlobalLoadingIndicatorState();
}
class _GlobalLoadingIndicatorState extends ConsumerState<GlobalLoadingIndicator> {
//We need to cache the overlay entries we are showing as part of the indicator in order to remove them when the indicator is hidden.
final List<OverlayEntry> _entries = [];
#override
Widget build(BuildContext context) {
ref.listen<bool>(loadingProvider, (previous, next) {
// We just want to make changes if the states are different
if (previous == next) return;
if (next) {
// Add a modal barrier so the user cannot interact with the app while the loading indicator is visible
_entries.add(OverlayEntry(builder: (_) => ModalBarrier(color: Colors.black12.withOpacity(.5))));
_entries.add(OverlayEntry(
builder: (_) =>const Center(
child: Card(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator())))));
// Insert the overlay entries into the overlay to actually show the loading indicator
Overlay.of(context)?.insertAll(_entries);
} else {
// Remove the overlay entries from the overlay to hide the loading indicator
_entries.forEach((e) => e.remove());
// Remove the cached overlay entries from the widget state
_entries.clear();
}
});
return widget.child;
}
}
We insert the GlobalLoadingIndicator high up in the widget tree although anywhere below the MaterialApp is fine (as long as it can access the Overlay via context).
The GlobalLoadingIndicator wont create extra widgets in the widget tree, and will only manage the overlays, here I add two overlays, one is a ModalBarrier which the user from interacting with widgets behind itself. And the other the actual LoadingIndicator. You are free to not add the ModalBarrier, or make it dismissible (or even if you decide to create a more complex loadingProvider, customize it in case you need to cater different use cases).
A sample usage after you have this set up is just switching the state of the loadingProvider, most of the times you would do this programatically, but for interactiveness I'll use a Switch :
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
Future.delayed(const Duration(seconds: 4)).then((value) {
ref.read(loadingProvider.notifier).state = false;
});
},
title: const FlutterLogo(),
),
));
}
}
You can fiddle with this snippet in dartpad
Result:
Per Screen/Section loading indicator
As a side note when displaying loading states inside components of the app I recommend you to use an AnimatedSwitcher , as it fades between the widgets , super handy when dealing with screens which can change content abruptly.
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(ProviderScope(child: MaterialApp(home: Home())));
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
},
title: AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: isLoading?CircularProgressIndicator():FlutterLogo()
),
),
));
}
}
I want to create an Interface in which it is possible to drag your finger over several Areas. This changes the state of the areas to a selected state (See images).
What is the best way to approach this?
Start Position:
Start Dragging:
Select First Area:
Selected All Areas:
The code needs some updates for current Flutter/Dart versions but this worked for me.
Updated code:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Grid(),
);
}
}
class Grid extends StatefulWidget {
#override
GridState createState() {
return new GridState();
}
}
class GridState extends State<Grid> {
final Set<int> selectedIndexes = Set<int>();
final key = GlobalKey();
final Set<_Foo> _trackTaped = Set<_Foo>();
_detectTapedItem(PointerEvent event) {
final RenderBox box = key.currentContext!.findAncestorRenderObjectOfType<RenderBox>()!;
final result = BoxHitTestResult();
Offset local = box.globalToLocal(event.position);
if (box.hitTest(result, position: local)) {
for (final hit in result.path) {
/// temporary variable so that the [is] allows access of [index]
final target = hit.target;
if (target is _Foo && !_trackTaped.contains(target)) {
_trackTaped.add(target);
_selectIndex(target.index);
}
}
}
}
_selectIndex(int index) {
setState(() {
selectedIndexes.add(index);
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _detectTapedItem,
onPointerMove: _detectTapedItem,
onPointerUp: _clearSelection,
child: GridView.builder(
key: key,
itemCount: 6,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemBuilder: (context, index) {
return Foo(
index: index,
child: Container(
color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
),
);
},
),
);
}
void _clearSelection(PointerUpEvent event) {
_trackTaped.clear();
setState(() {
selectedIndexes.clear();
});
}
}
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({required Widget child, required this.index, Key? key}) : super(child: child, key: key);
#override
_Foo createRenderObject(BuildContext context) {
return _Foo(index);
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
_Foo(this.index);
}
I use Rect class.
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class StackOverflow extends StatefulWidget {
const StackOverflow({Key? key}) : super(key: key);
#override
_StackOverflowState createState() => _StackOverflowState();
}
class _StackOverflowState extends State<StackOverflow> {
late List<bool> isSelected;
late List<GlobalKey> myGlobalKey;
late List<Offset> offsetWidgets;
late List<Size> sizeWidgets;
late List<Rect> listRect;
#override
void initState() {
super.initState();
isSelected = List.generate(3, (index) => false);
myGlobalKey = List.generate(3, (index) => GlobalKey());
offsetWidgets = <Offset>[];
sizeWidgets = <Size>[];
listRect = <Rect>[];
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
for (final key in myGlobalKey) {
sizeWidgets
.add((key.currentContext!.findRenderObject() as RenderBox).size);
offsetWidgets.add((key.currentContext!.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero));
}
for (int i = 0; i < 3; i++) {
final dx = offsetWidgets[i].dx + sizeWidgets[i].width;
final dy = offsetWidgets[i].dy + sizeWidgets[i].height;
listRect.add(Rect.fromPoints(offsetWidgets[i], Offset(dx, dy)));
}
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
if (listRect[0].contains(pointerMoveEvent.position)) {
if (!isSelected[0]) {
setState(() {
isSelected[0] = true;
});
}
} else if (listRect[1].contains(pointerMoveEvent.position)) {
if (!isSelected[1]) {
setState(() {
isSelected[1] = true;
});
}
} else if (listRect[2].contains(pointerMoveEvent.position)) {
if (!isSelected[2]) {
setState(() {
isSelected[2] = true;
});
}
}
},
child: Container(
color: Colors.amber,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RawMaterialButton(
key: myGlobalKey[0],
fillColor: isSelected[0] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[0] = false;
});
},
),
RawMaterialButton(
key: myGlobalKey[1],
fillColor: isSelected[1] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[1] = false;
});
},
),
RawMaterialButton(
key: myGlobalKey[2],
fillColor: isSelected[2] ? Colors.blueGrey : Colors.transparent,
shape:
const CircleBorder(side: BorderSide(color: Colors.blueGrey)),
onPressed: () {
setState(() {
isSelected[2] = false;
});
},
),
],
),
),
);
}
}
This package: drag_select_grid_view offers another related approach. From the code, you can see among other fun things:
Use of a GestureDetector to encapsulate the selection area (a GridView here)
In the GridView.itemBuilder, a custom ProxyWidget (his Selectable) wraps the normal widget builder for your selectable items. This is used to expose mount/unmount points in order to hang onto the corresponding custom ProxyElement.
When a tap/motion is detected, he uses the current context to get a RenderObject covering the selection area to do manual hit testing with the local position Offset, by checking if any of the cached Elements contain the point, using each item's bounding box in the selection area's coordinate system. (see _findIndexOfSelectable and Selectable.containsOffset) (This is like #mario's answer, and presumably could be costlier than #a.shak's if there are many possible Elements to choose from on
screen.)
Results are passed back to the user through a ValueNotifier, which also lets the user control clearing or setting a custom the selection. (see the controller code)
For contrast, I'll try to describe #a.shak's answer in words:
In his GridState class wrap a Listener around the subtree representing your selection area. (although a GestureDetector could work, too)
in onPointerDown|Move, start detecting; onPointerUp you can clear/etc.
Detection requires getting the subtree’s RenderBox (a RenderObject) so you can do hitTesting with the pointer’s local position to find other intersecting ROs. Given the selection area's RB, convert the pointer to its local coordinates and do the RenderBox.hitTest, then walk along the BoxHitTestResult.path of intersecting objects to check whether any HitTestEntry is of a type we know can be selected. (i.e. the _Foo extends RenderProxyBox class - see below)
If it's a match, success! Track its info for UI updates and later use elsewhere.
Use a GlobalKey with the GridView to get the RenderBox corresponding to the selection area’s extents during hit testing. (Probably don’t need this as you can use the State’s own context…)
In The GridView.itemBuilder, wrap your selectable objects in a custom SingleChildRenderObjectWidget, used to get the item's RenderBox for hit testing and storing info.
Store info here like your item's index and push it down into a custom RenderBox that our SCROW creates.
Uses a RenderProxyBox since we don’t actually care about controlling the rendering; just delegate it all to the child. This custom class also lets us find our selectable object(s) of interest more easily during the hit test (see _detectTapedItem).
So in both cases, you need to implement some extra custom classes (ProxyWidget+ProxyElement vs SingleChildRenderObjectWidget+RenderProxyBox) in order to get the right RenderBoxes for hit testing with the selected point on the screen, and to store misc info like an item's index to update the UI and use later.
For custom shapes, you can have your CustomPainter override its hitTest method to leverage Path.contains() to restrict touches to be within the path only. See this answer. Or just use a a package like touchable to give your shapes gesture callbacks.
So, I'm new to Flutter and was trying to code a simple notes app to learn my way around it. The layout of the app is a HomePage with a ListView of NoteTiles that when tapped open a corresponding NotePage where you can write down things. The issue I got is that every time I leave the NotePage and then re-open it from the HomePage, the NotePage loses its content.
My first idea was to keep the content in the corresponding NoteTile so that when I leave the NotePage I would pop with the content, and when needed I would push to the NotePage with the previously saved content. The problem is that I didn't find any simple way to push and set the content. I've seen there are Notification and Route methods but they come with quite a lot of boilerplate and they look like it's more for passing data from child to parent, which I can do easily when popping.
So, is there a way to avoid the reset of the content of the NotePage? Or maybe is there a simple way to initState with the content previously saved?
Here is the code I have so far:
class NoteTile extends ListTile {
final NotePage note;
final Text title;
final BuildContext context;
NoteTile(this.title, this.note, this.context) : super(
title: title,
onTap: () => {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => note),
),
},
onLongPress: () => null,
);
void switchToNote() async {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => note),
);
}
}
onLongPress will later be used to delete the note.
class NotePage extends StatefulWidget {
final String title;
NotePage({Key key, this.title}) : super(key: key);
#override
_NotePageState createState() => _NotePageState();
}
class _NotePageState extends State<NotePage> {
TextEditingController _controller;
String _value;
void initState() {
super.initState();
_controller = TextEditingController();
_controller.addListener(_updateValue);
_value = '';
}
void dispose() {
_controller.dispose();
super.dispose();
}
void _updateValue(){
_value = _controller.text;
}
Future<bool> _onWillPop() async {
Navigator.pop(context, _value);
return true;
}
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintStyle: TextStyle(fontStyle: FontStyle.italic),
hintText: 'New note',
),
maxLines: null,
controller: _controller,
),
padding: EdgeInsets.all(12.0),
),
),
onWillPop: _onWillPop,
);
}
}
The _onWillPop is to send back the content to the NoteTile, which currently disregards the return data because I failed to find a way to use that data later when pushing the NotePage again.
ListTile is StatelessWidget widget, so you cannot reserve the state, the NoteTile should be StatefulWidget.
Here is a sample of NoteTile:
class NoteTile extends StatefulWidget {
final Text title;
const NoteTile({Key key, this.title}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _NoteTileState();
}
}
class _NoteTileState extends State<NoteTile> {
String _result;
#override
void initState() {
super.initState();
_result = "";
}
void _onOpenNote() async {
String result = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) => NotePage(title: _result)),
);
if (result != null) {
_result = result;
}
}
#override
Widget build(BuildContext context) {
return ListTile(
title: widget.title,
onTap: _onOpenNote,
);
}
}
And NotePage should be edit in some lines:
TextEditingController can have initialize string & have to initialize by title:
_controller = TextEditingController(text: widget.title);
Navigator.pop can post result and you did it correctly, but if you want to get the result, should wait for Navigator.of(context).push, because its run in another thread.
I'm using the flutter url_launcher https://pub.dev/packages/url_launcher package to open urls when i click some button.
With the new Link widget im now able to open a web page on the same tab but i cant add mouse pointer when user is hovering the button
import 'package:bianca/UI/botao_azul.dart';
import 'package:url_launcher/link.dart';
import 'package:flutter/material.dart';
String link = "https://www.google.com";
class MesmaAba extends StatelessWidget {
final double tamanho;
final String conteudo;
MesmaAba({this.tamanho, this.conteudo});
#override
Widget build(BuildContext context) {
return Link(
uri: Uri.parse(link),
builder: (BuildContext context, FollowLink followLink) => BotaoAzul(
conteudo: conteudo,
tamanho: tamanho,
funcao: followLink
),
);
}
}
BotaoAzul class:
import 'package:flutter/material.dart';
class BotaoAzul extends StatelessWidget {
final String conteudo;
final double tamanho;
final Function funcao;
BotaoAzul({this.conteudo, this.tamanho,this.funcao});
#override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FlatButton(
onPressed: funcao,
child: Text(conteudo,
style: TextStyle(
fontSize: tamanho,
color: Colors.white,
fontWeight: FontWeight.bold))),
),
decoration: BoxDecoration(
color: Colors.blue[900], borderRadius: BorderRadius.circular(20.0)),
);
}
}
I can already open urls with botaoAzul button on another tab using this function (and without the Link widget, the mouse changes on hovering the button)
import 'package:url_launcher/url_launcher.dart';
void launchLink(String link) async {
await launch(
link,
);
}
But i need to open the url on the same tab.
I've already tried all implementations of this other question without success:
https://stackoverflow.com/questions/56211844/flutter-web-mouse-hover-change-cursor-to-pointer
As I know latest version of flutter web supports hand cursor for InkWell widget automatically. Below simple class:
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
/// Provides an anchor link to web URL.
class HoveredWebAnchor extends StatefulWidget {
HoveredWebAnchor(
{Key key,
#required this.label,
#required this.url,
this.underlined = true})
: assert(label != null),
assert(url != null),
assert(underlined != null),
super(key: key);
/// The label of anchor
final String label;
/// The web URL to open when anchor clicked
final String url;
/// Identifies if anchor label will be underlined.
final bool underlined;
#override
_HoveredWebAnchorState createState() => _HoveredWebAnchorState();
}
class _HoveredWebAnchorState extends State<HoveredWebAnchor> {
/// Current text style
TextStyle _textStyle;
#override
Widget build(BuildContext context) {
return InkWell(
hoverColor: Colors.transparent,
child: Text(
widget.label,
style: _textStyle,
),
onHover: (hovered) {
setState(() {
if (hovered) {
_textStyle = TextStyle(color: Theme.of(context).accentColor);
if (widget.underlined) {
_textStyle = _textStyle.copyWith(
decoration: TextDecoration.underline,
);
}
} else {
_textStyle = null;
}
});
},
onTap: () {
launch(widget.url, forceWebView: true);
},
);
}
}
Using:
HoveredWebAnchor(
label: 'Open Google',
url: 'http://www.google.com',
),
I have improved suggestion of #BambinoUA to sound null safety and some minor changes so I decided to share it with y'all
class HoveredWebAnchor extends StatefulWidget {
const HoveredWebAnchor(
this.label, {
Key? key,
required this.style,
this.maxLines,
required this.onTap,
}) : super(key: key);
final String label;
final TextStyle? style;
final int? maxLines;
final VoidCallback onTap;
#override
_HoveredWebAnchorState createState() => _HoveredWebAnchorState();
}
class _HoveredWebAnchorState extends State<HoveredWebAnchor> {
TextStyle? _textStyle;
#override
void initState() {
_textStyle = widget.style;
super.initState();
}
#override
Widget build(BuildContext context) {
return InkWell(
hoverColor: Colors.transparent,
onHover: (hovered) {
setState(() {
if (hovered) {
_textStyle = _textStyle?.copyWith(
decoration: TextDecoration.underline,
);
} else {
_textStyle = _textStyle?.copyWith(
decoration: widget.style?.decoration,
);
}
});
},
onTap: widget.onTap,
child: Text(
widget.label,
style: _textStyle,
maxLines: widget.maxLines,
),
);
}
}
The way to change your mouse cursor whilst keeping the behavior of the Link Widget the same would be to wrap the Link Widget in a MouseRegion
MouseRegion(
cursor: SystemMouseCursors.click,
child: Link(
uri: Uri.parse(link),
builder: (BuildContext context, FollowLink followLink) =>
BotaoAzul(
conteudo: conteudo,
tamanho: tamanho,
funcao: followLink
),
),
)
From the Link widget revision 2 document:
The Link widget doesn’t provide any mouse cursor, and fully relies on the user to do their own mouse cursor. In many cases, users will be using a button, which already shows the correct mouse cursor. In other cases, the user can wrap the Link (or the child of the Link) in a mouse region and give it a cursor.
found something last night that solves the problem:
Instead of using url_launcher Link, i'm now importing the html package
import 'dart:html' as html;
String link = "https://www.google.com";
.....
void openPage(){
html.window.location.assign(link);
}
...... (widget build method)
BotaoAzul(
conteudo: "Hey",
tamanho: 30,
funcao: openPage
),
It now opens the link on the same tab and i can return to my flutter app from the chrome back button