Related
A few days I have problem to complete idea about how should this work.
I want to manage state of each model (item in AnimatedList) by its ListItemCubit. Whole list has its own cubit - ConstructionLogsCubit. It's responsible for loading items, show circular indicator, when they are loading and adding item, when create form is submitted. When I arrive to ListPageLayout, actual items are loaded and with staged animation placed on its place. First problem is that when I scroll down by smart refresher, it work only once and next time it is disabled.
Create and item update form has its own cubit - ConstructionLogFormCubit. It contains controllers and initial values setting, when item is updating. When I submit create form, I want to listen for that form cubit in ConstructionLogsCubit, so on top of list I can render new item with loading spinner in its center. When it's rendered, its ListItemCubit must start to listen to same ConstructionLogFormCubit. So that when user has slow internet or is offline, he sees, that item is creating or he can tap to retry / update form and try again. My main problem is that I don't know, how to handle that redirecting of listening, because meanwhile user can swipe down list and list is going to rerender. I think, that I must then check identifier of each local item and fetched item for rerender. When item is creating we cannot check id, so we are going to put hash to each model, that I will send to backend and then, when item with that hash arrive, I can remove local item with that hash, because if form completes in the middle of refresh, ListItemCubit will change its value from loading spinner to detail of that item, so then i will have in list two same items.
Next thing is that always, when I arrive to that page, I must see, that it is loading or not.*
ListPageLayout.dart
abstract class NewListPageLayout<M extends Model, T extends ModelCubit>
extends StatefulWidget {
final String title;
final String? createButtonText;
const NewListPageLayout({
Key? key,
required this.title,
this.createButtonText,
}) : super(key: key);
#override
State<NewListPageLayout<M, T>> createState() =>
_NewListPageLayoutState<M, T>();
Widget buildTile(BuildContext context, M item);
void createButtonAction(BuildContext context);
}
class _NewListPageLayoutState<M extends Model, T extends ModelCubit>
extends State<NewListPageLayout<M, T>> {
GlobalKey<AnimatedListState>? listKey = GlobalKey<AnimatedListState>();
late ModelCubit bloc;
List<Widget> items = [];
addItem(M item) {
WidgetsBinding.instance.addPostFrameCallback((_) {
items.add(widget.buildTile(context, item));
listKey?.currentState
?.insertItem(items.length - 1, duration: Duration(milliseconds: 400));
});
}
removeItem(int index, M item) {
WidgetsBinding.instance.addPostFrameCallback((_) {
items.removeAt(index);
listKey?.currentState?.removeItem(index,
(ctx, animation) => buildTileWithAnimation(ctx, index, animation),
duration: Duration(milliseconds: 400));
});
}
#override
void didChangeDependencies() {
bloc = BlocProvider.of<T>(context);
}
#override
Widget build(BuildContext context) {
return AdminPageLayout(
title: widget.title,
child: BlocConsumer(
bloc: bloc,
listener: (context, state) {
if (state is ModelListLoaded) {}
},
buildWhen: (s1, s2) {
return s1.runtimeType != s2.runtimeType;
},
builder: (context, state) {
final cubit = context.watch<T>();
final state = cubit.state;
if (state is ModelListLoaded) {
items.clear();
listKey = GlobalKey<AnimatedListState>(); //);
Future fut = Future(() {});
state.items.forEach((element) {
fut = fut.then((value) => Future.delayed(
const Duration(milliseconds: 100),
() => addItem(element),
));
});
}
return Refresher(
onDrag: () async {
await bloc.load();
},
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 32, bottom: 24),
child: PrimaryButton(
text: widget.createButtonText,
leftWidget: loadExportedSvgIcon('add', size: 24),
mainAxisAlignment: MainAxisAlignment.start,
onPressed: () async {
// addItem(ConstructionLog() as M);
widget.createButtonAction(context);
},
),
),
if (state is ModelListLoaded)
Container(
child: AnimatedList(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
key: listKey,
itemBuilder: buildTileWithAnimation,
initialItemCount: 0,
),
)
else
const Center(
child: MyLoadingSpinner(),
),
],
),
),
);
},
));
}
Widget buildTileWithAnimation(
BuildContext ctx, int index, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: const Offset(0, 0),
).animate(animation),
child: FadeTransition(
opacity: animation,
child: Padding(
padding: EdgeInsets.only(
top: widget.createButtonText != null || (index != 0) ? 0 : 32.0,
bottom: index != items.length - 1 ? 16 : 0),
child: BlocProvider(
create: (context) => ListItemCubit(
(bloc.state as ModelListLoaded<Model>).items[index],
bloc,
(bloc.state as ModelListLoaded<Model>).items[index].id == 0 &&
index == 0
? bloc.createFormStream
: null),
child: Builder(
builder: (context) {
final itemBloc = context.watch<ListItemCubit>();
Model item = (bloc.state as ModelListLoaded).items[index];
if (itemBloc.state is ListItemUpdated) {
(bloc.state as ModelListLoaded<Model>).items[index] =
(itemBloc.state as ListItemUpdated).model;
item = (itemBloc.state as ListItemUpdated).model;
}
return Stack(
clipBehavior: Clip.hardEdge,
children: [
(item.id == 0)
? const RoundedContainer(
bottomMargin: 0,
color: UIColors.gray3,
child: Center(
child: MyLoadingSpinner(),
),
)
: widget.buildTile(context, item as M),
Positioned.fill(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: bloc.state is ListItemLoading
? const RoundedContainer(
bottomMargin: 0,
color: UIColors.gray3,
child: Center(
child: MyLoadingSpinner(),
),
)
: const SizedBox.shrink()),
)
],
);
},
)),
),
),
);
}
}
ConstructionLogFormCubit
class ConstructionLogFormCubit extends MyFormCubit {
// final ConstructionLogsRepository bcRepo = ConstructionLogsRepository();
final BusinessCase? businessCase;
final ConstructionLog? item;
ConstructionLogFormCubit({this.businessCase, this.item}) : super(1);
// final ConstructionLog item;
// ConstructionLogFormCubit() : super(InitializingConstructionLogForm()) {
// // if (item != null) {
// // _loadEditInputs();
// // } else {
// _loadCreateInputs();
// // }
// }
#override
load() async {
emit(OpenedConstructionLogFormState());
if (item != null) {
OpenedConstructionLogFormState st = state as OpenedConstructionLogFormState;
// st.imagesController.images = item!.images!;
st.dateController.value = item!.date!;
st.descriptionController.text = item!.description!;
st.machinesController.text = item!.machines ?? '';
st.subSupplyCompaniesController.text = item!.subsupplyCompanies ?? '';
st.employeesController.values = item!.employees!.map((e) => e.id).toList();
st.weatherController.value = item!.weather;
// TODO: aj obrazky
}
}
#override
MyFormOpened createOpenedState() {
return OpenedConstructionLogFormState();
}
#override
submit() async {
if (state is OpenedConstructionLogFormState) {
try {
OpenedConstructionLogFormState st =
state as OpenedConstructionLogFormState;
emit(MyFormSubmitting(ConstructionLog()));
await Future.delayed(const Duration(seconds: 3));
// if (item == null) {
// await ConstructionLogsProvider.create(data);
// } else {
// await ConstructionLogsProvider.update(data, item.id);
// }
emit(MyFormSubmitted(1));
} catch (e) {
emit(MyFormError(e));
//print(e);
}
}
}
}
AppRouter
case ConstructionLogForm.routeName:
final ConstructionLogFormCubit cubit = ConstructionLogFormCubit(
// businessCase: argument!.businessCase,
item: argument?.model == null ? null : argument?.model as ConstructionLog);
if (argument?.itemCubit != null) {
argument?.itemCubit!.registerFormCubit(cubit);
}else if (argument?.listCubit != null) {
argument?.listCubit!.registerFormCubit(cubit);
} else if (argument?.detailCubit != null) {
// final _detailCubit = settings.arguments as ReceiptCubit;
// _detailCubit.registerFormCubit(cubit);
}
Refresher
class Refresher extends StatelessWidget {
final Function()? onDrag;
final Widget child;
final RefreshController _refreshController =
RefreshController(initialRefresh: false);
// GlobalKey _refresherKey = GlobalKey();
Refresher({Key? key, this.onDrag, required this.child}) : super(key: key);
void _onRefresh() async {
if(onDrag != null) await onDrag!();
_refreshController.refreshCompleted();
}
void _onLoading() async {
if(onDrag != null) await onDrag!();
_refreshController.loadComplete();
}
#override
Widget build(BuildContext context) {
// return child;
return SmartRefresher(
// key: _refresherKey,
enablePullDown: true,
header: const MaterialClassicHeader(
color: UIColors.primary,
backgroundColor: Colors.transparent,
),
footer: CustomFooter(
builder: (BuildContext context, LoadStatus? mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = Text("pull up load");
} else if (mode == LoadStatus.loading) {
body = const CupertinoActivityIndicator();
} else if (mode == LoadStatus.failed) {
body = Text("Load Failed!Click retry!");
} else if (mode == LoadStatus.canLoading) {
body = Text("release to load more");
} else {
body = Text("No more Data");
}
return Container(
height: 55.0,
child: Center(child: body),
);
},
),
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
child: child,
);
}
}
ConstructionLogsCubit
import 'dart:async';
import 'package:listform/logic/bloc/ModelCubit.dart';
import 'package:listform/logic/bloc/ModelState.dart';
import 'package:listform/logic/bloc/MyFormCubit.dart';
import 'package:listform/logic/models/business_case.dart';
import 'package:listform/logic/models/construction_log.dart';
import 'package:listform/logic/repositories/contruction_log_repository.dart';
class ConstructionLogsCubit extends ModelCubit {
final BusinessCase businessCase;
ConstructionLogsCubit(this.businessCase);
#override
Future<void> load() async {
emit(const ModelLoading());
emit(ModelListLoaded<ConstructionLog>(
await ConstructionLogRepository().getAll(businessCase.id)));
// await Future.delayed(Duration(seconds: 2));
// emit(ModelListLoaded<ConstructionLog>(
// (state as ModelListLoaded<ConstructionLog>).items..insert(0,ConstructionLog(
// 1, DateTime.now(), EWeather.Cloudy, [], 'description', [Employee(1, 'name')])),
// newItemIsCreating: true));
}
#override
void afterSubmittingForm(event) async {
if (event is MyFormSubmitting &&
state is ModelListLoaded<ConstructionLog>) {
(state as ModelListLoaded<ConstructionLog>)
.items
.insert(0, event.temporaryModel as ConstructionLog);
emit(ModelListLoaded<ConstructionLog>(
(state as ModelListLoaded<ConstructionLog>).items,
// newItems: (state as ModelListLoaded<ConstructionLog>).newItems.add(event.temporaryModel as ConstructionLog)
newItemsCount:
(state as ModelListLoaded<ConstructionLog>).newItemsCount + 1));
}
else if (event is MyFormSubmitted &&
state is ModelListLoaded<ConstructionLog>) {
(state as ModelListLoaded<ConstructionLog>).items[
(state as ModelListLoaded<ConstructionLog>).items.indexWhere(
(element) => element.id == 0,
)] = await ConstructionLogRepository().getDetail(event.id!);
emit(ModelListLoaded<ConstructionLog>(
(state as ModelListLoaded<ConstructionLog>).items,
newItemsCount:
(state as ModelListLoaded<ConstructionLog>).newItemsCount - 1));
}
}
}
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.
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,
),
),
),
],
),
);
}
}
In the Below code even though My var Light and var Fan are updated on a regular intervals the overall text content var str on screen is not updated. I am a beginner to flutter sorry if the question sounds too dumb 😉 I did keep in mind about the stateless and stateful widget stuff i am just unable to figure out why its not updating on screen.
class _MyHomePageState extends State<MyHomePage> {
var Fan = "Off";
var Light = "Off";
var str = "";
int fanState, lightState;
final firestoreInstance = Firestore.instance;
final databaseReference = Firestore.instance;
#override
void initState() {
// TODO: implement initState
super.initState();
getData();
}
#override
Widget build(BuildContext context) {
return ThemeSwitchingArea(
child: Scaffold(
drawer: Drawer(
child: SafeArea(
child: Stack(
children: <Widget>[
Align(
alignment: Alignment.topRight,
child: ThemeSwitcher(
builder: (context) {
return IconButton(
onPressed: () {
ThemeSwitcher.of(context).changeTheme(
theme: ThemeProvider.of(context).brightness ==
Brightness.light
? darkTheme
: lightTheme,
);
},
icon: Icon(Icons.brightness_3, size: 25),
);
},
),
),
],
),
),
),
appBar: AppBar(
title: Text(
'Home Control',
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text(
str,
style: TextStyle(fontSize: 30),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ThemeSwitcher(
builder: (context) {
return Checkbox(
value: ThemeProvider.of(context) == darkBlueTheme,
onChanged: (needDarkBlue) {
ThemeSwitcher.of(context).changeTheme(
theme: needDarkBlue ? darkBlueTheme : lightTheme,
);
triggerLight();
},
);
},
),
ThemeSwitcher(
builder: (context) {
return Checkbox(
value: ThemeProvider.of(context) == halloweenTheme,
onChanged: (needBlue) {
ThemeSwitcher.of(context).changeTheme(
theme: needBlue ? halloweenTheme : lightTheme,
);
triggerFan();
},
);
},
),
],
),
],
),
),
),
);
}
void getData() {
databaseReference
.collection("Home")
.getDocuments()
.then((QuerySnapshot snapshot) {
snapshot.documents.forEach((f) {
fanState = f.data['Fan'];
lightState = f.data['Light'];
if ((fanState == 1) && (lightState == 1)) {
Fan = "On";
Light = "On";
} else if ((fanState == 0) && (lightState == 1)) {
Fan = "Off";
Light = "On";
} else if ((fanState == 1) && (lightState == 0)) {
Fan = "On";
Light = "Off";
} else {
Fan = "Off";
Light = "Off";
}
str = "Fan Status: $Fan" + "\n" + "Light Status: $Light";
});
});
}
void triggerFan() {
print("Fan Triggered");
if (fanState == 1) {
databaseReference
.collection("Home")
.document("myroom")
.updateData({'Fan': 0}).then((value) {
print("Status Updated Off");
Fan = "Off";
fanState = 0;
getData();
}).catchError((error) => print("Failed to add user: $error"));
} else {
databaseReference
.collection("Home")
.document("myroom")
.updateData({'Fan': 1}).then((value) {
print("Status Updated On");
Fan = "On";
fanState = 1;
getData();
}).catchError((error) => print("Failed to add user: $error"));
}
}
void triggerLight() {
print("Light Triggered");
if (lightState == 1) {
databaseReference
.collection("Home")
.document("myroom")
.updateData({'Light': 0}).then((value) {
print("Status Updated to Off");
Light = "Off";
lightState = 0;
getData();
}).catchError((error) => print("Failed to add user: $error"));
} else {
databaseReference
.collection("Home")
.document("myroom")
.updateData({'Light': 1}).then((value) {
print("Status Updated to On");
Light = "On";
lightState = 1;
getData();
}).catchError((error) => print("Failed to add user: $error"));
}
}
}
Please use this Code to update your state of widgets in Statefull Class
setState(() {
//your Code Here
});
To know more about the Stateless and Staefull Class Please see this link from flutter.
For A simple Example, If I have a Statefull Class and have I am having a counter variable in that class set to 0 :
int counter = 0;
But, on some method call I want to increase that counter variable by 1, then I have to change the state for that counter variable and use this line of code to change its state.
setState(() {
counter = counter + 1;
});
Now, This state change of counter variable will effect on your UI too, and You will see that your UI is also updated with the changed counter value.
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',
),
),
),
),
],
);
}
}