How to resolve the Show Snack Bar Error in Line Context - android

Recently i am trying to run the project but seem the snackbar is have some kind of error like this
the compiler show the error below the "context" message
how to solve it?
void showSnackBarSuccess(BuildContext context, String text) {
showTopSnackBar(
context,
CustomSnackBar.success(
message: text,
),
);
}
void showSnackBarInfo(BuildContext context, String text) {
showTopSnackBar(
context,
CustomSnackBar.info(
message: text,
),
);
}
void showSnackBarError(BuildContext context, String text) {
showTopSnackBar(
context,
CustomSnackBar.error(
message: text,
),
);
}
Here the error of the compile
Launching lib\main.dart on Edge in debug mode...
lib\main.dart:1
: Error: The argument type 'BuildContext' can't be assigned to the parameter type 'OverlayState'.
lib/…/snackar/show_snackbar.dart:16
- 'BuildContext' is from 'package:flutter/src/widgets/framework.dart' ('/C:/src/flutter_windows_3.0.4-stable/flutter/packages/flutter/lib/src/widgets/framework.dart').
package:flutter/…/widgets/framework.dart:1
- 'OverlayState' is from 'package:flutter/src/widgets/overlay.dart' ('/C:/src/flutter_windows_3.0.4-stable/flutter/packages/flutter/lib/src/widgets/overlay.dart').
package:flutter/…/widgets/overlay.dart:1
context,
^
: Error: The argument type 'BuildContext' can't be assigned to the parameter type 'OverlayState'.
lib/…/snackar/show_snackbar.dart:25
Here the full of source code of TopSnackBar
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:top_snackbar_flutter/safe_area_values.dart';
import 'package:top_snackbar_flutter/tap_bounce_container.dart';
typedef ControllerCallback = void Function(AnimationController);
enum DismissType { onTap, onSwipe, none }
OverlayEntry? _previousEntry;
/// The [overlayState] argument is used to add specific overlay state.
/// If you are sure that there is a overlay state in your [BuildContext],
/// You can get it [Overlay.of(BuildContext)]
/// Displays a widget that will be passed to [child] parameter above the current
/// contents of the app, with transition animation
///
/// The [child] argument is used to pass widget that you want to show
///
/// The [animationDuration] argument is used to specify duration of
/// enter transition
///
/// The [reverseAnimationDuration] argument is used to specify duration of
/// exit transition
///
/// The [displayDuration] argument is used to specify duration displaying
///
/// The [onTap] callback of [_TopSnackBar]
///
/// The [persistent] argument is used to make snack bar persistent, so
/// [displayDuration] will be ignored. Default is false.
///
/// The [onAnimationControllerInit] callback is called on internal
/// [AnimationController] has been initialized.
///
/// The [padding] argument is used to specify amount of outer padding
///
/// [curve] and [reverseCurve] arguments are used to specify curves
/// for in and out animations respectively
///
/// The [safeAreaValues] argument is used to specify the arguments of the
/// [SafeArea] widget that wrap the snackbar.
///
/// The [dismissType] argument specify which action to trigger to
/// dismiss the snackbar. Defaults to `TopSnackBarDismissType.onTap`
///
/// The [dismissDirection] argument specify in which direction the snackbar
/// can be dismissed. This argument is only used when [dismissType] is equal
/// to `DismissType.onSwipe`. Defaults to `[DismissDirection.up]`
void showTopSnackBar(
OverlayState overlayState,
Widget child, {
Duration animationDuration = const Duration(milliseconds: 1200),
Duration reverseAnimationDuration = const Duration(milliseconds: 550),
Duration displayDuration = const Duration(milliseconds: 3000),
VoidCallback? onTap,
bool persistent = false,
ControllerCallback? onAnimationControllerInit,
EdgeInsets padding = const EdgeInsets.all(16),
Curve curve = Curves.elasticOut,
Curve reverseCurve = Curves.linearToEaseOut,
SafeAreaValues safeAreaValues = const SafeAreaValues(),
DismissType dismissType = DismissType.onTap,
List<DismissDirection> dismissDirection = const [DismissDirection.up],
}) {
late OverlayEntry _overlayEntry;
_overlayEntry = OverlayEntry(
builder: (_) {
return _TopSnackBar(
onDismissed: () {
_overlayEntry.remove();
_previousEntry = null;
},
animationDuration: animationDuration,
reverseAnimationDuration: reverseAnimationDuration,
displayDuration: displayDuration,
onTap: onTap,
persistent: persistent,
onAnimationControllerInit: onAnimationControllerInit,
padding: padding,
curve: curve,
reverseCurve: reverseCurve,
safeAreaValues: safeAreaValues,
dismissType: dismissType,
dismissDirections: dismissDirection,
child: child,
);
},
);
if (_previousEntry != null && _previousEntry!.mounted) {
_previousEntry?.remove();
}
overlayState.insert(_overlayEntry);
_previousEntry = _overlayEntry;
}
/// Widget that controls all animations
class _TopSnackBar extends StatefulWidget {
const _TopSnackBar({
Key? key,
required this.child,
required this.onDismissed,
required this.animationDuration,
required this.reverseAnimationDuration,
required this.displayDuration,
required this.padding,
required this.curve,
required this.reverseCurve,
required this.safeAreaValues,
required this.dismissDirections,
this.onTap,
this.persistent = false,
this.onAnimationControllerInit,
this.dismissType = DismissType.onTap,
}) : super(key: key);
final Widget child;
final VoidCallback onDismissed;
final Duration animationDuration;
final Duration reverseAnimationDuration;
final Duration displayDuration;
final VoidCallback? onTap;
final ControllerCallback? onAnimationControllerInit;
final bool persistent;
final EdgeInsets padding;
final Curve curve;
final Curve reverseCurve;
final SafeAreaValues safeAreaValues;
final DismissType dismissType;
final List<DismissDirection> dismissDirections;
#override
_TopSnackBarState createState() => _TopSnackBarState();
}
class _TopSnackBarState extends State<_TopSnackBar>
with SingleTickerProviderStateMixin {
late final Animation<Offset> _offsetAnimation;
late final AnimationController _animationController;
Timer? _timer;
final _offsetTween = Tween(begin: const Offset(0, -1), end: Offset.zero);
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
reverseDuration: widget.reverseAnimationDuration,
);
_animationController.addStatusListener(
(status) {
if (status == AnimationStatus.completed && !widget.persistent) {
_timer = Timer(widget.displayDuration, () {
if (mounted) {
_animationController.reverse();
}
});
}
if (status == AnimationStatus.dismissed) {
_timer?.cancel();
widget.onDismissed.call();
}
},
);
widget.onAnimationControllerInit?.call(_animationController);
_offsetAnimation = _offsetTween.animate(
CurvedAnimation(
parent: _animationController,
curve: widget.curve,
reverseCurve: widget.reverseCurve,
),
);
if (mounted) {
_animationController.forward();
}
super.initState();
}
#override
void dispose() {
_animationController.dispose();
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Positioned(
top: widget.padding.top,
left: widget.padding.left,
right: widget.padding.right,
child: SlideTransition(
position: _offsetAnimation,
child: SafeArea(
top: widget.safeAreaValues.top,
bottom: widget.safeAreaValues.bottom,
left: widget.safeAreaValues.left,
right: widget.safeAreaValues.right,
minimum: widget.safeAreaValues.minimum,
maintainBottomViewPadding:
widget.safeAreaValues.maintainBottomViewPadding,
child: _buildDismissibleChild(),
),
),
);
}
/// Build different type of [Widget] depending on [DismissType] value
Widget _buildDismissibleChild() {
switch (widget.dismissType) {
case DismissType.onTap:
return TapBounceContainer(
onTap: () {
widget.onTap?.call();
if (!widget.persistent && mounted) {
_animationController.reverse();
}
},
child: widget.child,
);
case DismissType.onSwipe:
var childWidget = widget.child;
for (final direction in widget.dismissDirections) {
childWidget = Dismissible(
direction: direction,
key: UniqueKey(),
dismissThresholds: const {DismissDirection.up: 0.2},
confirmDismiss: (direction) async {
if (!widget.persistent && mounted) {
if (direction == DismissDirection.down) {
await _animationController.reverse();
} else {
_animationController.reset();
}
}
return false;
},
child: childWidget,
);
}
return childWidget;
case DismissType.none:
return widget.child;
}
}
}
I am already try to add some "this" line beside the "context" line but still not worked
I am recently edit this post for some of comments requests and i am showing some post of showtopsnackbar and some error message that i am got.
I am hope this can become solution or reference to answer all of my error

As you can see in example page you need to pass Overlay like this:
showTopSnackBar(
Overlay.of(context)!,
CustomSnackBar.error(
message: text,
),
);
the package updated but they didn't update the readme page.

Related

Flutter: How to change state of the widget when a certain function has been called?

How it looks like
I've a got bluetooth in my flutter app based on flutter_blue package (^0.8.0). I can connect with my external device and exchange the data. To read data from bluetooth following function is being called when data arrived :
void parseBleMsg(List<int> data) {
print("Data1: $data");
/* Parse the income message */
msgID = data[0];
luxValue = data[1];
print("lux: $luxValue");
print("msgid: $msgID");
}
The parseBleMsg() callback is being set by using specific flutter_blue package methods like setNotifyValue() and .value.listen() :
late BluetoothCharacteristic colsRX;
void setNotifyRX() async {
await colsRX.setNotifyValue(true);
subscription = colsRX.value.listen(
(event) {
parseBleMsg(event);
},
);
}
In one of my app pages I have got a multiple number of buttons which are a custom statefull widgets :
class MeasurementPoint extends StatefulWidget {
final int id;
final double leftPos;
final double topPos;
const MeasurementPoint(
{required this.id, required this.leftPos, required this.topPos});
#override
State<MeasurementPoint> createState() => _MeasurementPointState();
}
class _MeasurementPointState extends State<MeasurementPoint> {
bool pointState = false;
#override
Widget build(BuildContext context) {
return Positioned(
left: widget.leftPos,
top: widget.topPos,
child: ClipOval(
child: Material(
color: pointState ? Colors.green : Styles.primaryColor,
child: InkWell(
onTap: () async {
Bluetooth().bleWrite();
lastClicked = widget.id;
/* TODO : AWAIT FOR BLUETOOTH RESPONSE AND THEN CHANGE STATE */
// setState(() {
// pointState = !pointState;
// });
},
child: const SizedBox(
width: 25, height: 25, child: Icon(Icons.sensors)),
),
),
),
);
}
}
What I want to achieve
As you can see, there is the "TODO" In onTap method. Inside onTap() method I want to send the data through bluetooth to my external device (it works fine) and after that I want to await for the response and then rebuild the widget, to simply change the color of the button as an indicator that the response frame has been received.
The problem I have is that I have no idea, how to await in onTap(), or how in other way rebuilt that widget with new color when I will receive the response from bluetooth.

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

Flutter: enable image zoom in/out on double tap using InteractiveViewer

I want to enable zoom in and out on double tap of the image, together with scaling in/out on pinch.
I saw some tutorials on YouTube where they implemented this feature using GestureDetector like this one but for some reason, it didn't work out for me.
In order to implement scaling in/out on pinch, I relied on this answer, and it really works well, but I also want to enable zoom in/out on double tapping the image. Looking up a way to do so on the internet, unfortunately, yielded nothing.
Is there any way to enable zoom in/out with both pinch and double tap using InteractiveViewer?
here is my code:
#override
Widget build(BuildContext context) {
return Center(
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(80),
panEnabled: false,
scaleEnabled: true,
minScale: 1.0,
maxScale: 2.2,
child: Image.network("https://pngimg.com/uploads/muffin/muffin_PNG123.png",
fit: BoxFit.fitWidth,
)
),
);
}
You can use a GestureDetector, that gives you the position of the click and with that you can zoom with the TransformationController at the click position:
final _transformationController = TransformationController();
TapDownDetails _doubleTapDetails;
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTapDown: _handleDoubleTapDown,
onDoubleTap: _handleDoubleTap,
child: Center(
child: InteractiveViewer(
transformationController: _transformationController,
/* ... */
),
),
);
}
void _handleDoubleTapDown(TapDownDetails details) {
_doubleTapDetails = details;
}
void _handleDoubleTap() {
if (_transformationController.value != Matrix4.identity()) {
_transformationController.value = Matrix4.identity();
} else {
final position = _doubleTapDetails.localPosition;
// For a 3x zoom
_transformationController.value = Matrix4.identity()
..translate(-position.dx * 2, -position.dy * 2)
..scale(3.0);
// Fox a 2x zoom
// ..translate(-position.dx, -position.dy)
// ..scale(2.0);
}
}
To animate the transition on double tap, you have to create an explicit animation on top of Till's code.
class _WidgetState extends State<Widget> with SingleTickerProviderStateMixin {
.
.
.
AnimationController _animationController;
Animation<Matrix4> _animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 400),
)..addListener(() {
_transformationController.value = _animation.value;
});
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
.
.
.
void _handleDoubleTap() {
Matrix4 _endMatrix;
Offset _position = _doubleTapDetails.localPosition;
if (_transformationController.value != Matrix4.identity()) {
_endMatrix = Matrix4.identity();
} else {
_endMatrix = Matrix4.identity()
..translate(-_position.dx * 2, -_position.dy * 2)
..scale(3.0);
}
_animation = Matrix4Tween(
begin: _transformationController.value,
end: _endMatrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController.forward(from: 0);
}
.
.
.
}
Here's a full, portable solution with included customizable animation:
class DoubleTappableInteractiveViewer extends StatefulWidget {
final double scale;
final Duration scaleDuration;
final Curve curve;
final Widget child;
const DoubleTappableInteractiveViewer({
super.key,
this.scale = 2,
this.curve = Curves.fastLinearToSlowEaseIn,
required this.scaleDuration,
required this.child,
});
#override
State<DoubleTappableInteractiveViewer> createState() => _DoubleTappableInteractiveViewerState();
}
class _DoubleTappableInteractiveViewerState extends State<DoubleTappableInteractiveViewer>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
Animation<Matrix4>? _zoomAnimation;
late TransformationController _transformationController;
TapDownDetails? _doubleTapDetails;
#override
void initState() {
super.initState();
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: widget.scaleDuration,
)..addListener(() {
_transformationController.value = _zoomAnimation!.value;
});
}
#override
void dispose() {
_transformationController.dispose();
_animationController.dispose();
super.dispose();
}
void _handleDoubleTapDown(TapDownDetails details) {
_doubleTapDetails = details;
}
void _handleDoubleTap() {
final newValue =
_transformationController.value.isIdentity() ?
_applyZoom() : _revertZoom();
_zoomAnimation = Matrix4Tween(
begin: _transformationController.value,
end: newValue,
).animate(
CurveTween(curve: widget.curve)
.animate(_animationController)
);
_animationController.forward(from: 0);
}
Matrix4 _applyZoom() {
final tapPosition = _doubleTapDetails!.localPosition;
final translationCorrection = widget.scale - 1;
final zoomed = Matrix4.identity()
..translate(
-tapPosition.dx * translationCorrection,
-tapPosition.dy * translationCorrection,
)
..scale(widget.scale);
return zoomed;
}
Matrix4 _revertZoom() => Matrix4.identity();
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTapDown: _handleDoubleTapDown,
onDoubleTap: _handleDoubleTap,
child: InteractiveViewer(
transformationController: _transformationController,
child: widget.child,
),
);
}
}
Example usage:
DoubleTappableInteractiveViewer(
scaleDuration: const Duration(milliseconds: 600),
child: Image.network(imageUrl),
),
Play around with it on dartpad.

How to run animation after specified duration in proper way?

Let's say I have 3 shapes in Stack widget which needs to be moved from point A to point B. I would like to start these 3 animations after specified delay 0ms 1000ms 2000ms .. . So for that I have 3 separated AnimationController objects but I don't see constructor parameter like delay:. I tried to run forward method 3 times in loop using
int delay = 0;
for (final AnimationController currentController in controllers) {
Future.delayed(Duration(milliseconds: delay), () {
currentController.forward(from: value);
});
delay += 1000;
}
or
await Future.delayed(Duration(milliseconds: delay));
currentController.forward(from: value);
or using Timer class instead of Future but it doesn't work properly. In foreground its working good but when I move application to background and go back to foreground the gap between each shape disappearing and they are in the same position sticked together and moving like one shape.
You can make a stateful widget like below. Change the animation according to your needs.
class SlideUpWithFadeIn extends StatefulWidget {
final Widget child;
final int delay;
final Curve curve;
SlideUpWithFadeIn({#required this.child, #required this.curve, this.delay});
#override
_SlideUpWithFadeInState createState() => _SlideUpWithFadeInState();
}
class _SlideUpWithFadeInState extends State<SlideUpWithFadeIn>
with TickerProviderStateMixin {
AnimationController _animController;
Animation<Offset> _animOffset;
#override
void initState() {
super.initState();
_animController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1250));
final curve =
CurvedAnimation(curve: widget.curve, parent: _animController);
_animOffset =
Tween<Offset>(begin: const Offset(0.0, 0.75), end: Offset.zero)
.animate(curve);
if (widget.delay == null) {
_animController.forward();
} else {
Timer(Duration(milliseconds: widget.delay), () {
_animController.forward();
});
}
}
#override
void dispose() {
_animController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
child: SlideTransition(
position: _animOffset,
child: widget.child,
),
opacity: _animController,
);
}
}
And use it like
SlideUpWithFadeIn(
child: ...,
delay: 0,
curve: ...,
),
SlideUpWithFadeIn(
child: ...,
delay: 1000,
curve: ...,
),
SlideUpWithFadeIn(
child: ...,
delay: 2000,
curve: ...,
),

Flutter Execute Method so long the button pressed

I want to execute a method while a user is pressing down on a button. In pseudocode:
while (button.isPressed) {
executeCallback();
}
In other words, the executeCallback method should fire repeatedly as long as the user is pressing down on the button, and stop firing when the button is released. How can I achieve this in Flutter?
Use a Listener and a stateful widget. I also introduced a slight delay after every loop:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(brightness: Brightness.dark),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool _buttonPressed = false;
bool _loopActive = false;
void _increaseCounterWhilePressed() async {
// make sure that only one loop is active
if (_loopActive) return;
_loopActive = true;
while (_buttonPressed) {
// do your thing
setState(() {
_counter++;
});
// wait a bit
await Future.delayed(Duration(milliseconds: 200));
}
_loopActive = false;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Listener(
onPointerDown: (details) {
_buttonPressed = true;
_increaseCounterWhilePressed();
},
onPointerUp: (details) {
_buttonPressed = false;
},
child: Container(
decoration: BoxDecoration(color: Colors.orange, border: Border.all()),
padding: EdgeInsets.all(16.0),
child: Text('Value: $_counter'),
),
),
),
);
}
}
A simpler way, without the listener, is as follows:
GestureDetector(
child: InkWell(
child: Icon(Icons.skip_previous_rounded),
onTap: widget.onPrevious,
),
onLongPressStart: (_) async {
isPressed = true;
do {
print('long pressing'); // for testing
await Future.delayed(Duration(seconds: 1));
} while (isPressed);
},
onLongPressEnd: (_) => setState(() => isPressed = false),
);
}
Building on the solution from ThinkDigital, my observation is that InkWell contains all the events necessary to do this without an extra GestureDetector (I find that the GestureDetector interferes with the ink animation on long press). Here's a control I implemented for a pet project that fires its event with a decreasing delay when held (this is a rounded button with an icon, but anything using InkWell will do):
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay,
"The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
#override
Widget build(BuildContext context) {
var shape = CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color:
Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step =
(widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (_holding) {
widget.onUpdate();
await Future.delayed(Duration(milliseconds: delay.round()));
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
Here it is in action:
To improve Elte Hupkes's solution, I fixed an issue where the number of clicks and the number of calls to the onUpdate callback did not match when tapping consecutively.
_tapDownCount variable is additionally used.
import 'package:flutter/material.dart';
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay, "The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
int _tapDownCount = 0;
#override
Widget build(BuildContext context) {
var shape = const CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color: Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
widget.onUpdate();
_tapDownCount += 1;
final int myCount = _tapDownCount;
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step = (widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (true) {
await Future.delayed(Duration(milliseconds: delay.round()));
if (_holding && myCount == _tapDownCount) {
widget.onUpdate();
} else {
return;
}
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
USING SETSTATE
You can achieve this by simply "onLongPressStart" and "onLongPressEnd" properties of a button.
In case you can't find "onLongPressStart" / "onLongPressEnd" properties in your widget, wrap your widget with the "GestureDetector" widget.
GestureDetector(
child: ..,
onLongPressStart: (_) async {
isTap = true;
do {
await Future.delayed(Duration(seconds: 1));
} while (isTap );
},
onLongPressEnd: (_) => setState(() => isTap = false),
);
}

Categories

Resources