Left and right to cancel or confirm slider flutter - flutter

I need to use a slider which slides both to the left to cancel and right to confirm
this is the desired slider
I couldn't find a way to do it, is there any way to do it ?

You can achieve it by using a Slider and customizing it.
...
double _currentSliderValue = 5;
Slider customSlider() {
return Slider(
value: _currentSliderValue,
min: 0,
max: 10,
divisions: 10,
onChanged: (double value) {
setState(() {
_currentSliderValue = value;
});
if (_currentSliderValue == 0) // Decline
else if (_currentSliderValue == 10) // Accept
else // Nothing
},
);
}
The UI can be achieved by including the customSlider() as a child of a Row widget as follows (didn't try it but it should be the right path):
Row declineOrAcceptSlider() {
return Row(children: [
Text("Decline"),
customSlider(),
Text("Accept")
], mainAxisAlignment: MainAxisAlignment.spacedEvenly);
}

Use Gesture Detector this
Example :
#override
Widget build(BuildContext context) {
String swipeDirection;
return GestureDetector(
onPanUpdate: (details) {
swipeDirection = details.delta.dx < 0 ? 'left' : 'right';
},
onPanEnd: (details) {
if (swipeDirection == 'left') {
//handle swipe left event
}
if (swipeDirection == 'right') {
//handle swipe right event
}
},
child: //child widget
);
}

Related

Prevent Small Hops in Drag Upon Touch Up/End in Flutter

I have built a custom slider and have been using GestureDetector with onHorizontalDragUpdate to report drag changes, update the UI and value.
However, when a user lifts their finger, there can sometimes be a small, unintentional hop/drag, enough to adjust the value on the slider and reduce accuracy. How can I stop this occuring?
I have considered adding a small delay to prevent updates if the drag hasn't moved for a tiny period and assessing the primaryDelta, but unsure if this would be fit for purpose or of there is a more routine common practive to prevent this.
--
Example of existing drag logic I am using. The initial drag data is from onHorizontalDragUpdate in _buildThumb. When the slider is rebuilt, the track size and thumb position is calculated in the LayoutBuilder and then the value is calculated based on the thumb position.
double valueForPosition({required double min, required double max}) {
double posIncrements = ((max) / (_divisions));
double posIncrement = (_thumbPosX / (posIncrements));
double incrementVal =
(increment) * (posIncrement + widget.minimumValue).round() +
(widget.minimumValue - widget.minimumValue.truncate());
return incrementVal.clamp(widget.minimumValue, widget.maximumValue);
}
double thumbPositionForValue({required double min, required double max}) {
return (max / (widget.maximumValue - widget.minimumValue - 1)) *
(value - widget.minimumValue - 1);
}
double trackWidthForValue({
required double min,
required double max,
required double thumbPosition,
}) {
return (thumbPosition + (_thumbTouchZoneWidth / 2))
.clamp(min, max)
.toDouble();
}
bool isDragging = false;
bool isSnapping = false;
Widget _buildSlider() {
return SizedBox(
height: _contentHeight,
child: LayoutBuilder(
builder: (context, constraints) {
double minThumbPosX = -(_thumbTouchZoneWidth - _thumbWidth) / 2;
double maxThumbPosX =
constraints.maxWidth - (_thumbTouchZoneWidth / 2);
if (isDragging) {
_thumbPosX = _thumbPosX.clamp(minThumbPosX, maxThumbPosX);
value = valueForPosition(min: minThumbPosX, max: maxThumbPosX);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
widget.onChanged(value);
});
} else {
_thumbPosX = thumbPositionForValue(
min: minThumbPosX,
max: maxThumbPosX,
);
}
double minTrackWidth = 0;
double maxTrackWidth = constraints.maxWidth;
double trackWidth = 0;
if (isDragging) {
trackWidth = (_thumbPosX + (_thumbTouchZoneWidth / 2))
.clamp(_thumbWidth, constraints.maxWidth);
} else {
trackWidth = trackWidthForValue(
min: minTrackWidth,
max: maxTrackWidth,
thumbPosition: _thumbPosX,
);
}
return Stack(
alignment: Alignment.centerLeft,
clipBehavior: Clip.none,
children: [
_buildLabels(),
_buildInactiveTrack(),
Positioned(
width: trackWidth,
child: _buildActiveTrack(),
),
Positioned(
left: _thumbPosX,
child: _buildThumb(),
),
],
);
},
),
);
}
Widget _buildThumb() {
return GestureDetector(
behavior: HitTestBehavior.opaque,
dragStartBehavior: DragStartBehavior.down,
onHorizontalDragUpdate: (details) {
setState(() {
_thumbPosX += details.delta.dx;
isDragging = true;
});
},
child: // Thumb UI
);
}
Updated: I make a little adjustment by adding a delay state and lastChangedTime.
If the user stops dragging for a short period (3 sec), the slider will be locked until the next new value is updated + a short delay (1.5 sec)
I follow your train of thought and make a simple example from Slider widget.
Is the result act like your expected? (You can adjust the Duration to any number)
DartPad: https://dartpad.dev/?id=95f2bd6d004604b3c37f27dd2852cb31
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
double _currentSliderValue = 20;
DateTime lastChangedTime = DateTime.now();
bool isDalying = false;
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(_currentSliderValue.toString()),
const SizedBox(height: 30),
Slider(
value: _currentSliderValue,
max: 100,
label: _currentSliderValue.round().toString(),
onChanged: (double value) async {
if (isDalying) {
await Future.delayed(
Duration(milliseconds: 1500),
() => isDalying = false,
);
} else {
if (DateTime.now().difference(lastChangedTime) >
Duration(seconds: 3)) {
isDalying = true;
} else {
setState(() {
_currentSliderValue = value;
});
}
}
lastChangedTime = DateTime.now();
},
),
],
);
}
}

Why the provider sometimes does not work?

The provider has a very strange behavior, when a product is added, the isEmpty property changes, but the provider is not called, and when the product is removed, the provider is called, what is the reason for this behavior.
There is a button with a price, when pressed noInCart, the button adds a product and the text on the button changes, if there is a product, then the button has two zones inCart, the left zone deletes the product and the right one adds more, if click on the left, the button changes as needed.
class AllGoodsViewModel extends ChangeNotifier {
var _isEmpty = true;
bool get isEmpty => _isEmpty;
void detailSetting(Goods item) {
final ind = cart.value.indexWhere((element) => element.id == item.id);
if (ind != -1) {
changeButtonState(false);
} else {
changeButtonState(true);
}
}
void changeButtonState(bool state) {
_isEmpty = state;
notifyListeners();
}
}
// adds and reduces a product
void haveItem({required Goods item, required int operation}) async {
final ind = cart.value.indexWhere((element) => element.id == item.id);
if (ind == -1) {
final minCount = item.optState == 0 ? 1 : item.opt!.count;
if (item.count < minCount) {
//order.shake();
} else {
changeButtonState(false); --------- cart is not empty, not working
cart.value.add(item);
final ind = cart.value.length - 1;
cart.value.last.isOpt = item.optState == 0 ? false : true;
cart.value.last.orderCount = minCount;
cart.value = List.from(cart.value);
await SQFliteService.cart.addToCart(cart.value.last);
changeCountInCart(operation);
}
} else {
final count = cart.value[ind].orderCount;
if (count <= item.count) {} else { return; } //order.shake()
if (operation < 0 || count + operation <= item.count) {} else { return; } //order.shake()
changeButtonState(false); --------- cart is not empty, not working
cart.value[ind].orderCount += operation;
cart.value = List.from(cart.value);
await SQFliteService.cart.updateItem(cart.value[ind].id, {"orderCount":cart.value[ind].orderCount});
changeCountInCart(operation);
}
}
class _DetailGoodsPageState extends State<DetailGoodsPage> {
GlobalKey _key = GlobalKey();
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){
Provider.of<AllGoodsViewModel>(context, listen: false).detailSetting(widget.item);
});
}
#override
Widget build(BuildContext context) {
final model = Provider.of<AllGoodsViewModel>(context, listen: false);
Widget inCart(){
return GestureDetector(
onPanDown: (details) {
Goods? item = widget.item;
RenderBox _cardBox = _key.currentContext!.findRenderObject() as RenderBox;
final localPosition = details.localPosition;
final localDx = localPosition.dx;
if (localDx <= _cardBox.size.width/2) {
Goods value = cart.value.firstWhere((element) => element.id == item.id);
if (item.optState == 0 ? value.orderCount <= 1 : value.orderCount <= value.opt!.count) {
setState(() {
final ind = cart.value.indexWhere((element) => element.id == item.id);
if (ind != -1) {
model.changeButtonState(true); ------ cart is empty it works
cart.value[ind].orderCount = 0;
SQFliteService.cart.delete(cart.value[ind].id);
cart.value = List.from(cart.value)..removeAt(ind);
}
});
} else {
model.haveItem(item: item, operation: item.optState == 0 ? -1 : (-1 * value.opt!.count));
}
} else {
model.haveItem(item: item, operation: item.optState == 0 ? 1 : item.count);
}
},
child: ...
);
}
Widget noInCart(){
return Container(
width: size.width - 16.w,
margin: EdgeInsets.symmetric(vertical: 10.h),
key: _key,
child: TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Design.appColor),
padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.w)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.h),
))
),
onPressed: (){
Goods? item = widget.item;
model.haveItem(item: item, operation: item.optState == 0 ? 1 : item.count);
},
child: ...
),
);
}
return ScreenUtilInitService().build((context) => Scaffold(
backgroundColor: Colors.white,
body: Container(
height: 64.h,
color: Colors.white,
child: model.isEmpty ? noInCart() : inCart()
)
in order to listen to updates you must have consumers
notifylistners function orders consumers to rebuild with the new data
wrap your widget with a consumer
Consumer<yourproviderclass>(
builder: (context, yourproviderclassinstance, child) => widget,
),

Do we have onTapDown and Drag functionality in flutter?

I have a simple usecase which is some how super tricky for a beginner in flutter.
I need these values returned for the scenario explained below
There are 2 containers in a row (green and orange)
OnTapDown on green container it should return ‘Green’ (this is straight forward and done)
Without lifting the finger off the screen, I drag my finger over the Orange container and I need that to return ‘Orange’
How do I solve this?
One solution could be to wrap your layout with GestureDetector and "guess" the position of your elements to then know where the drag ends.
EDIT: Adding a real check on the target position to make it more robust thanks to #GoodSp33d comment:
class DragView extends StatefulWidget {
const DragView({Key? key}) : super(key: key);
#override
_DragViewState createState() => _DragViewState();
}
GlobalKey orangeContainerKey = GlobalKey();
GlobalKey greenContainerKey = GlobalKey();
class _DragViewState extends State<DragView> {
Rect? getGlobalPaintBounds(GlobalKey element) {
final renderObject = element.currentContext!.findRenderObject();
var translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
return renderObject?.paintBounds
.shift(Offset(translation.x, translation.y));
} else {
return null;
}
}
bool isInRect(double x, double y, Rect? rect) {
if (rect != null)
return x >= rect.left &&
x <= rect.right &&
y <= rect.bottom &&
y >= rect.top;
return false;
}
#override
Widget build(BuildContext context) {
double _cursorX = 0;
double _cursorY = 0;
return GestureDetector(
onHorizontalDragUpdate: (details) {
_cursorX = details.globalPosition.dx;
_cursorY = details.globalPosition.dy;
},
onHorizontalDragEnd: (details) {
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(orangeContainerKey)))
print("Orange");
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(greenContainerKey)))
print("Green");
},
child: Row(
children: [
Expanded(
child: Container(key: greenContainerKey, color: Colors.green),
),
Expanded(
child: Container(key: orangeContainerKey, color: Colors.orange),
),
],
),
);
}
}
Second edit moving the detection to the onDragUpdate and checks to make it happens only on rect changes:
GlobalKey? currentObject;
onHorizontalDragUpdate: (details) {
_cursorX = details.globalPosition.dx;
_cursorY = details.globalPosition.dy;
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(orangeContainerKey))) {
if (currentObject == null || currentObject != orangeContainerKey) {
print("Orange");
currentObject = orangeContainerKey;
}
}
if (isInRect(_cursorX, _cursorY,
getGlobalPaintBounds(greenContainerKey))) if (currentObject ==
null ||
currentObject != greenContainerKey) {
print("Green");
currentObject = greenContainerKey;
}
},

Let user settle picking value from CupertinoPicker(onSelectedItemChanged ), after that it should send call to API

I am using CupertinoWidget for iOS users to scroll through List and check the price of a currency. But when scroll happens, onSelectedItemChanged sends callback to API for every value from the list. I read the document but unable to understand what to do. It pleasing if there is an example.
In document it's mentioned as CupertinoPicker > onSelectedItemChanged property
This can be called during scrolls and during ballistic flings. To get the value only when the scrolling settles, use a NotificationListener, listen for ScrollEndNotification and read its FixedExtentMetrics.
NotificationListener cupertinoPickerList() {
List<Text> textWidgetList = [];
for (String curreny in currenciesList) {
textWidgetList.add(
Text(
curreny,
style: TextStyle(
color: Colors.white,
),
),
);
}
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
return true;
} else {
return false;
}
},
child: CupertinoPicker(
itemExtent: 30,
scrollController: FixedExtentScrollController(initialItem: 19),
onSelectedItemChanged: (selectedIndex) {
selectedCurreny = currenciesList[selectedIndex];
updateUI(selectedCurreny);
print(selectedCurreny);
},
children: textWidgetList,
),
);
}
You can check if the metrics of the scrollNotification are of type FixedExtentMetrics. This type has the value itemIndex which you can use to determine which item is currently selected.
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification &&
scrollNotification.metrics is FixedExtentMetrics) {
(scrollNotification.metrics as FixedExtentMetrics).itemIndex; // Index of the list
return true;
} else {
return false;
}
},
With the help of Juliantje15's code, here is a full solution:
final widget = NotificationListener<ScrollEndNotification>(
onNotification: (notification) {
if (notification.metrics is! FixedExtentMetrics) {
return false;
}
final index = (notification.metrics as FixedExtentMetrics).itemIndex;
// This would be your callback function. Could use items[index]
// or something if that's more appropriate of course.
onItemChanged(index);
// False allows the event to bubble up further
return false;
},
child: CupertinoPicker(
itemExtent: 32,
onSelectedItemChanged: null, // Attribute is marked required
children: [Text('item1'), Text('item2'), Text('etc')],
),
);
It seems quite sensible to want this, so I guess it's a bit strange that this isn't included as an (optional) default behavior. I guess you could turn the wrapper into a custom widget if it's needed more often.

DragTarget widget is not responding

I am coding a chess game in flutter.
and this is the relevant bits of my code :
class Rank extends StatelessWidget {
final _number;
Rank(this._number);
#override
Widget build(BuildContext context) {
var widgets = <Widget>[];
for (var j = 'a'.codeUnitAt(0); j <= 'h'.codeUnitAt(0); j++) {
widgets
.add(
DroppableBoardSquare(String.fromCharCode(j) + this._number.toString())
);
//
}
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgets);
}
}
class DroppableBoardSquare extends StatelessWidget {
final String _coordinate;
const DroppableBoardSquare(this._coordinate) ;
#override
Widget build(BuildContext context) {
return DragTarget(
builder:(BuildContext context, List candidate, List rejectedData){
return BoardSquare(_coordinate);
},
onAccept: (data ) {
print('Accepted');
},
onWillAccept: (data){
return true;
},
onLeave: (data) => print("leave"),);
}
}
class BoardSquare extends StatelessWidget {
final String _coordinate;
BoardSquare(this._coordinate);
#override
Widget build(BuildContext context) {
ChessBloc bloc = ChessBlocProvider.of(context);
return
StreamBuilder<chess.Chess>(
stream: bloc.chessState,
builder: (context, AsyncSnapshot<chess.Chess> chess) {
return DraggablePieceWidget(chess.data.get(_coordinate), _coordinate);
});
}
}
class DraggablePieceWidget extends StatelessWidget {
final chess.Piece _piece;
final String _coordinate;
DraggablePieceWidget(this._piece, String this._coordinate);
#override
Widget build(BuildContext context) {
return Draggable(
child: PieceWidget(_piece),
feedback: PieceWidget(_piece),
childWhenDragging: PieceWidget(null),
data: {"piece": _piece, "origin": _coordinate} ,
);
}
}
Now the problem is that I can drag the piece fine, but cannot drop them. None of the methods on DragTarget is getting called.
what I am doing wrong?
I developed a drag-n-drop photos grid, where you can drag photos to reorder them based on numeric indexes.
Essentially, I assume, it is the same thing as the chessboard concept you have.
The problem possibly occurs due to Draggable (DraggablePieceWidget) element being inside of DragTarget (DroppableBoardSquare).
In my app I made it the other way around - I placed DragTarget into Draggable.
Providing some pseudo-code as an example:
int _dragSelectedIndex;
int _draggingIndex;
// Basically this is what you'd use to build every chess item
Draggable(
maxSimultaneousDrags: 1,
data: index,
onDragStarted: () { _draggingIndex = index; print("Debug: drag started"); }, // Use setState for _draggingIndex, _dragSelectedIndex.
onDragEnd: (details) { onDragEnded(); _draggingIndex = null; print("Debug: drag ended; $details"); },
onDraggableCanceled: (_, __) { onDragEnded(); _draggingIndex = null; print("Debug: drag cancelled."); },
feedback: Material(type: MaterialType.transparency, child: Opacity(opacity: 0.85, child: Transform.scale(scale: 1.1, child: createDraggableBlock(index, includeTarget: false)))),
child: createDraggableBlock(index, includeTarget: true),
);
// This func is used in 2 places - Draggable's `child` & `feedback` props.
// Creating dynamic widgets through functions is a bad practice, switch to StatefulWidget if you'd like.
Widget createDraggableBlock(int index, { bool includeTarget = true }) {
if (includeTarget) {
return DragTarget(builder: (context, candidateData, rejectedData) {
if (_draggingIndex == index || candidateData.length > 0) {
return Container(); // Display empty widget in the originally selected cell, and in any cell that we drag the chess over.
}
// Display a chess, but wrapped in DragTarget widget. All chessboard cells will be displayed this way, except for the one you start dragging.
return ChessPiece(..., index: index);
}, onWillAccept: (int elemIndex) {
if (index == _draggingIndex) {
return false; // Do not accept the chess being dragged into it's own widget
}
setState(() { _dragSelectedIndex = index; });
return true;
}, onLeave: (int elemIndex) {
setState(() { _dragSelectedIndex = null; });
});
}
// Display a chess without DragTarget wrapper, e.g. for the draggable(feedback) widget
return ChessPiece(..., index: index);
}
onDragEnded() {
// Check whether _draggingIndex & _dragSelectedIndex are not null and act accordingly.
}
I assume if you change index system to custom objects that you have - this would work for you too.
Please let me know if this helped.