Is it possible "extend" ThemeData in Flutter - dart
I might very well be missing something as I'm so new to flutter, but I'm finding ThemeData's options very limited (at least with my understanding of how to implement it).
If you look at this random design below from MaterialUp, I'd want to model something roughly like:
Themedata.cyclingColor = Color.pink;
ThemeData.runningColor = Color.green;
That way everywhere in my app I can reference cycling, running, swimming, gym colors (Or whatever colors make sense in the context of my app/design) and keep things consistent.
Is there a recommended way to achieve this currently in Flutter? What are my options?
I recommend this approach, which is simple, works with hot reload and can be easily extended to support switching between dark and light themes.
First create your own analog to ThemeData, let's call it AppThemeData:
class AppThemeData {
final BorderRadius borderRadius = BorderRadius.circular(8);
final Color colorYellow = Color(0xffffff00);
final Color colorPrimary = Color(0xffabcdef);
ThemeData get materialTheme {
return ThemeData(
primaryColor: colorPrimary
);
}
}
The materialTheme can be used whenever the standard ThemeData is needed.
Then create a widget called AppTheme, which provides an instance of AppThemeData using the provider package.
class AppTheme extends StatelessWidget {
final Widget child;
AppTheme({this.child});
#override
Widget build(BuildContext context) {
final themeData = AppThemeData(context);
return Provider.value(value: themeData, child: child);
}
}
Finally, wrap the whole app with AppTheme. To access the theme you can call context.watch<AppThemeData>(). Or create this extension...
extension BuildContextExtension on BuildContext {
AppThemeData get appTheme {
return watch<AppThemeData>();
}
}
... and use context.appTheme. I usually put final theme = context.appTheme; on the first line of the widget build method.
Updated for null-safety
I've extended standard ThemeData class so that at any time one could access own theme fields like that:
Theme.of(context).own().errorShade
Or like that:
ownTheme(context).errorShade
A theme can be defined and extended with new fields as follows(via addOwn() called on a certain ThemeData instance):
final ThemeData lightTheme = ThemeData.light().copyWith(
accentColor: Colors.grey.withAlpha(128),
backgroundColor: Color.fromARGB(255, 255, 255, 255),
textTheme: TextTheme(
caption: TextStyle(
fontSize: 17.0, fontFamily: 'Montserrat', color: Colors.black),
))
..addOwn(OwnThemeFields(
errorShade: Color.fromARGB(240, 255, 200, 200),
textBaloon: Color.fromARGB(240, 255, 200, 200)));
final ThemeData darkTheme = ThemeData.dark().copyWith( ...
...
Themes can be applied to MaterialApp widget in a conventional way:
MaterialApp(
...
theme: lightTheme,
darkTheme: darkTheme,
)
The idea is to put all custom fields required for theming in a separate class OwnThemeFields.
Then extend ThemeData class with 2 methods:
addOwn() that connects a certain instance of ThemedData to OwnThemeFields instance
own() that allows to lookup for own fields associated with the given theme data
Also ownTheme helper method can be created to shorten the extraction of own fields.
class OwnThemeFields {
final Color? errorShade;
final Color? textBaloon;
const OwnThemeFields({Color? errorShade, Color? textBaloon})
: this.errorShade = errorShade,
this.textBaloon = textBaloon;
factory OwnThemeFields.empty() {
return OwnThemeFields(errorShade: Colors.black, textBaloon: Colors.black);
}
}
extension ThemeDataExtensions on ThemeData {
static Map<InputDecorationTheme, OwnThemeFields> _own = {};
void addOwn(OwnThemeFields own) {
_own[this.inputDecorationTheme] = own;
}
static OwnThemeFields? empty = null;
OwnThemeFields own() {
var o = _own[this.inputDecorationTheme];
if (o == null) {
if (empty == null) empty = OwnThemeFields.empty();
o = empty;
}
return o!;
}
}
OwnThemeFields ownTheme(BuildContext context) => Theme.of(context).own();
Complete source: https://github.com/maxim-saplin/dikt/blob/master/lib/ui/themes.dart
2022: Use ThemeExtensions introduced in flutter 3
Here's a link! to the medium article I wrote.
Create your ThemeExtension class
import 'package:flutter/material.dart';
#immutable
class MyCardTheme extends ThemeExtension<MyCardTheme> {
const MyCardTheme({
this.background = Colors.white,
this.shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
});
final Color background;
final ShapeBorder shape;
#override
MyCardTheme copyWith({
Color? background,
ShapeBorder? shape,
}) {
return MyCardTheme(
background: background ?? this.background,
shape: shape ?? this.shape,
);
}
#override
MyCardTheme lerp(ThemeExtension<MyCardTheme>? other, double t) {
if (other is! MyCardTheme) {
return this;
}
return MyCardTheme(
background: Color.lerp(background, other.background, t) ?? Colors.white,
shape: ShapeBorder.lerp(shape, other.shape, t) ??
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
);
}
#override
String toString() => 'MyCardTheme('
'background: $background, radius: $shape'
')';
}
Create dark and light themes as per requirements
MyCardTheme lightCardTheme = MyCardTheme(
background: Colors.blueGrey[200]!,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
);
MyCardTheme darkCardTheme = MyCardTheme(
background: Colors.blueGrey[800]!,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
);
Add extensions to your ThemeData for both, light and dark themes.
theme: ThemeData(
primarySwatch: Colors.green,
cardTheme: const CardTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
color: Colors.green,
),
extensions: <ThemeExtension<dynamic>>[
lightCardTheme,
],
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.green,
cardTheme: const CardTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
color: Colors.green,
),
extensions: <ThemeExtension<dynamic>>[
darkCardTheme,
],
),
Use them in your build methods
final MyCardTheme customCardTheme =
Theme.of(context).extension<MyCardTheme>()!;
Card(
shape: customCardTheme.shape,
color: customCardTheme.background,
child: Container(
padding: const EdgeInsets.all(16),
child: const Text('Card styled from custom theme')),
),
),
You can't extend ThemeData because then material components won't find it anymore.
You can just create and provide MyThemeData in addition to the ThemeData included in Flutter the same way.
Create a widget CustomThemeWidget that extends InheritedWidget and provide your custom theme there.
When you want to get a value from the current theme use
myTheme = CustomThemeWidget.of(context).myTheme;
To change the current theme change the MyThemeData in CustomThemeWidget.myTheme
Update
Like shown in https://github.com/flutter/flutter/pull/14793/files, it should be possible to extend ThemeData and provide it as ThemeData by overriding runtimeType
See also the comment in https://github.com/flutter/flutter/issues/16487#event-1573761656
Dart 2.7 later, extension support
you can add extension for system class
only add instance property is easy, but if you would get a dynamic color
you need think about it. for example, Use a constant to get the colors in light and dark modes
Determine if it is dark mode
two ways
MediaQuery.of(context).platformBrightnes == Brightness.dark;
Theme.of(context).brightness == Brightness.dark;
As you can see, you need the context, the context
Add Extension for BuildContext
Here is the code
extension MYContext on BuildContext {
Color dynamicColor({int light, int dark}) {
return (Theme.of(this).brightness == Brightness.light)
? Color(light)
: Color(dark);
}
Color dynamicColour({Color light, Color dark}) {
return (Theme.of(this).brightness == Brightness.light)
? light
: dark;
}
/// the white background
Color get bgWhite => dynamicColor(light: 0xFFFFFFFF, dark: 0xFF000000);
}
How to use
import 'package:flutter/material.dart';
import 'buildcontext_extension.dart';
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
color: context.bgWhite,
);
}
}
Also
This color may require multiple files, so you can create a public.dart file to manage it all
Like This
public.dart
library public;
// Export some common header files
// extensions
export 'buildcontext_extension.dart';
DarkMode images support
Put the light images in the same category as the dark ones
some code
static String getImgPath(String name, {
String folder = '',
String format = 'png',
bool isDark = false,
bool needDark = true
}) {
String finalImagePath;
if (needDark) {
final folderName = isDark ? '${folder}_dark' : folder;
finalImagePath = 'assets/images/$folderName/$name.$format';
} else {
finalImagePath = 'assets/images/$folder/$name.$format';
}
String isDarkPath = isDark ? "🌙 DarkMode" : "🌞 LightMode";
print('$isDarkPath imagePath 🖼 $finalImagePath');
return finalImagePath;
}
Instead of extending, you can use the new feature ThemeExtension in flutter.
We can add custom styling and even use class type theme configuration in css.
example:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
#immutable
class MyColors extends ThemeExtension<MyColors> {
const MyColors({
required this.brandColor,
required this.danger,
});
final Color? brandColor;
final Color? danger;
#override
MyColors copyWith({Color? brandColor, Color? danger}) {
return MyColors(
brandColor: brandColor ?? this.brandColor,
danger: danger ?? this.danger,
);
}
#override
MyColors lerp(ThemeExtension<MyColors>? other, double t) {
if (other is! MyColors) {
return this;
}
return MyColors(
brandColor: Color.lerp(brandColor, other.brandColor, t),
danger: Color.lerp(danger, other.danger, t),
);
}
// Optional
#override
String toString() => 'MyColors(brandColor: $brandColor, danger: $danger)';
}
void main() {
// Slow down time to see lerping.
timeDilation = 5.0;
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isLightTheme = true;
void toggleTheme() {
setState(() => isLightTheme = !isLightTheme);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: MyApp._title,
theme: ThemeData.light().copyWith(
extensions: <ThemeExtension<dynamic>>[
const MyColors(
brandColor: Color(0xFF1E88E5),
danger: Color(0xFFE53935),
),
],
),
darkTheme: ThemeData.dark().copyWith(
extensions: <ThemeExtension<dynamic>>[
const MyColors(
brandColor: Color(0xFF90CAF9),
danger: Color(0xFFEF9A9A),
),
],
),
themeMode: isLightTheme ? ThemeMode.light : ThemeMode.dark,
home: Home(
isLightTheme: isLightTheme,
toggleTheme: toggleTheme,
),
);
}
}
class Home extends StatelessWidget {
const Home({
Key? key,
required this.isLightTheme,
required this.toggleTheme,
}) : super(key: key);
final bool isLightTheme;
final void Function() toggleTheme;
#override
Widget build(BuildContext context) {
final MyColors myColors = Theme.of(context).extension<MyColors>()!;
return Material(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(width: 100, height: 100, color: myColors.brandColor),
const SizedBox(width: 10),
Container(width: 100, height: 100, color: myColors.danger),
const SizedBox(width: 50),
IconButton(
icon: Icon(isLightTheme ? Icons.nightlight : Icons.wb_sunny),
onPressed: toggleTheme,
),
],
)),
);
}
}
from Flutter API Documentation
I have also found that the ThemeData is restricting. What I have done, and will be doing for all of my apps in the future is creating my own ThemeData.
I have created a file named color_themes.dart and created a class named ColorThemes with constructors with the name of the colors that I desire. such as cyclingColor;
class ColorThemes {
static const cyclingColor = const Color(0xffb74093);
}
You can then call these colors by importing the file and calling ColorThemes.cyclingColor You can assign these values within your ThemeData to have these colors default to your ColorThemes. One of the benefits with using this method is that you do not need to use/reference context like so ThemeData.of(context) making it a lot easier to use your code in extracted widgets.
I solved this problem also for multiple themes by creating a CustomThemeData class like this:
class CustomThemeData {
final double imageSize;
CustomThemeData({
this.imageSize = 100,
});
}
Then, creating instances for each Theme:
final _customTheme = CustomThemeData(imageSize: 150);
final _customDarkTheme = CustomThemeData();
And writing an extension on ThemeData:
extension CustomTheme on ThemeData {
CustomThemeData get custom => brightness == Brightness.dark ? _customDarkTheme : _customTheme;
}
Finally, the value can be accessed like this:
Theme.of(context).custom.imageSize
For more information see: https://bettercoding.dev/flutter/tutorial-themes-riverpod/#custom_attributes_extending_themedata
use this lib adaptive_theme for theme switch.
And create extension of ColorSheme
extension MenuColorScheme on ColorScheme {
Color get menuBackground => brightness == Brightness.light
? InlLightColors.White
: InlDarkColors.Black;
}
In widget use that
Container(
color: Theme.of(context).colorScheme.menuBackground,
...
)
This way is very simple and elegance. Nice to codding.
A simple workaround if you are not using all the textTheme headlines you can set some colors of some of them and use them like you normally use other colors.
set the headline1 color:
ThemeData(textTheme: TextTheme(headline1: TextStyle(color: Colors.red),),),
Use it:
RawMaterialButton(fillColor: Theme.of(context).textTheme.headline1.color,onPressed: onPressed,)
I created an implementation analog to the implementation of ThemeData:
#override
Widget build(BuildContext context) {
final Brightness platformBrightness = Theme.of(context).brightness;
final bool darkTheme = platformBrightness == Brightness.dark;
return CustomAppTheme(
customAppTheme:
darkTheme ? CustomAppThemeData.dark : CustomAppThemeData.light,
child: Icon(Icons.add, color: CustomAppTheme.of(context).addColor,),
);
}
import 'package:calendarflutter/style/custom_app_theme_data.dart';
import 'package:flutter/material.dart';
class CustomAppTheme extends InheritedWidget {
CustomAppTheme({
Key key,
#required Widget child,
this.customAppTheme,
}) : super(key: key, child: child);
final CustomAppThemeData customAppTheme;
static CustomAppThemeData of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CustomAppTheme>()
.customAppTheme;
}
#override
bool updateShouldNotify(CustomAppTheme oldWidget) =>
customAppTheme != oldWidget.customAppTheme;
}
import 'package:flutter/material.dart';
class CustomAppThemeData {
final Color plusColor;
const CustomAppThemeData({
#required this.plusColor,
});
static CustomAppThemeData get dark {
return CustomAppThemeData(
plusColor: Colors.red,
);
}
static CustomAppThemeData get light {
return CustomAppThemeData(
plusColor: Colors.green,
);
}
}
To extend (pun not intended) the answer of Maxim Saplin:
You may encounter a problem, where theme stays on the last one initialized in your code. This is happening because InputDecorationTheme is always the same for all of yours themes.
What solved it for me, was changing key (InputDecorationTheme) in _own to something unique, like themeID (you'll have to implement it somehow).
Related
How to make 'stacked card list view' in flutter?
I want to build ui similar to this link in flutter. https://github.com/loopeer/CardStackView/blob/master/screenshot/screenshot1.gif Key ideal features are followings. Behave like list view, but cards should be stacked at the top of screen. List can have infinite items. So old cards should be recycled to save memory. I also want to set different size to each card. First, I found some 'tinder' like ui like following and tried them. https://blog.geekyants.com/tinder-swipe-in-flutter-7e4fc56021bc However, users need to swipe each single card, that required user to swipe many times to browse list items. And then I could somehow make a list view whose items are overlapped with next ones. import 'package:flutter/material.dart'; class StackedList extends StatelessWidget { List<ItemCard> cards = []; StackedList() { for (int i = 0; i < 20; i++) { cards.add(ItemCard(i)); } } #override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('title')), body: Container( child: ListView.builder( itemBuilder: (context, index) { return Align( alignment: Alignment.topCenter, heightFactor: 0.8, child: cards[index], ); }, itemCount: cards.length, ), ), ); } } class ItemCard extends StatelessWidget { int index; ItemCard(this.index); #override Widget build(BuildContext context) { return Container( decoration: const BoxDecoration( boxShadow: [ BoxShadow(color: Colors.black, blurRadius: 20.0), ], ), child: SizedBox.fromSize( size: const Size(300, 400), child: Card( elevation: 5.0, color: index % 2 == 0 ? Colors.blue : Colors.red, child: Center( child: Text(index.toString()), ), ), ), ); } } However items don't stop at the top of screen, which is not exactly what I want. I guess I can achieve this effect by customizing ScrollController or ScrollPhysics but I'm not sure where I should change.
You can achieve a similar behaviour with SliverPersistentHeader and a CustomScrollView, and you can wrap your cards in GestureDetector to modify their height by changing the value of SliverPersistentHeaderDelegate's maxExtent parameter. Here is a small app I wrote that achieves something that might look like what you are looking for: import 'package:flutter/material.dart'; import 'dart:math' as math; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. #override Widget build(BuildContext context) { return MaterialApp( title: 'Stacked list example', home: Scaffold( appBar: AppBar( title: Text("Stacked list example"), backgroundColor: Colors.black, ), body: StackedList()), ); } } class StackedList extends StatelessWidget { final List<Color> _colors = Colors.primaries; static const _minHeight = 16.0; static const _maxHeight = 120.0; #override Widget build(BuildContext context) => CustomScrollView( slivers: _colors .map( (color) => StackedListChild( minHeight: _minHeight, maxHeight: _colors.indexOf(color) == _colors.length - 1 ? MediaQuery.of(context).size.height : _maxHeight, pinned: true, child: Container( color: _colors.indexOf(color) == 0 ? Colors.black : _colors[_colors.indexOf(color) - 1], child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.vertical( top: Radius.circular(_minHeight)), color: color, ), ), ), ), ) .toList(), ); } class StackedListChild extends StatelessWidget { final double minHeight; final double maxHeight; final bool pinned; final bool floating; final Widget child; SliverPersistentHeaderDelegate get _delegate => _StackedListDelegate( minHeight: minHeight, maxHeight: maxHeight, child: child); const StackedListChild({ Key key, #required this.minHeight, #required this.maxHeight, #required this.child, this.pinned = false, this.floating = false, }) : assert(child != null), assert(minHeight != null), assert(maxHeight != null), assert(pinned != null), assert(floating != null), super(key: key); #override Widget build(BuildContext context) => SliverPersistentHeader( key: key, pinned: pinned, floating: floating, delegate: _delegate); } class _StackedListDelegate extends SliverPersistentHeaderDelegate { final double minHeight; final double maxHeight; final Widget child; _StackedListDelegate({ #required this.minHeight, #required this.maxHeight, #required this.child, }); #override double get minExtent => minHeight; #override double get maxExtent => math.max(maxHeight, minHeight); #override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return new SizedBox.expand(child: child); } #override bool shouldRebuild(_StackedListDelegate oldDelegate) { return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child; } } Here is how it looks like in action: Stacked list example .gif And here is a really good article about Flutter's slivers that might help you in this regard: Slivers, demystified Hope this helps you get in the right direction.
Flutter - PopupMenu on long press
I'm making an image gallery and I need the user to be able to long-press an image to show a popup menu which will let him delete the image. My code, so far: return GestureDetector( onLongPress: () { showMenu( items: <PopupMenuEntry>[ PopupMenuItem( value: this._index, child: Row( children: <Widget>[ Icon(Icons.delete), Text("Delete"), ], ), ) ], context: context, ); }, child: Image.memory( this._asset.thumbData.buffer.asUint8List(), fit: BoxFit.cover, gaplessPlayback: true, ), ); Which produces: But also, I couldn't find out how to completely remove the image's widget when the longPress function is called. How to do so?
The OP and the First Answerer bypassed the original problem using PopupMenuButton, which worked fine in their case. But I think the more general question of how to position one's own menu and how to receive the user's response without using PopupMenuButton is worth answering, because sometimes we want a popup menu on a custom widget, and we want it to appear on some gestures other than a simple tap (e.g. the OP's original intention was to long-press). I set out to make a simple app demonstrating the following: Use a GestureDetector to capture long-press Use the function showMenu() to display a popup menu, and position it near the finger's touch How to receive the user's selection (Bonus) How to make a PopupMenuEntry that represents multiple values (the oft-used PopupMenuItem can only represent a single value) The result is, when you long-press on a big yellow area, a popup menu appears on which you can select +1 or -1, and the big number would increment or decrement accordingly: Skip to the end for the entire body of code. Comments are sprinkled in there to explain what I am doing. Here are a few things to note: showMenu()'s position parameter takes some effort to understand. It's a RelativeRect, which represents how a smaller rect is positioned inside a bigger rect. In our case, the bigger rect is the entire screen, the smaller rect is the area of touch. Flutter positions the popup menu according to these rules (in plain English): if the smaller rect leans toward the left half of the bigger rect, the popup menu would align with the smaller rect's left edge if the smaller rect leans toward the right half of the bigger rect, the popup menu would align with the smaller rect's right edge if the smaller rect is in the middle, which edge wins depends on the language's text direction. Left edge wins if using English and other left-to-right languages, right edge wins otherwise. It's always useful to reference PopupMenuButton's official implementation to see how it uses showMenu() to display the menu. showMenu() returns a Future. Use Future.then() to register a callback to handle user selection. Another option is to use await. Remember that PopupMenuEntry is a (subclass of) StatefulWidget. You can layout any number of sub-widgets inside it. This is how you represent multiple values in a PopupMenuEntry. If you want it to represent two values, just make it contain two buttons, however you want to lay them out. To close the popup menu, use Navigator.pop(). Flutter treats popup menus like a smaller "page". When we display a popup menu, we are actually pushing a "page" to the navigator's stack. To close a popup menu, we pop it from the stack, thus completing the aforementioned Future. Here is the full code: import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { #override Widget build(BuildContext context) { return MaterialApp( title: 'Popup Menu Usage', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Popup Menu Usage'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; #override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { var _count = 0; var _tapPosition; void _showCustomMenu() { final RenderBox overlay = Overlay.of(context).context.findRenderObject(); showMenu( context: context, items: <PopupMenuEntry<int>>[PlusMinusEntry()], position: RelativeRect.fromRect( _tapPosition & const Size(40, 40), // smaller rect, the touch area Offset.zero & overlay.size // Bigger rect, the entire screen ) ) // This is how you handle user selection .then<void>((int delta) { // delta would be null if user taps on outside the popup menu // (causing it to close without making selection) if (delta == null) return; setState(() { _count = _count + delta; }); }); // Another option: // // final delta = await showMenu(...); // // Then process `delta` however you want. // Remember to make the surrounding function `async`, that is: // // void _showCustomMenu() async { ... } } void _storePosition(TapDownDetails details) { _tapPosition = details.globalPosition; } #override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ GestureDetector( // This does not give the tap position ... onLongPress: _showCustomMenu, // Have to remember it on tap-down. onTapDown: _storePosition, child: Container( color: Colors.amberAccent, padding: const EdgeInsets.all(100.0), child: Text( '$_count', style: const TextStyle( fontSize: 100, fontWeight: FontWeight.bold), ), ), ), ], ), ), ); } } class PlusMinusEntry extends PopupMenuEntry<int> { #override double height = 100; // height doesn't matter, as long as we are not giving // initialValue to showMenu(). #override bool represents(int n) => n == 1 || n == -1; #override PlusMinusEntryState createState() => PlusMinusEntryState(); } class PlusMinusEntryState extends State<PlusMinusEntry> { void _plus1() { // This is how you close the popup menu and return user selection. Navigator.pop<int>(context, 1); } void _minus1() { Navigator.pop<int>(context, -1); } #override Widget build(BuildContext context) { return Row( children: <Widget>[ Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))), Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))), ], ); } }
If you are going to use a gridView or listview for laying out the images on the screen, you can wrap each item with a gesture detector then you should keep your images in a list somewhere, then simply remove the image from the list and call setState(). Something like the following. (This code will probably won't compile but it should give you the idea) ListView.builder( itemCount: imageList.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( onLongPress: () { showMenu( onSelected: () => setState(() => imageList.remove(index))} items: <PopupMenuEntry>[ PopupMenuItem( value: this._index, child: Row( children: <Widget>[ Icon(Icons.delete), Text("Delete"), ], ), ) ], context: context, ); }, child: imageList[index], ); } ) Edit: You can use a popup menu too, like following Container( margin: EdgeInsets.symmetric(vertical: 10), height: 100, width: 100, child: PopupMenuButton( child: FlutterLogo(), itemBuilder: (context) { return <PopupMenuItem>[new PopupMenuItem(child: Text('Delete'))]; }, ), ),
Building on the answers by Nick Lee and hacker1024, but instead of turning the solution into a mixin, you could simply just turn it into a widget: class PopupMenuContainer<T> extends StatefulWidget { final Widget child; final List<PopupMenuEntry<T>> items; final void Function(T) onItemSelected; PopupMenuContainer({#required this.child, #required this.items, #required this.onItemSelected, Key key}) : super(key: key); #override State<StatefulWidget> createState() => PopupMenuContainerState<T>(); } class PopupMenuContainerState<T> extends State<PopupMenuContainer<T>>{ Offset _tapDownPosition; #override Widget build(BuildContext context) { return GestureDetector( onTapDown: (TapDownDetails details){ _tapDownPosition = details.globalPosition; }, onLongPress: () async { final RenderBox overlay = Overlay.of(context).context.findRenderObject(); T value = await showMenu<T>( context: context, items: widget.items, position: RelativeRect.fromLTRB( _tapDownPosition.dx, _tapDownPosition.dy, overlay.size.width - _tapDownPosition.dx, overlay.size.height - _tapDownPosition.dy, ), ); widget.onItemSelected(value); }, child: widget.child ); } } And then you'd use it like this: child: PopupMenuContainer<String>( child: Image.asset('assets/image.png'), items: [ PopupMenuItem(value: 'delete', child: Text('Delete')) ], onItemSelected: (value) async { if( value == 'delete' ){ await showDialog(context: context, child: AlertDialog( title: Text('Delete image'), content: Text('Are you sure you want to delete the image?'), actions: [ uiFlatButton(child: Text('NO'), onTap: (){ Navigator.of(context).pop(false); }), uiFlatButton(child: Text('YES'), onTap: (){ Navigator.of(context).pop(true); }), ], )); } }, ), Adjust the code to fit your needs.
Nick Lee's answer can be turned into a mixin quite easily, which can then be used anywhere you want to use a popup menu. The mixin: import 'package:flutter/material.dart' hide showMenu; import 'package:flutter/material.dart' as material show showMenu; /// A mixin to provide convenience methods to record a tap position and show a popup menu. mixin CustomPopupMenu<T extends StatefulWidget> on State<T> { Offset _tapPosition; /// Pass this method to an onTapDown parameter to record the tap position. void storePosition(TapDownDetails details) => _tapPosition = details.globalPosition; /// Use this method to show the menu. Future<T> showMenu<T>({ #required BuildContext context, #required List<PopupMenuEntry<T>> items, T initialValue, double elevation, String semanticLabel, ShapeBorder shape, Color color, bool captureInheritedThemes = true, bool useRootNavigator = false, }) { final RenderBox overlay = Overlay.of(context).context.findRenderObject(); return material.showMenu<T>( context: context, position: RelativeRect.fromLTRB( _tapPosition.dx, _tapPosition.dy, overlay.size.width - _tapPosition.dx, overlay.size.height - _tapPosition.dy, ), items: items, initialValue: initialValue, elevation: elevation, semanticLabel: semanticLabel, shape: shape, color: color, captureInheritedThemes: captureInheritedThemes, useRootNavigator: useRootNavigator, ); } } And then, to use it: import 'package:flutter/material.dart'; import './custom_context_menu.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { #override Widget build(BuildContext context) { return MaterialApp( title: 'Popup Menu Usage', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Popup Menu Usage'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; #override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> with CustomPopupMenu { var _count = 0; void _showCustomMenu() { this.showMenu( context: context, items: <PopupMenuEntry<int>>[PlusMinusEntry()], ) // This is how you handle user selection .then<void>((int delta) { // delta would be null if user taps on outside the popup menu // (causing it to close without making selection) if (delta == null) return; setState(() { _count = _count + delta; }); }); // Another option: // // final delta = await showMenu(...); // // Then process `delta` however you want. // Remember to make the surrounding function `async`, that is: // // void _showCustomMenu() async { ... } } #override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ GestureDetector( // This does not give the tap position ... onLongPress: _showCustomMenu, // Have to remember it on tap-down. onTapDown: storePosition, child: Container( color: Colors.amberAccent, padding: const EdgeInsets.all(100.0), child: Text( '$_count', style: const TextStyle(fontSize: 100, fontWeight: FontWeight.bold), ), ), ), ], ), ), ); } } class PlusMinusEntry extends PopupMenuEntry<int> { #override double height = 100; // height doesn't matter, as long as we are not giving // initialValue to showMenu(). #override bool represents(int n) => n == 1 || n == -1; #override PlusMinusEntryState createState() => PlusMinusEntryState(); } class PlusMinusEntryState extends State<PlusMinusEntry> { void _plus1() { // This is how you close the popup menu and return user selection. Navigator.pop<int>(context, 1); } void _minus1() { Navigator.pop<int>(context, -1); } #override Widget build(BuildContext context) { return Row( children: <Widget>[ Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))), Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))), ], ); } }
Answer for 2023 In Flutter 3.7 there is now a ContextMenuRegion widget that you can wrap around any existing widget. When the user long presses or right-clicks (depending on the platform), the menu you give it will appear. return Scaffold( body: Center( child: ContextMenuRegion( contextMenuBuilder: (context, offset) { return AdaptiveTextSelectionToolbar.buttonItems( anchors: TextSelectionToolbarAnchors( primaryAnchor: offset, ), buttonItems: <ContextMenuButtonItem>[ ContextMenuButtonItem( onPressed: () { ContextMenuController.removeAny(); }, label: 'Save', ), ], ); }, child: const SizedBox( width: 200.0, height: 200.0, child: FlutterLogo(), ), ), ), );
How to update a widget state from another widget's build in flutter
My application has a Stack holding a CustomScrollView and a Container. The CustomScrollView has a SliverAppBar with an expanding header (FlexibleSpaceBar). The Container has properties which depend on the degree of expansion of the FlexibleSpaceBar. I'm having difficulty getting the Container properties to update, in response to the SliverAppBar being manually expanded/collapsed, by the user. My naive approach is to determine the expansion fraction during the build of the SliverAppBar's FlexibleSpaceBar (following the code in FlexibleSpaceBar.build) and then notify the Stack's parent, using setState. However this causes the exception "setState() or markNeedsBuild() called during build." What would be the correct way to use the artefact of FlexibleSpaceBar's build, to build the un-related widget? import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class MyDemoApp extends StatelessWidget { #override Widget build(BuildContext context) { return MaterialApp( title: 'demo app', home: MyHome(), ); } } /// Notifies when there has been a change in the expansion or /// collapsion[!] of the FlexibleSpace in the SliverAppBar class FlexibleSpaceBarChangeNotification extends Notification { final double collapsedFraction; FlexibleSpaceBarChangeNotification({ this.collapsedFraction, }); } class MyHome extends StatefulWidget { #override _MyHomeState createState() => _MyHomeState(); } class _MyHomeState extends State<MyHome> { double _headerCollapsedFraction = 0.5; #override Widget build(BuildContext context) { return NotificationListener<FlexibleSpaceBarChangeNotification>( onNotification: (FlexibleSpaceBarChangeNotification notification) { // This notification occurs when the SliverAppBar is expanded/contracted setState(() { _headerCollapsedFraction = notification.collapsedFraction; }); return true; }, child: Stack( children: <Widget>[ Material( child: CustomScrollView( slivers: <Widget>[ SliverAppBar( pinned: true, expandedHeight: 200.0, flexibleSpace: FlexibleSpaceBar( title: MyFlexibleSpaceBarTitle( // The MyFlexibleSpaceBarTitle widget child: Text('A List of Items'), ), ), ), SliverList( delegate: SliverChildBuilderDelegate((context, i) => ListTile( title: Text('List tile #$i'), ), childCount: 50, ), ), ], ), ), Container( width: 100.0, height: _headerCollapsedFraction * 100.0, color: Colors.pink, ), ], ), ); } } class MyFlexibleSpaceBarTitle extends StatefulWidget { final Widget child; MyFlexibleSpaceBarTitle({ this.child, }); #override _MyFlexibleSpaceBarTitleState createState() => _MyFlexibleSpaceBarTitleState(); } class _MyFlexibleSpaceBarTitleState extends State<MyFlexibleSpaceBarTitle> { #override Widget build(BuildContext context) { // Arithmetic mostly derived from FlexibleSpaceBar.build() final FlexibleSpaceBarSettings settings = context.inheritFromWidgetOfExactType(FlexibleSpaceBarSettings); assert(settings != null, 'No FlexibleSpaceBarSettings found'); final double deltaExtent = settings.maxExtent - settings.minExtent; // 0.0 -> Expanded // 1.0 -> Collapsed to toolbar final double t = (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0); final double fadeStart = max(0.0, 1.0 - kToolbarHeight / deltaExtent); const double fadeEnd = 1.0; assert(fadeStart <= fadeEnd); final double opacity = Interval(fadeStart, fadeEnd).transform(t); // This is probably wrong ? FlexibleSpaceBarChangeNotification(collapsedFraction: t)..dispatch(context); return Opacity( opacity: opacity, child: widget.child, ); } }
You can implement state management using the provider package. This should enable you to update data from anywhere in the app. You can check this guide for more details.
making word cloud in flutter
I want to create a word cloud in flutter. Is there a way to make it? For instance, if I have a list List words = ['banana','banana','banana','banana','purple','orange','apple','apple','green'] I want elements repeated several times to be shown in a big font, and elements repeated a few times to be shown in a small font.
I needed to create a word cloud in Flutter, so I created this package: https://github.com/letsar/flutter_scatter You can create a word cloud like this: class WordCloudExample extends StatelessWidget { #override Widget build(BuildContext context) { List<Widget> widgets = <Widget>[]; for (var i = 0; i < kFlutterHashtags.length; i++) { widgets.add(ScatterItem(kFlutterHashtags[i], i)); } final screenSize = MediaQuery.of(context).size; final ratio = screenSize.width / screenSize.height; return Center( child: FittedBox( child: Scatter( fillGaps: true, delegate: ArchimedeanSpiralScatterDelegate(ratio: ratio), children: widgets, ), ), ); } } class ScatterItem extends StatelessWidget { ScatterItem(this.hashtag, this.index); final FlutterHashtag hashtag; final int index; #override Widget build(BuildContext context) { final TextStyle style = Theme.of(context).textTheme.body1.copyWith( fontSize: hashtag.size.toDouble(), color: hashtag.color, ); return RotatedBox( quarterTurns: hashtag.rotated ? 1 : 0, child: Text( hashtag.hashtag, style: style, ), ); } } And you will end up with the following layout:
Flutter: How to create a widget class that is transparent?
I would like to create a new widget class with a container size of 250x500 with the rest of the class/widget 0.5 opacity - allowing the prior widget - we launched from - to be partially visible. Is this possible ? if so how ? -Thanks Below is the Stateful class I am calling class ShowMyTitles extends StatefulWidget { #override _ShowMyTitlesState createState() => _ShowMyTitlesState(); } class _ShowMyTitlesState extends State<ShowMyTitles> { List<Map<String, bool>> myListOfMapTitles; Map<String, bool> valuesHeaders; int trueCount = 0; #override void initState() { // TODO: implement initState super.initState(); SettingsForMap SFM = new SettingsForMap(); myListOfMapTitles = SFM.myListOfMapTitles; valuesHeaders = SFM.valuesHeaders; } #override Widget build(BuildContext context) { List myTitles = []; return new WillPopScope( onWillPop: (){ myListOfMapTitles.forEach((valuesAll) { valuesAll.forEach((s,b){ if(b == true) { myTitles.add(s); print('My Selecteed titles are = ' + s.toString()); } }); }); Navigator.pop(context, myTitles); }, child: new Container( child: new GestureDetector( onTap: (){ myListOfMapTitles.forEach((valuesAll) { valuesAll.forEach((s,b){ if(b == true) { myTitles.add(s); print('My Selecteed titles are = ' + s.toString()); } }); }); Navigator.pop(context, myTitles); }, child: _titlesDialog(context, 'Select 2 Titles (Max)'), ), ), ); }
There is an Opacity widget. Wrap the widget you want to be transparent within it. Opacity(child:MyTransparentWidget(),opacity:0.45)
You can use Stack widget for that. Surround the widget which you want to change the opacity and the container with Stack widget. Then wrap the widget which you want to change the opacity with Opacity widget and mention required opacity. Make sure you put the container after the widget which you want to change the opacity then only it will sit above the transparent widget
To make any widget transparent, you make that widget a child of a parent Theme() widget. e.g class TransparentWidget extends StatelessWidget { #override Widget build(BuildContext context) { return Theme( data: Theme.of(context).copyWith( // Set the transparency here canvasColor: Colors.white70, //or any other color you want. e.g Colors.blue.withOpacity(0.5) ), child: Container( height: 250.0, width: 250.0, child: Text("Hello World") ) ); } } OR Simply make the main container background decoration an opaque color. Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.7), ) );