Flutter AnimatedList itemsCount is not true - android

I'm managing items in an AnimatedList widget the code is below. And here is widget that builds animated list and data is pulled from list map.
Stack(
children: <Widget>[
AnimatedList(
key: Global.globalKey,
initialItemCount: model.listTiles.length,
itemBuilder: (context, index, animation) {
bool isLast = index == model.listTiles.length - 1;
return SlideTransition(
position: animation.drive(model.tween),
child: Padding(
padding:
EdgeInsets.only(bottom: isLast ? 300.0 : 0.0),
child: MenuListWidget(
item: model.listTiles[index], indextile: index),
),
);
},
),
GestureDetector(
onVerticalDragStart: (start) {
model.verticalDragStart = start.globalPosition.dy;
},
onVerticalDragUpdate: (update) {
if (model.verticalDragStart > update.globalPosition.dy) {
model.setScrollDirection(BoxScrollDirection.Up);
} else {
model.setScrollDirection(BoxScrollDirection.Down);
}
},
onVerticalDragEnd: (_) {
if (model.state == ViewState.Idle) {
if (model.scroll == BoxScrollDirection.Down &&
!model.isToggled) {
print('GOING DOWN');
model.toggleMenu(height: height);
//model.uploadItems();
} else if (model.scroll == BoxScrollDirection.Up &&
model.isToggled) {
print('GOING UP');
model.toggleMenu(height: height);
//model.deleteItems();
}
}
},
child: AnimatedContainer(
transform: Matrix4.identity()
..translate(0.0, model.translateY),
duration: Duration(milliseconds: 500),
curve: model.curve,
child: //homeScreen()
Screen(
builder: (context) => homeScreen(),
),
),
),
],
and this is my homemodel that strucutres deletion,addition and animation of items
and this is my model to handle deletion,isert,animation of items
class HomeModel {
//PUBLIC
bool isToggled = false;
double translateY = 0.0;
List<MenuData> listTiles = [];
Curve curve = Curves.easeInOutQuad;
MenuData? _active; //= MenuData.fromMap(Global.menuItems.first) ;
MenuData? get active => _active;
set active(MenuData? item) {
_active = item;
notifyListeners();
}
//METHODS
void getListTiles() {
listTiles = Global.menuItems.map((item) => MenuData.fromMap(item)).toList();
notifyListeners();
}
void additem(Map item) {
//Global.globalKey.currentState?.insertItem(index)
Global.menuItems.add(item);
//listTiles.add(item);
notifyListeners();
}
void toggleMenu({height}) {
isToggled = !isToggled;
getListTiles();
if (isToggled) {
translateY = height - 180;
uploadItems();
} else {
translateY = 0.0;
deleteItems();
}
notifyListeners();
}
Animatable<Offset> get tween => _tween();
Animatable<Offset> _tween() {
var begin = Offset(0.0, isToggled ? 1.0 : 2.0);
var end = Offset.zero;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return tween;
}
void uploadItems() async {
//setState(ViewState.Busy);
final reversed = Global.menuItems.reversed.toList();
for (int i = 0; i < reversed.length; i++) {
await Future.delayed(
Duration(milliseconds: 75),
() {
Global.globalKey.currentState!
.insertItem(i, duration: Duration(milliseconds: 100));
},
);
}
//setState(ViewState.Idle);
}
void deleteItems() {
//setState(ViewState.Busy);
for (int i = Global.menuItems.length - 1; i >= 0; i--) {
final deletedItem = listTiles.removeAt(i);
Global.globalKey.currentState?.removeItem(i, (context, animation) {
return SlideTransition(
position: animation.drive(tween),
child: MenuListWidget(item: deletedItem, indextile: i),
);
}, duration: Duration(milliseconds: 200));
}
//setState(ViewState.Idle);
}
void removeitem(int index) {
//setState(ViewState.Busy);
//int index = listTiles.indexWhere((element) => element.name == item.name);
// listTiles.reversed
// .forEach((index) {
// final reservation = oldReservations[index];
// widget.listKey.currentState?.removeItem(
// index,
listTiles.reversed.toList();
final deletedItem = listTiles.removeAt(index);
Global.globalKey.currentState?.removeItem(index, (context, animation) {
return MenuListWidget(item: deletedItem, indextile: index);
}, duration: Duration(milliseconds: 200));
Global.menuItems.reversed.map((item) => MenuData.fromMap(item)).toList();
//uploadItems();
//getListTiles();
//setState(ViewState.Idle);
notifyListeners();
}
}
The problem is that when i press on delete of item in the animated list or add item get error that says
══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
The following assertion was thrown while handling a gesture:
'package:flutter/src/widgets/animated_list.dart': Failed assertion: line 621 pos 12: 'itemIndex >= 0
&& itemIndex < _itemsCount': is not true.
Either the assertion indicates an error in the framework itself, or we should provide substantially
more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
https://github.com/flutter/flutter/issues/new?template=2_bug.md
When the exception was thrown, this was the stack:
#2 SliverAnimatedListState.removeItem (package:flutter/src/widgets/animated_list.dart:621:12)
#3 AnimatedListState.removeItem (package:flutter/src/widgets/animated_list.dart:305:42)
#4 HomeModel.deleteItems (package:wire/core/viewmodels/home_model.dart:87:38)
#5 HomeModel.toggleMenu (package:wire/core/viewmodels/home_model.dart:54:7)
#6 HomeView.build.<anonymous closure> (package:wire/ui/views/home_view.dart:100:33)
#7 DragGestureRecognizer._checkEnd.<anonymous closure>
(package:flutter/src/gestures/monodrag.dart:521:47)
#8 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:253:24)
#9 DragGestureRecognizer._checkEnd (package:flutter/src/gestures/monodrag.dart:521:5)
#10 DragGestureRecognizer.didStopTrackingLastPointer (package:flutter/src/gestures/monodrag.dart:426:9)
#11 OneSequenceGestureRecognizer.stopTrackingPointer (package:flutter/src/gestures/recognizer.dart:446:9)
#12 DragGestureRecognizer._giveUpPointer (package:flutter/src/gestures/monodrag.dart:435:5)
#13 DragGestureRecognizer.handleEvent (package:flutter/src/gestures/monodrag.dart:354:7)
#14 PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12)
#15 PointerRouter._dispatchEventToRoutes.<anonymous closure>
How can i solve this problem?

Related

How to handle state of individual items in AnimatedList by cubit

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));
}
}
}

Custom Text Selection Toolbar in Flutter

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,
),
),
),
],
),
);
}
}

Unable to get Context update in Flutter even after updating the respective variables

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.

Controller's length issue in TabBar + BottomBarNavigation scenario

I am trying to build an app which uses a TabBar and a BottomBarNavigation.
I tried to manage the length of the DefaultTabController in several ways, but I keep having an error only when I go from index 2 to index 1 :
Controller's length property (3) does not match the flutter: number of tab elements (2) present in TabBar's tabs property.
My code :
import 'package:flutter/material.dart';
import './activites.dart';
import './evenements.dart';
import './offres_promos.dart';
class PrivesPage extends StatefulWidget {
#override
State createState() => PrivesPageState();
}
class PrivesPageState extends State<PrivesPage>
with SingleTickerProviderStateMixin {
int _index;
int _length;
#override
void initState() {
super.initState();
_index = 0;
_length = 3;
}
Widget _buildTabBar() {
Widget _content;
if (_index == 0) {
_content = TabBar(tabs: <Tab>[
Tab(text: "Tab 1 - index 0"),
Tab(text: "Tab 2 - index 0"),
Tab(text: "Tab 3 - index 0"),
]);
} else if (_index == 1) {
_content = TabBar(tabs: <Tab>[
Tab(text: "Tab 1 - index 1"),
Tab(text: "Tab 2 - index 1"),
]);
} else if (_index == 2) {
_content = null;
}
return _content;
}
Widget _buildTabBarView() {
Widget _content;
if (_index == 0) {
_content = TabBarView(children: <Widget>[
Page1(),
Page2(),
Page3(),
]);
} else if (_index == 1) {
_content = TabBarView(
children: <Widget>[
Page1(),
Page2(),
],
);
} else if (_index == 2) {
_content = TabBarView(
children: <Widget>[
],
);
}
return _content;
}
#override
Widget build(BuildContext context) {
print('Index ' + this._index.toString());
print('length ' + this._length.toString());
return DefaultTabController(
length: _length,
child: Scaffold(
appBar: AppBar(
title: Text("Home"),
bottom: _buildTabBar(),
),
body: _buildTabBarView(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (int _index) {
int l;
if (_index == 0) {
l = 3;
} else if (_index == 1) {
l = 2;
} else if (_index == 2) {
l = 0;
}
setState(() {
this._length = l;
this._index = _index;
});
},
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.my_location),
title: Text("Around me"),
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text("My city"),
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
title: Text("My account"),
),
]),
));
}
}
I also tried to correct the issue using the TabBarController to set the length in the initState method but it does not work either.
TabController _controller;
int _index;
#override
void initState() {
super.initState();
_controller = new TabController(length: _length, vsync: this);
_index = 0;
}
I think I may not have find the right way to accomplish this but it is the only way that got so far in the development of this solution.
Can you please help me correcting my code or build this in another way.
Thanks !
You are changing the tab bar length but not updating the controller's one. Update it accordingly by adding it to your setState for example.
setState(() {
this._length = l;
this._controller = new TabController(length: _length, vsync: this);
this._index = _index;
});
Note : Use a TickerProviderStateMixin instead of a SingleTickerProviderStateMixin in your class

Changing colour of CustomPaint changes for all previous points

So I'm trying to create a draw app using Flutter following the "signature canvas" method. However, I'm having trouble being able to change the colour of the CustomPaint object without it already changing the colours for each line draw before the change as shown here:
As you can see, the colour change happens once the Page Widget's state is changed (either by clicking on the main FAB or if I were to draw on the canvas again). Here is my code below for my DrawPage:
class DrawPage extends StatefulWidget {
#override
DrawPageState createState() => new DrawPageState();
}
class DrawPageState extends State<DrawPage> with TickerProviderStateMixin {
AnimationController controller;
List<Offset> points = <Offset>[];
Color color = Colors.black;
StrokeCap strokeCap = StrokeCap.round;
double strokeWidth = 5.0;
#override
void initState() {
super.initState();
controller = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox object = context.findRenderObject();
Offset localPosition =
object.globalToLocal(details.globalPosition);
points = new List.from(points);
points.add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => points.add(null),
child: CustomPaint(
painter: Painter(
points: points,
color: color,
strokeCap: strokeCap,
strokeWidth: strokeWidth),
size: Size.infinite,
),
),
),
floatingActionButton:
Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
Container(
height: 70.0,
width: 56.0,
alignment: FractionalOffset.topCenter,
child: ScaleTransition(
scale: CurvedAnimation(
parent: controller,
curve: Interval(0.0, 1.0 - 0 / 3 / 2.0, curve: Curves.easeOut),
),
child: FloatingActionButton(
mini: true,
child: Icon(Icons.clear),
onPressed: () {
points.clear();
},
),
),
),
Container(
height: 70.0,
width: 56.0,
alignment: FractionalOffset.topCenter,
child: ScaleTransition(
scale: CurvedAnimation(
parent: controller,
curve: Interval(0.0, 1.0 - 1 / 3 / 2.0, curve: Curves.easeOut),
),
child: FloatingActionButton(
mini: true,
child: Icon(Icons.lens),
onPressed: () {},
),
),
),
Container(
height: 70.0,
width: 56.0,
alignment: FractionalOffset.topCenter,
child: ScaleTransition(
scale: CurvedAnimation(
parent: controller,
curve:
Interval(0.0, 1.0 - 2 / 3 / 2.0, curve: Curves.easeOut),
),
child: FloatingActionButton(
mini: true,
child: Icon(Icons.color_lens),
onPressed: () async {
Color temp;
temp = await showDialog(
context: context,
builder: (context) => ColorDialog());
if (temp != null) {
setState(() {
color = temp;
});
}
}))),
FloatingActionButton(
child: AnimatedBuilder(
animation: controller,
builder: (BuildContext context, Widget child) {
return Transform(
transform: Matrix4.rotationZ(controller.value * 0.5 * math.pi),
alignment: FractionalOffset.center,
child: Icon(Icons.brush),
);
},
),
onPressed: () {
if (controller.isDismissed) {
controller.forward();
} else {
controller.reverse();
}
},
),
]),
);
}
}
What I've tried so far:
I've tried playing around with how the points are added to my List of Offsets as this list is recreated after each "draw" gesture such as just adding to the current List without recreating it but this breaks the "draw" gesture:
setState(() {
RenderBox object = context.findRenderObject();
Offset localPosition =
object.globalToLocal(details.globalPosition);
points = new List.from(points);
points.add(localPosition);
});
I've tried making a reference to the CustomPaint object or my Painter object outside of the build() scope and updating the color property that way but this also breaks the "draw" gesture.
Any help would be greatly appreciated!
Also, here's the code for my Painter class in case people wish to see it:
class Painter extends CustomPainter {
List<Offset> points;
Color color;
StrokeCap strokeCap;
double strokeWidth;
Painter({this.points, this.color, this.strokeCap, this.strokeWidth});
#override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint();
paint.color = color;
paint.strokeCap = strokeCap;
paint.strokeWidth = strokeWidth;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
#override
bool shouldRepaint(Painter oldPainter) => oldPainter.points != points;
}
I think, for different colors you have to use different Paints. I've added small changes to your code, it works.
class DrawPageState extends State<DrawPage> with TickerProviderStateMixin {
...
List<Painter> painterList = [];
#override
Widget build(BuildContext context) {
...
child: CustomPaint(
painter: Painter(
points: points, color: color, strokeCap: strokeCap, strokeWidth: strokeWidth, painters: painterList),
size: Size.infinite,
),
...
onPressed: () async {
Color temp;
temp = await showDialog(
context: context,
builder: (context) => ColorDialog());
if (temp != null) {
setState(() {
painterList
.add(Painter(points: points.toList(), color: color, strokeCap: strokeCap, strokeWidth: strokeWidth));
points.clear();
strokeCap = StrokeCap.round;
strokeWidth = 5.0;
color = temp;
});
}
...
}
}
class Painter extends CustomPainter {
List<Offset> points;
Color color;
StrokeCap strokeCap;
double strokeWidth;
List<Painter> painters;
Painter({this.points, this.color, this.strokeCap, this.strokeWidth, this.painters = const []});
#override
void paint(Canvas canvas, Size size) {
for (Painter painter in painters) {
painter.paint(canvas, size);
}
Paint paint = new Paint()
..color = color
..strokeCap = strokeCap
..strokeWidth = strokeWidth;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
#override
bool shouldRepaint(Painter oldDelegate) => oldDelegate.points != points;
}
2020, I have a nice solution for this, because the actually chosen doesn't work for me and I see some redundant calls.
So, I start creating a small class:
class _GroupPoints {
Offset offset;
Color color;
_GroupPoints({this.offset, this.color});
}
next, i declare my CustomPainter like this:
class Signature extends CustomPainter {
List<_GroupPoints> points;
Color color;
Signature({
this.color,
this.points,
});
#override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()
// if you need this next params as dynamic, you can move it inside the for part
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < newPoints.length - 1; i++) {
paint.color = points[i].color;
if (points[i].offset != null && points[i + 1].offset != null) {
canvas.drawLine(points[i].offset, points[i + 1].offset, paint);
}
canvas.clipRect(Offset.zero & size);
}
}
#override
bool shouldRepaint(Signature oldDelegate) => true;
}
And on my widget:
...
class _MyPageState extends State<MyPage> {
...
List<_GroupPoints> points = [];
...
Container(
height: 500,
width: double.infinity,
child: GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
points = new List.from(points)
..add(
new _GroupPoints(
offset: details.localPosition,
color: myDynamicColor,
),
);
});
},
onPanEnd: (DragEndDetails details) {
points.add(
_GroupPoints(
color: myDynamicColor,
offset: null),
);
},
child: CustomPaint(
painter: Signature(
newPoints: points,
color: myDynamicColor,
),
),
),
),
}
On this way we can use multiple draws of points with their respective color. Hope this can help anybody.

Categories

Resources