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,
),
),
),
],
),
);
}
}
Related
There are other ways of handling this overlay, but AFAIK this should work, and it does sometimes, but not always on Android. On Windows it appeared to work perfectly. I have the latest Flutter (3.0.3) and the latest Dart (2.17.5) and Flutter doctor shows no problems. I have tested this on 2 physical devices (Android 6.0.1 - level 23, and Android 11 - level 30). I have also tested it on an emulator (Android 11 - level 30). The results on all three were very similar - the overlay doesn't always show. I also tested it on Windows and it worked perfectly for all attempts (30).
The code below is a cut-down version of the code to demonstrate the problem. The problem is that the overlay does not always display. After a reboot it will often work well for about 10-12 reloads of the program, and then it is inconsistent, sometimes showing the overlay and sometimes not. As I said, it worked perfectly on Windows.
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
const D_OVERLAY_HEIGHT = 290.0;
const D_BTN_HEIGHT = 45.0;
void main() => runApp(OverlayTestApp());
class OverlayTestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: OverlayTest(),
);
}
}
class OverlayTest extends StatefulWidget {
#override
_PageOverlayTest createState() => _PageOverlayTest();
}
//==== THE MAIN PAGE FOR THE APP ====//
class _PageOverlayTest extends State<OverlayTest> {
TextEditingController? _wController = TextEditingController();
ClassOverlay? _clOverlay;
#override
Widget build(BuildContext context) {
WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding.instance
.addPostFrameCallback((_) => fnOnBuildComplete(context));
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back), onPressed: () => fnExit()),
title: Center(child: Text('Overlay Test'))),
body: WillPopScope(
onWillPop: () async => await _fnDispose(),
child: Column(
children: [
SizedBox(height: 50),
TextField(
controller: _wController,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(3),
),
),
style: TextStyle(fontSize: 16),
autofocus: true,
showCursor: true,
readOnly: true,
toolbarOptions: ToolbarOptions(
copy: false,
cut: false,
paste: false,
selectAll: false,
),
),
],
),
));
}
fnExit() {
dispose();
SystemNavigator.pop();
exit(0);
}
#override
void dispose() {
_fnDispose();
super.dispose();
}
Future<bool> _fnDispose() async {
if (_wController != null) {
_wController!.dispose();
_wController = null;
}
if (_clOverlay != null) {
_clOverlay!.fnDispose();
}
return true;
}
void fnOnBuildComplete(BuildContext context) async {
if (_clOverlay == null) {
double dScreenWidth = MediaQuery.of(context).size.width;
dScreenWidth = dScreenWidth < 510 ? dScreenWidth : 500;
double dScreenHeight = MediaQuery.of(context).size.height;
_clOverlay = ClassOverlay(dScreenWidth, dScreenHeight - 320);
}
_clOverlay!.fnInitContext(context, _wController!);
}
}
//==== CLASS FOR THE OVERLAY ====//
class ClassOverlay {
TextEditingController? _wCtlText;
OverlayEntry? _wOverlayEntry;
final double dScreenWidth;
final double dOverlayTop;
Material? _wOverlayWidget;
ClassOverlay(this.dScreenWidth, this.dOverlayTop) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]);
_wOverlayWidget = fnCreateOverlayWidget(dScreenWidth: dScreenWidth);
}
//==== AFTER BUILD OF CALLER INITIALIZE WITH CONTEXT AND OVERLAYSTATE ====//
void fnInitContext(BuildContext context, TextEditingController wCtlText) {
try {
_wCtlText = wCtlText;
if (_wOverlayEntry != null) {
_wOverlayEntry!.remove();
_wOverlayEntry = null;
}
if (_wOverlayWidget == null) {
throw ('_wOverlayWidget is null');
}
_wOverlayEntry = OverlayEntry(builder: (context) {
return Positioned(left: 0, top: dOverlayTop, child: _wOverlayWidget!);
});
OverlayState? wOverlayState = Navigator.of(context).overlay;
if (wOverlayState == null) {
throw ('Failed to get OverlayState');
}
if (_wOverlayEntry == null) {
throw ('OverlayEntry is null');
}
wOverlayState.insert(_wOverlayEntry!);
if (!(wOverlayState.mounted)) {
wOverlayState.activate();
}
} catch (vError) {
_wCtlText!.text = '**** Fatal Error ${vError.toString()}';
}
}
void fnDispose() {
if (_wOverlayEntry != null) {
_wOverlayEntry!.remove();
_wOverlayEntry = null;
}
}
}
//==== CLASS - OVERLAY WIDGET ====//
Material fnCreateOverlayWidget({required double dScreenWidth}) {
final double dBtnWidth = (dScreenWidth - 60) / 10;
final double dLineSeparator = 10;
return Material(
child: Container(
height: (D_OVERLAY_HEIGHT),
width: dScreenWidth - 10,
padding: EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade500, width: 2),
borderRadius: BorderRadius.circular(10)),
child: Column(
children: [
fnCreateRow('1234567890', dBtnWidth),
SizedBox(height: dLineSeparator),
fnCreateRow('qwertyuiop', dBtnWidth),
SizedBox(height: dLineSeparator),
fnCreateRow('asdfghjkl', dBtnWidth),
SizedBox(height: dLineSeparator),
fnCreateRow(',zxcvbnm.', dBtnWidth),
SizedBox(height: dLineSeparator),
fnCreateRow('1234567890', dBtnWidth)
],
),
));
}
//==== FUNCTION - CREATE A ROW OF TEXT BUTTONS ====//
Row fnCreateRow(String sRow, double dBtnWidth) {
List<Widget> wRow = [];
double dSpace = sRow.length == 10 ? 0 : (dBtnWidth * (10 - sRow.length)) / 2;
if (dSpace > 0) {
wRow.add(SizedBox(width: dSpace));
}
for (int iNdx = 0; iNdx < sRow.length; iNdx++) {
double dFontSize = sRow[iNdx] == '.' || sRow[iNdx] == ',' ? 30 : 20;
wRow.add(SizedBox(
height: D_BTN_HEIGHT,
width: dBtnWidth,
child: TextButton(
onPressed: () {},
child: Text(sRow[iNdx],
style: TextStyle(
color: Colors.black,
fontSize: dFontSize,
fontWeight: FontWeight.w400)),
)));
}
if (dSpace > 0) {
wRow.add(SizedBox(width: dSpace));
}
return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: wRow);
}
It took a lot of time to find the reason for this problem. I was not aware of the fact that MediaQuery can return zero for the height or width because of a timing issue, and the application program must cater for that. It appears that this was working correctly, but the height and/or width was set to zero. There must be a lot of programs that don't check for that.
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 am creating an App in flutter and needed some ideas on how to go about doing this,
I am adding pdf - png images to my app which has Arabic text and need a way to change the text of an image. If an image has so much text I want to go to a certain line in a text and swipe to the right and change background and text. What widget should I use? Or how should I go about doing this? swipe to the right I want it to highlight English text with a nice background and swipe to the left back to Arabic. I am using the flutter framework and am stuck on how to go about doing this?
and how to do it code-wise... will I need to add dimensions per each line?
import 'dart:io';
import 'package:Quran_highlighter/main.dart';
import 'package:system_shortcuts/system_shortcuts.dart';
import 'package:Quran_highlighter/Widgets/NavDrawer.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:zoom_widget/zoom_widget.dart';
import 'package:flutter/gestures.dart';
// onPanEnd: (details) {
// if (swipeLeft) {
// handleSwipeLeft();
// } else
// handleSwipeRight();
// },
// onPanUpdate: (details) {
// if (details.delta.dx > 0) {
// swipeLeft = false;
// } else {
// //print("Dragging in -X direction");
// swipeLeft = true;
// }
// },
Future main() async {
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
runApp(new Aliflaammeem());
}
class Aliflaammeem extends StatefulWidget {
#override
_AliflaammeemState createState() => _AliflaammeemState();
}
class _AliflaammeemState extends State<Aliflaammeem> {
// Widget body(BuildContext context) {
// if(MediaQuery.of(context).orientation == Orientation.portrait)
// {
// return portrait();
// }
// else {
// return landscape();
// }
// }
// landscapeLeft
// landscapeRight
// portraitDown
// portraitUp
double value;
#override
void initState() {
value = 0.0;
super.initState();
}
#override
Widget build(BuildContext context) {
// appBar: AppBar(
// title: Text('Para 1, Pg2'),
// backgroundColor: Colors.teal[400],
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
return Scaffold(
body: GestureDetector(onPanUpdate: (DragUpdateDetails details) {
if (details.delta.dx > 0) {
print("right swipe english");
setState(() {
// highlightColor: Colors.green,
// textColor: Colors.white,
new Text(
"In The Name of Allah, the Benificient, The Mericiful",
style: new TextStyle(fontSize: 12.0),
);
// // fontWeight: FontWeight.w200,
// fontFamily: "Roboto"),
// value = 2.1;
});
} else if (details.delta.dx < 0) {
print("left swipe arabic");
setState(() {
// highlightColor: Colors.green,
// textColor: Colors.white,
new Text(
"In The Name of Allah,The Mericiful",
style: new TextStyle(fontSize: 12.0),
);
// value= 0.1;
});
new Container(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
// scrollDirection: Axis.horizontal,
// child: Zoom(
// height:1800,
// width: 1800,
child: SafeArea(
top: true,
bottom: true,
right: true,
left: true,
child: Image(
image: AssetImage('test/assets/quranpg0.png'),
fit: BoxFit.cover))))));
}
}));
}
}
To detect swipe to right and left , you can use GestureDetector .
Declare a boolean to know whether its a left or right swipe.
bool swipeLeft=false;
and inside your scaffold or wherever you are writing this code, wrap GestureDetector around your child widget.
child: GestureDetector(
onPanEnd: (details) {
if (swipeLeft) {
handleSwipeLeft();
} else
handleSwipeRight();
},
onPanUpdate: (details) {
if (details.delta.dx > 0) {
swipeLeft = false;
} else {
//print("Dragging in -X direction");
swipeLeft = true;
}
},
child: Container(
child: PDFPngImage(),
)
),
Whenever user starts swiping on screen, onPanUpdate is called with details of user interaction.
From the details, you can extract whether its right swipe or left swipe
onPanUpdate: (details) {
if (details.delta.dx > 0) {
//it means , user is swiping towards right
//hence set bool variable to false.
swipeLeft = false;
}
}
And when swiping ends completely, onPanEnd is called , hence check the status of bool variable, whether it was a swipe left or right and update UI accordingly.
Hope this is clear and helpful!!
#Edit: Similar Complete Code related to what you want to implement in particular.
Try following a similar approach and implement it. I hope this is clear!
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget {
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
var myText = "";
var image;
var pageIndex = 0;
var numberOfOnBoardScreens = 3;
var swipeLeft = false;
var data = [
[
'assets/images/urdu.png',
'Text in urdu',
],
[
'assets/images/English.png',
'English translation',
],
[
'assets/images/french.png',
'Translation in french',
]
];
handleClick(direction) {
print('handle click :$direction');
if (direction == -1) //moving left
{
if (pageIndex > 0) {
setState(() {
pageIndex -= 1;
});
}
} else if (numberOfOnBoardScreens - 1 > pageIndex) {
setState(() {
pageIndex += 1;
});
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: GestureDetector(
//onHorizontalDragEnd: handleClick(1),
onPanEnd: (details) {
if (swipeLeft) {
handleClick(1);
} else
handleClick(-1);
},
onPanUpdate: (details) {
if (details.delta.dx > 0) {
swipeLeft = false;
} else {
//print("Dragging in -X direction");
swipeLeft = true;
}
},
child: TestPageScreenWidgetState(
image: data[pageIndex][0],
myText: data[pageIndex][1],
)),
);
}
}
class TestPageScreenWidgetState extends StatelessWidget {
final myText;
var image;
var currentScreen = 1;
TestPageScreenWidgetState({
this.image,
this.myText,
});
#override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
SizedBox(
height: MediaQuery.of(context).viewPadding.top,
),
Image.asset(
image,
),
Center(
child: Padding(
padding: const EdgeInsets.fromLTRB(100, 10, 90, 60),
child: Center(
child: Text(
'$myText',
),
),
),
),
],
);
}
}
In my app, I do have a list, on which I have implemented the long press selection of this post of Raouf Rahiche. When the selection is enabled I do have a different appbar, that has an IconButton on it, that should disable the selection. But I do not know how to do that.
Till now it is not working the way it should. The behaviour is displayed in the video below.
The longpress-selection is a StatefulWidget:
class _SelectableItems extends State<SelectableItems> {
bool isSelected = false;
GoogleMaterialColors googleMaterialColors = new GoogleMaterialColors();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onLongPress: () {
setState(() {
isSelected = !isSelected;
});
widget.callback();
},
onTap: () {
setState(() {
isSelected = !isSelected;
});
if (widget.longPressEnabled) {
widget.callback();
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (context)=>RecipeDetails(widget.name))
);
}
},
child: ListTile(
leading: CircleAvatar(
child: (isSelected
? Icon(
Icons.check,
color: Colors.white,
)
: (widget.image != "no image"
? Container(
width: 40.0,
height: 40.0,
decoration: new BoxDecoration(
image: new DecorationImage(
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.2), BlendMode.darken),
image: AssetImage(widget.image),
fit: BoxFit.cover,
),
borderRadius: new BorderRadius.all(new Radius.circular(50.0)),
),
)
: Text(
widget.name[0].toUpperCase(),
style: TextStyle(
color: Colors.white,
fontSize: 21.0,
fontWeight: FontWeight.w400
),
)
)
),
backgroundColor: (isSelected
? googleMaterialColors.primaryColor()
: widget.color.withOpacity(1.00)
)
),
title: Padding(
padding: EdgeInsets.only(top: 25.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
widget.title
],
),
),
),
);
}
}
I am calling this widget inside a SideHeaderListView like this:
bool longPressFlag = false;
List<String> indexList = new List();
//other code
return SideHeaderListView(
hasSameHeader: (int a, int b){
return snapshot.data[a].name[0] == snapshot.data[b].name[0];
},
itemCount: snapshot.data.length,
headerBuilder: (BuildContext context, int index){
return new Padding(
padding: EdgeInsets.only(top: 30.0, left: 20.0, right: 25.0),
child: Container(
width: 10.0,
child: Text(
snapshot.data[index].name[0].toUpperCase(),
style: TextStyle(
color: googleMaterialColors.primaryColor().withGreen(120),
fontFamily: "Google-Sans",
fontSize: 15.0,
fontWeight: FontWeight.w600
),
),
),
);
},
itemExtend: 70.0,
itemBuilder: (BuildContext context, int index){
Color usedColor = convertColor.convertToColor(snapshot.data[index].backgroundColor);
String image = snapshot.data[index].image;
return SelectableItems(
color: usedColor,
name: snapshot.data[index].name,
title: (searchController.text.isEmpty
? Text(snapshot.data[index].name)
: recipeName(searchCondition, snapshot.data[index].name)
),
index: index,
image: image,
longPressEnabled: longPressFlag,
//isSelected: selectedFlag,
callback: () {
if (indexList.contains(snapshot.data[index].name)) {
indexList.remove(snapshot.data[index].name);
} else {
indexList.add(snapshot.data[index].name);
}
longPress();
},
);
},
);
void longPress() {
setState(() {
if (indexList.length == 0) {
longPressFlag = false;
} else {
longPressFlag = true;
}
});
}
I hope somebody would be able to solve my problem. Thanks in advance.
The first thing is that you should add each item a key in constructor like this :
MyItem({Key key}): super(key: key);
Why a key ?
A key allow you to identify your widget correctly.
See in doc :
A new widget will only be used to update an existing element if its
key is the same as the key of the current widget associated with the
element.
Create a GlobalKey (a GLobal key extends Key)
For each item to access the widget from, create a global key.
From the doc :
A key that is unique across the entire app. Global keys uniquely
identify elements. Global keys provide access to other objects that
are associated with elements, such as the a [BuildContext] and, for
[StatefulWidget]s, a [State].
Add in the code the creation of a global key for each item (in your SelectableItem for you) :
...
var key = new GlobalKey<SelectableItem >();
this.items.put(position, key);
return new SelectableItem(key: key,...);
Items is a map where you can save position and Global Key.
Now when you want to select a View from the parent just access the globalKey from the map of items and access the widget to do what you want.(update, uncheck, etc...)
Edit : exemple :
class SideHeaderListView {
Map<int, GlobalKey<_SelectableItems>> map = new Map();
create() {
for (int i = 0; i< 10; i++) {
var key = new GlobalKey<_SelectableItems>();
var item = new SelectableItems(key: key);
map.putIfAbsent(i, () => key);
}
}
redrawItem(int i) {
var widget = this.map[i].currentState;
widget.redraw();
}
}
class SelectableItems extends StatefulWidget {
SelectableItems({key: Key}) : super(key: key);
#override
State<StatefulWidget> createState() {
return new _SelectableItems();
}
}
class _SelectableItems extends State<SelectableItems> {
#override
Widget build(BuildContext context) {
return new Text("test");
}
redraw() {
setState(() {
});
}
}
You have commented part of code - //isSelected: selectedFlag,
I think, you have to add this field to your widget
class SelectableItems extands StatefulWidget {
SelectableItems({this.isSelected = false});
final bool isSelected;
...
class _SelectableItems extends State<SelectableItems> {
bool isSelected;
#override
void initState() {
isSelected = widget.isSelected ?? false;
super.initState();
}
....
And when you're creating list of items:
return SelectableItems(
...
isSelected: indexList.contains(snapshot.data[index].name)
I think this could work
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;
}