Related
I am building a custom TextSelectionControls with an extends from MaterialTextSelectionControls.
How do I verticalize the Text Selection Toolbar in TextField?
It's like this now:
And I want it to be like this so I can add more custom options:
This is the part that builds the toolbar:
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
children: [
...itemDatas
.asMap()
.entries
.map((MapEntry<int, _TextSelectionToolbarItemData> entry) {
return TextSelectionToolbarTextButton(
padding: TextSelectionToolbarTextButton.getPadding(
entry.key, itemDatas.length),
onPressed: entry.value.onPressed,
child: Text(entry.value.label),
);
}),
],
);
And this is the complete code:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class CustomTextSelectionControls extends MaterialTextSelectionControls {
/// Builder for material-style copy/paste text selection toolbar.
#override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset selectionMidpoint,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ClipboardStatusNotifier? clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) {
return _TextSelectionControlsToolbar(
globalEditableRegion: globalEditableRegion,
textLineHeight: textLineHeight,
selectionMidpoint: selectionMidpoint,
endpoints: endpoints,
delegate: delegate,
clipboardStatus: clipboardStatus,
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
handleSelectAll:
canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
);
}
}
class _TextSelectionControlsToolbar extends StatefulWidget {
const _TextSelectionControlsToolbar({
required this.clipboardStatus,
required this.delegate,
required this.endpoints,
required this.globalEditableRegion,
required this.handleCut,
required this.handleCopy,
required this.handlePaste,
required this.handleSelectAll,
required this.selectionMidpoint,
required this.textLineHeight,
});
final ClipboardStatusNotifier? clipboardStatus;
final TextSelectionDelegate delegate;
final List<TextSelectionPoint> endpoints;
final Rect globalEditableRegion;
final VoidCallback? handleCut;
final VoidCallback? handleCopy;
final VoidCallback? handlePaste;
final VoidCallback? handleSelectAll;
final Offset selectionMidpoint;
final double textLineHeight;
#override
_TextSelectionControlsToolbarState createState() =>
_TextSelectionControlsToolbarState();
}
class _TextSelectionControlsToolbarState
extends State<_TextSelectionControlsToolbar> with TickerProviderStateMixin {
// Padding between the toolbar and the anchor.
static const double _kToolbarContentDistanceBelow = 20.0;
static const double _kToolbarContentDistance = 8.0;
void _onChangedClipboardStatus() {
setState(() {
// Inform the widget that the value of clipboardStatus has changed.
});
}
#override
void initState() {
super.initState();
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
}
#override
void didUpdateWidget(_TextSelectionControlsToolbar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.clipboardStatus != oldWidget.clipboardStatus) {
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
}
}
#override
void dispose() {
super.dispose();
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
}
#override
Widget build(BuildContext context) {
// If there are no buttons to be shown, don't render anything.
if (widget.handleCut == null &&
widget.handleCopy == null &&
widget.handlePaste == null &&
widget.handleSelectAll == null) {
return const SizedBox.shrink();
}
// If the paste button is desired, don't render anything until the state of
// the clipboard is known, since it's used to determine if paste is shown.
if (widget.handlePaste != null &&
widget.clipboardStatus?.value == ClipboardStatus.unknown) {
return const SizedBox.shrink();
}
// Calculate the positioning of the menu. It is placed above the selection
// if there is enough room, or otherwise below.
final TextSelectionPoint startTextSelectionPoint = widget.endpoints[0];
final TextSelectionPoint endTextSelectionPoint =
widget.endpoints.length > 1 ? widget.endpoints[1] : widget.endpoints[0];
final Offset anchorAbove = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
widget.globalEditableRegion.top +
startTextSelectionPoint.point.dy -
widget.textLineHeight -
_kToolbarContentDistance,
);
final Offset anchorBelow = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
widget.globalEditableRegion.top +
endTextSelectionPoint.point.dy +
_kToolbarContentDistanceBelow,
);
// Determine which buttons will appear so that the order and total number is
// known. A button's position in the menu can slightly affect its
// appearance.
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final List<_TextSelectionToolbarItemData> itemDatas =
<_TextSelectionToolbarItemData>[
if (widget.handleCut != null)
_TextSelectionToolbarItemData(
label: localizations.cutButtonLabel,
onPressed: widget.handleCut!,
),
if (widget.handleCopy != null)
_TextSelectionToolbarItemData(
label: localizations.copyButtonLabel,
onPressed: widget.handleCopy!,
),
if (widget.handlePaste != null &&
widget.clipboardStatus?.value == ClipboardStatus.pasteable)
_TextSelectionToolbarItemData(
label: localizations.pasteButtonLabel,
onPressed: widget.handlePaste!,
),
if (widget.handleSelectAll != null)
_TextSelectionToolbarItemData(
label: localizations.selectAllButtonLabel,
onPressed: widget.handleSelectAll!,
),
];
// If there is no option available, build an empty widget.
if (itemDatas.isEmpty) {
return const SizedBox(width: 0.0, height: 0.0);
}
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
children: [
...itemDatas
.asMap()
.entries
.map((MapEntry<int, _TextSelectionToolbarItemData> entry) {
return TextSelectionToolbarTextButton(
padding: TextSelectionToolbarTextButton.getPadding(
entry.key, itemDatas.length),
onPressed: entry.value.onPressed,
child: Text(entry.value.label),
);
}),
],
);
}
}
class _TextSelectionToolbarItemData {
const _TextSelectionToolbarItemData({
required this.label,
required this.onPressed,
});
final String label;
final VoidCallback onPressed;
}
I was able to customize the SelectionToolbar using the toolbarBuilder. You may encounter an error in which case you should use the widget used in the Material widget:
return TextSelectionToolbar(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
toolbarBuilder: (context, _) => Material(
child: SizedBox(
width: 230,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(child: ListTile())
],
),
),
),
children: const [SizedBox.shrink()],
);
Customizing the TextSelectionToolbar is the best solution I found:
import 'package:flutter/material.dart';
class CostumeSelectionToolbar extends TextSelectionToolbar {
const CostumeSelectionToolbar({
super.key,
required super.anchorAbove,
required super.anchorBelow,
required super.children,
});
static const double _kToolbarScreenPadding = 8.0;
static const double _kToolbarHeight = 275.0;
#override
Widget build(BuildContext context) {
final double paddingAbove =
MediaQuery.of(context).padding.top + _kToolbarScreenPadding;
final double availableHeight = anchorAbove.dy - paddingAbove;
final bool fitsAbove = _kToolbarHeight <= availableHeight;
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
return Padding(
padding: EdgeInsets.fromLTRB(
_kToolbarScreenPadding,
paddingAbove,
_kToolbarScreenPadding,
_kToolbarScreenPadding,
),
child: Stack(
children: <Widget>[
CustomSingleChildLayout(
delegate: TextSelectionToolbarLayoutDelegate(
anchorAbove: anchorAbove - localAdjustment,
anchorBelow: anchorBelow - localAdjustment,
fitsAbove: fitsAbove,
),
child: SizedBox(
width: 230,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
),
],
),
);
}
}
I have two textfields that accept number inputs. I want to calculate the sum of the two textfields while the user input the numbers in the textfields and in realtime show the results in a third textfield. This is what I have tried so far.
void _calculation() {
setState((){
_total = int.parse(_quantityController.text) * int.parse(feedPrice.text);
},
);
print(_total);
}
And show the result in the third textfield
TextField(
readOnly: true,
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: _total.toString(),
),
),
I pass the total as a string to Textfield hint field. What am I missing or what am I doing wrong?
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class _YourPageState extends State<YourPage> {
int _age1 = 0, _age2 = 0, _totalAge = 0;
final firstNumber = TextEditingController();
final secondNumber = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
children: <Widget>[
TextField(
controller: firstNumber,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
onSubmitted: (String value) {},
),
TextField(
controller: secondNumber,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
onSubmitted: (String value) {},
),
Text(
'Sum is: ${firstNumber.text + secondNumber.text}',
textAlign: TextAlign.center,
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
),
),
);
}
}
you are doing wrong at setting hintText .Because hint Text that suggests what sort of input the field accepts.
you should set text to text field instead of hinttext like this
text:_total.toString()
Demo Widget
class ApplicantsX extends StatefulWidget {
const ApplicantsX({Key? key}) : super(key: key);
#override
State<ApplicantsX> createState() => _ApplicantsXState();
}
class _ApplicantsXState extends State<ApplicantsX> {
double a = 0;
double b = 0;
final TextEditingController controller = TextEditingController();
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(onChanged: (value) {
final x = double.tryParse(value);
setState(() {
a = x ?? 0; // handle null and String
controller.text = (a + b).toStringAsFixed(2);
});
}),
TextField(onChanged: (value) {
final x = double.tryParse(value);
setState(() {
b = x ?? 0;
controller.text = (a + b).toStringAsFixed(2);
});
}),
TextField(
controller: controller,
readOnly: true,
)
],
);
}
}
After reset all textfield values and result when i enter a new number in any textfield that number is added to previous result so, can u plz give the code for previous result not considered.
Background: I am creating one app, where the internet connection status should be shown to the user dynamically. The result is already showing but I want the background color to be changed dynamically. I tried to implement it in setState() but the text color is not getting changed. and I don't know how to implement setState() inside button without onPressed.
main code:
import 'dart:async';
import 'dart:io';
import 'package:connectivity/connectivity.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:quote_app_one/components/TextStyles_Icons.dart';
import 'package:quote_app_one/utils/HexColor.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Color _newAppBgColor = HexColor("#111328");
Map _source = {ConnectivityResult.none: false};
MyConnectivity _connectivity = MyConnectivity.instance;
#override
void initState() {
super.initState();
_connectivity.initialise();
_connectivity.myStream.listen((source) {
setState(() => _source = source);
});
}
#override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: _newAppBgColor, // navigation bar color
));
String string;
switch (_source.keys.toList()[0]) {
case ConnectivityResult.none:
string = "Offline";
break;
case ConnectivityResult.mobile:
string = "Online";
break;
case ConnectivityResult.wifi:
string = "Online";
}
//This is the setState() I have implemented
String cData = string;
setState(() {
cData = string;
if(cData == "Online"){
return Text("Online",
style: TextStyle(
color: Colors.green
),);
}
else{
return Text("Offline",
style: TextStyle(
color: Colors.red
),);
}
});
return Scaffold(
appBar: AppBar(
title: homeScreenAppBarText,
backgroundColor: _newAppBgColor,
),
backgroundColor: _newAppBgColor,
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
child: cStatusText,
),
Text( cData,
// style: TextStyle(
// color: Colors.white
// ),
)
],
),
),
new Container(
child: Row(
children: [
// custom widgets
],
),
)
],
),
),
);
}
}
class MyConnectivity {
MyConnectivity._internal();
static final MyConnectivity _instance = MyConnectivity._internal();
static MyConnectivity get instance => _instance;
Connectivity connectivity = Connectivity();
StreamController controller = StreamController.broadcast();
Stream get myStream => controller.stream;
void initialise() async {
ConnectivityResult result = await connectivity.checkConnectivity();
_checkStatus(result);
connectivity.onConnectivityChanged.listen((result) {
_checkStatus(result);
});
}
void _checkStatus(ConnectivityResult result) async {
bool isOnline = false;
try {
final result = await InternetAddress.lookup('example.com');
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
isOnline = true;
} else
isOnline = false;
} on SocketException catch (_) {
isOnline = false;
}
controller.sink.add({result: isOnline});
}
void disposeStream() => controller.close();
}
connectivity_services code:
import 'package:flutter/material.dart';
import 'package:connectivity/connectivity.dart';
class ConnectivityChangeNotifier extends ChangeNotifier {
ConnectivityChangeNotifier() {
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
resultHandler(result);
});
}
ConnectivityResult _connectivityResult = ConnectivityResult.none;
String _pageText =
'Currently connected to no network. Please connect to a wifi network!';
ConnectivityResult get connectivity => _connectivityResult;
String get pageText => _pageText;
void resultHandler(ConnectivityResult result) {
_connectivityResult = result;
if (result == ConnectivityResult.none) {
_pageText =
'Currently connected to no network. Please connect to a wifi network!';
} else if (result == ConnectivityResult.mobile) {
_pageText =
'Currently connected to a celluar network. Please connect to a wifi network!';
} else if (result == ConnectivityResult.wifi) {
_pageText = 'Connected to a wifi network!';
}
notifyListeners();
}
void initialLoad() async {
ConnectivityResult connectivityResult =
await (Connectivity().checkConnectivity());
resultHandler(connectivityResult);
}
}
In the image, my code is displaying as offline.. but I want that offline beside status as shown in the image and the background color should change dynamically when the status is online. How can I implement this?
You're almost there, you just need to put all that functionality in the stream listener you have setup and not in the build method.
Since you already have a listener following changes in connectivity status, that listener just needs to update a Color variable. Here's a simplified example.
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Color _newAppBgColor = HexColor("#111328");
// these 2 get updated on connectivity changes
Color statusColor = Colors.red;
String cData = 'Offline';
Map _source = {ConnectivityResult.none: false};
MyConnectivity _connectivity = MyConnectivity.instance;
#override
void initState() {
super.initState();
_connectivity.initialise();
_connectivity.myStream.listen((source) {
setState(() {
_source = source;
_updateStatusText(); // moved from build method to listener
});
});
}
void _updateStatusText() {
switch (_source.keys.toList()[0]) {
case ConnectivityResult.none:
cData = "Offline";
statusColor = Colors.red;
break;
case ConnectivityResult.mobile:
cData = "Online";
statusColor = Colors.green;
break;
case ConnectivityResult.wifi:
cData = "Online";
statusColor = Colors.green;
}
}
#override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: _newAppBgColor, // navigation bar color
));
return Scaffold(
appBar: AppBar(
// title: homeScreenAppBarText, // didn't have the origin of homeScreenAppBarText to test this
title: Text('Demo'),
backgroundColor: _newAppBgColor,
),
backgroundColor: _newAppBgColor,
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
// child: cStatusText, // didn't have the origin of cStatusText to test this
child: Text('status',
style: TextStyle(
color:
statusColor)),
),
Text(
cData, // this updates on change in connectivity status
style: TextStyle(
color:
statusColor, // this updates on change in connectivity status
),
)
],
),
),
new Container(
child: Row(
children: [
// custom widgets
],
),
)
],
),
),
);
}
}
I want to execute a method while a user is pressing down on a button. In pseudocode:
while (button.isPressed) {
executeCallback();
}
In other words, the executeCallback method should fire repeatedly as long as the user is pressing down on the button, and stop firing when the button is released. How can I achieve this in Flutter?
Use a Listener and a stateful widget. I also introduced a slight delay after every loop:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(brightness: Brightness.dark),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool _buttonPressed = false;
bool _loopActive = false;
void _increaseCounterWhilePressed() async {
// make sure that only one loop is active
if (_loopActive) return;
_loopActive = true;
while (_buttonPressed) {
// do your thing
setState(() {
_counter++;
});
// wait a bit
await Future.delayed(Duration(milliseconds: 200));
}
_loopActive = false;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Listener(
onPointerDown: (details) {
_buttonPressed = true;
_increaseCounterWhilePressed();
},
onPointerUp: (details) {
_buttonPressed = false;
},
child: Container(
decoration: BoxDecoration(color: Colors.orange, border: Border.all()),
padding: EdgeInsets.all(16.0),
child: Text('Value: $_counter'),
),
),
),
);
}
}
A simpler way, without the listener, is as follows:
GestureDetector(
child: InkWell(
child: Icon(Icons.skip_previous_rounded),
onTap: widget.onPrevious,
),
onLongPressStart: (_) async {
isPressed = true;
do {
print('long pressing'); // for testing
await Future.delayed(Duration(seconds: 1));
} while (isPressed);
},
onLongPressEnd: (_) => setState(() => isPressed = false),
);
}
Building on the solution from ThinkDigital, my observation is that InkWell contains all the events necessary to do this without an extra GestureDetector (I find that the GestureDetector interferes with the ink animation on long press). Here's a control I implemented for a pet project that fires its event with a decreasing delay when held (this is a rounded button with an icon, but anything using InkWell will do):
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay,
"The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
#override
Widget build(BuildContext context) {
var shape = CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color:
Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step =
(widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (_holding) {
widget.onUpdate();
await Future.delayed(Duration(milliseconds: delay.round()));
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
Here it is in action:
To improve Elte Hupkes's solution, I fixed an issue where the number of clicks and the number of calls to the onUpdate callback did not match when tapping consecutively.
_tapDownCount variable is additionally used.
import 'package:flutter/material.dart';
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay, "The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
int _tapDownCount = 0;
#override
Widget build(BuildContext context) {
var shape = const CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color: Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
widget.onUpdate();
_tapDownCount += 1;
final int myCount = _tapDownCount;
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step = (widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (true) {
await Future.delayed(Duration(milliseconds: delay.round()));
if (_holding && myCount == _tapDownCount) {
widget.onUpdate();
} else {
return;
}
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
USING SETSTATE
You can achieve this by simply "onLongPressStart" and "onLongPressEnd" properties of a button.
In case you can't find "onLongPressStart" / "onLongPressEnd" properties in your widget, wrap your widget with the "GestureDetector" widget.
GestureDetector(
child: ..,
onLongPressStart: (_) async {
isTap = true;
do {
await Future.delayed(Duration(seconds: 1));
} while (isTap );
},
onLongPressEnd: (_) => setState(() => isTap = false),
);
}
I'm new to Dart/Flutter framework and I'm still exploring their possibilities.
I know in Android it's possible to take a picture and extract the main color value from it programmatically. (Android example)
I wonder, how would this be achieved in pure Dart? I would like it to be compatible with both iOS and Android operating system.
Here's a simple function which returns the dominant color given an ImageProvider. This shows the basic usage of Palette Generator without all the boilerplate.
import 'package:palette_generator/palette_generator.dart';
// Calculate dominant color from ImageProvider
Future<Color> getImagePalette (ImageProvider imageProvider) async {
final PaletteGenerator paletteGenerator = await PaletteGenerator
.fromImageProvider(imageProvider);
return paletteGenerator.dominantColor.color;
}
Then use FutureBuilder on the output to build a Widget.
I probably think you got a fix but for future searches to this question, I suggest you check Pallete Generator by the flutter team.
I will try and give a simple explanation of how the code works but for a detailed example head over to the plugin's GitHub repo.
The example below is going to take an image then select the dominant colors from it and then display the colors
First, we add the required imports
import 'package:palette_generator/palette_generator.dart';
After that let's create the main application class.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
...
home: const HomePage(
title: 'Colors from image',
image: AssetImage('assets/images/artwork_default.png',),
imageSize: Size(256.0, 170.0),
...
),
);
}
}
In the image field above, place the image that you want to extract the dominant colors from, i used the image shown here.
Next, we create the HomePage class
#immutable
class HomePage extends StatefulWidget {
/// Creates the home page.
const HomePage({
Key key,
this.title,
this.image,
this.imageSize,
}) : super(key: key);
final String title; //App title
final ImageProvider image; //Image provider to load the colors from
final Size imageSize; //Image dimensions
#override
_HomePageState createState() {
return _HomePageState();
}
}
Lets create the _HomePageState too
class _HomePageState extends State<HomePage> {
Rect region;
PaletteGenerator paletteGenerator;
final GlobalKey imageKey = GlobalKey();
#override
void initState() {
super.initState();
region = Offset.zero & widget.imageSize;
_updatePaletteGenerator(region);
}
Future<void> _updatePaletteGenerator(Rect newRegion) async {
paletteGenerator = await PaletteGenerator.fromImageProvider(
widget.image,
size: widget.imageSize,
region: newRegion,
maximumColorCount: 20,
);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _kBackgroundColor,
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new AspectRatio(
aspectRatio: 15 / 15,
child: Image(
key: imageKey,
image: widget.image,
),
),
Expanded(child: Swatches(generator: paletteGenerator)),
],
),
);
}
}
The code above just lays out the image and the Swatches which is a class defined below. In initState, we first select a region which the colors will be derived from which in our case is the whole image.
After that we create a class Swatches which receives a PalleteGenerator and draws the swatches for it.
class Swatches extends StatelessWidget {
const Swatches({Key key, this.generator}) : super(key: key);
// The PaletteGenerator that contains all of the swatches that we're going
// to display.
final PaletteGenerator generator;
#override
Widget build(BuildContext context) {
final List<Widget> swatches = <Widget>[];
//The generator field can be null, if so, we return an empty container
if (generator == null || generator.colors.isEmpty) {
return Container();
}
//Loop through the colors in the PaletteGenerator and add them to the list of swatches above
for (Color color in generator.colors) {
swatches.add(PaletteSwatch(color: color));
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
//All the colors,
Wrap(
children: swatches,
),
//The colors with ranking
Container(height: 30.0),
PaletteSwatch(label: 'Dominant', color: generator.dominantColor?.color),
PaletteSwatch(
label: 'Light Vibrant', color: generator.lightVibrantColor?.color),
PaletteSwatch(label: 'Vibrant', color: generator.vibrantColor?.color),
PaletteSwatch(
label: 'Dark Vibrant', color: generator.darkVibrantColor?.color),
PaletteSwatch(
label: 'Light Muted', color: generator.lightMutedColor?.color),
PaletteSwatch(label: 'Muted', color: generator.mutedColor?.color),
PaletteSwatch(
label: 'Dark Muted', color: generator.darkMutedColor?.color),
],
);
}
}
After that lets create a PaletteSwatch class. A palette swatch is just a square of color with an optional label
#immutable
class PaletteSwatch extends StatelessWidget {
// Creates a PaletteSwatch.
//
// If the [color] argument is omitted, then the swatch will show a
// placeholder instead, to indicate that there is no color.
const PaletteSwatch({
Key key,
this.color,
this.label,
}) : super(key: key);
// The color of the swatch. May be null.
final Color color;
// The optional label to display next to the swatch.
final String label;
#override
Widget build(BuildContext context) {
// Compute the "distance" of the color swatch and the background color
// so that we can put a border around those color swatches that are too
// close to the background's saturation and lightness. We ignore hue for
// the comparison.
final HSLColor hslColor = HSLColor.fromColor(color ?? Colors.transparent);
final HSLColor backgroundAsHsl = HSLColor.fromColor(_kBackgroundColor);
final double colorDistance = math.sqrt(
math.pow(hslColor.saturation - backgroundAsHsl.saturation, 2.0) +
math.pow(hslColor.lightness - backgroundAsHsl.lightness, 2.0));
Widget swatch = Padding(
padding: const EdgeInsets.all(2.0),
child: color == null
? const Placeholder(
fallbackWidth: 34.0,
fallbackHeight: 20.0,
color: Color(0xff404040),
strokeWidth: 2.0,
)
: Container(
decoration: BoxDecoration(
color: color,
border: Border.all(
width: 1.0,
color: _kPlaceholderColor,
style: colorDistance < 0.2
? BorderStyle.solid
: BorderStyle.none,
)),
width: 34.0,
height: 20.0,
),
);
if (label != null) {
swatch = ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 130.0, minWidth: 130.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
swatch,
Container(width: 5.0),
Text(label),
],
),
);
}
return swatch;
}
}
Hope this helps, thank you.
//////////////////////////////
//
// 2019, roipeker.com
// screencast - demo simple image:
// https://youtu.be/EJyRH4_pY8I
//
// screencast - demo snapshot:
// https://youtu.be/-LxPcL7T61E
//
//////////////////////////////
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:image/image.dart' as img;
import 'package:flutter/services.dart' show rootBundle;
void main() => runApp(const MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String imagePath = 'assets/5.jpg';
GlobalKey imageKey = GlobalKey();
GlobalKey paintKey = GlobalKey();
// CHANGE THIS FLAG TO TEST BASIC IMAGE, AND SNAPSHOT.
bool useSnapshot = true;
// based on useSnapshot=true ? paintKey : imageKey ;
// this key is used in this example to keep the code shorter.
late GlobalKey currentKey;
final StreamController<Color> _stateController = StreamController<Color>();
//late img.Image photo ;
img.Image? photo;
#override
void initState() {
currentKey = useSnapshot ? paintKey : imageKey;
super.initState();
}
#override
Widget build(BuildContext context) {
final String title = useSnapshot ? "snapshot" : "basic";
return SafeArea(
child: Scaffold(
appBar: AppBar(title: Text("Color picker $title")),
body: StreamBuilder(
initialData: Colors.green[500],
stream: _stateController.stream,
builder: (buildContext, snapshot) {
Color selectedColor = snapshot.data as Color ?? Colors.green;
return Stack(
children: <Widget>[
RepaintBoundary(
key: paintKey,
child: GestureDetector(
onPanDown: (details) {
searchPixel(details.globalPosition);
},
onPanUpdate: (details) {
searchPixel(details.globalPosition);
},
child: Center(
child: Image.asset(
imagePath,
key: imageKey,
//color: Colors.red,
//colorBlendMode: BlendMode.hue,
//alignment: Alignment.bottomRight,
fit: BoxFit.contain,
//scale: .8,
),
),
),
),
Container(
margin: const EdgeInsets.all(70),
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selectedColor!,
border: Border.all(width: 2.0, color: Colors.white),
boxShadow: [
const BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2))
]),
),
Positioned(
child: Text('${selectedColor}',
style: const TextStyle(
color: Colors.white,
backgroundColor: Colors.black54)),
left: 114,
top: 95,
),
],
);
}),
),
);
}
void searchPixel(Offset globalPosition) async {
if (photo == null) {
await (useSnapshot ? loadSnapshotBytes() : loadImageBundleBytes());
}
_calculatePixel(globalPosition);
}
void _calculatePixel(Offset globalPosition) {
RenderBox box = currentKey.currentContext!.findRenderObject() as RenderBox;
Offset localPosition = box.globalToLocal(globalPosition);
double px = localPosition.dx;
double py = localPosition.dy;
if (!useSnapshot) {
double widgetScale = box.size.width / photo!.width;
print(py);
px = (px / widgetScale);
py = (py / widgetScale);
}
int pixel32 = photo!.getPixelSafe(px.toInt(), py.toInt());
int hex = abgrToArgb(pixel32);
_stateController.add(Color(hex));
}
Future<void> loadImageBundleBytes() async {
ByteData imageBytes = await rootBundle.load(imagePath);
setImageBytes(imageBytes);
}
Future<void> loadSnapshotBytes() async {
RenderRepaintBoundary boxPaint =
paintKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
//RenderObject? boxPaint = paintKey.currentContext.findRenderObject();
ui.Image capture = await boxPaint.toImage();
ByteData? imageBytes =
await capture.toByteData(format: ui.ImageByteFormat.png);
setImageBytes(imageBytes!);
capture.dispose();
}
void setImageBytes(ByteData imageBytes) {
List<int> values = imageBytes.buffer.asUint8List();
photo;
photo = img.decodeImage(values)!;
}
}
// image lib uses uses KML color format, convert #AABBGGRR to regular #AARRGGBB
int abgrToArgb(int argbColor) {
int r = (argbColor >> 16) & 0xFF;
int b = argbColor & 0xFF;
return (argbColor & 0xFF00FF00) | (b << 16) | r;
}