Do we have onTapDown and Drag functionality in flutter? - 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;
}
},

Related

Flutter Slider how to Show Label instead of Values?

In Flutter, Slider, I want to show label instead of Values. The idea is for Search If I want to specify it at City level ( the lowest radius of search)
And next at County
Next at State level
Next at Country
and so on , last being at World Level ( the highest).
Currently Slider only displays numerical values.
Please use below code snippet
class DemoContent extends StatefulWidget {
const DemoContent({Key? key}) : super(key: key);
#override
State<DemoContent> createState() => _DemoContentState();
}
class _DemoContentState extends State<DemoContent> {
double _searchLevel = 0.0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Slider(
value: _searchLevel,
onChanged: (value) {
setState(() {
_searchLevel = value;
});
},
max: 4.0,
min: 0.0,
divisions: 4,
label: _getLabel(),
),
),
);
}
String _getLabel(){
String label = '';
if(_searchLevel == 0.0){
label = 'City';
}else if(_searchLevel == 1.0){
label = 'County';
}else if(_searchLevel == 2.0){
label = 'State';
} else if(_searchLevel == 3.0){
label = 'Country';
}else if(_searchLevel == 4.0){
label = 'World';
}
return label;
}
}

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,
),

Why "points.clear" and "points.add" (points is a list of Offset) do not work the same in 10 trial in CustomPaint in Flutter?

It works for some trials and then doesn't work for some trials and then works again (without any particular pattern). I mean for example it corrects the line and draw a perfect one for 3-4 times with only the first and last point and then for one time it leaves the line that the user drew. It seems that the points.clear or points.add sometimes work and sometimes doesn't. I know it sounds silly but I can not explain it in a logical way. I would be really thankful if someone could look at the code and tell me what's going on.
here is my GestureDetector:
body: GestureDetector(
onPanDown: (DragDownDetails details) {
setState(() {
points.clear();
});
},
onPanUpdate: (DragUpdateDetails details) {
//when the user touch the screen and move
setState(() {
RenderBox box = context.findRenderObject(); //finds the scaffold
Offset point = box.globalToLocal(details.globalPosition);
point = point.translate(0.0, -(AppBar().preferredSize.height + 30));
points = List.from(points)
..add(point); //add the points when user drag in screen
firstPoint = points.first; //storing the first point for drawing the line
});
},
onPanEnd: (DragEndDetails details) {
setState(() {
if (mode == "Line") {
Offset lastPoint = points.last; //storing the last point for drawing the line
points.clear();
points.add(firstPoint);
points.add(lastPoint);
points.add(null);
}
});
},
child: sketchArea,
),
and here is my CustomPainter class:
class Sketcher2 extends CustomPainter {
final List<Offset> points;
Sketcher2(this.points);
#override
bool shouldRepaint(Sketcher2 oldDelegate) {
return oldDelegate.points != points;
}
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black //seting the color of the drawing
..strokeCap = StrokeCap.round //the shape of a single dot (single touch)
..strokeWidth = 4.0; // the width of a single dot (single touch)
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);
}
}
}
}
You might have a problem with the shouldRepaint of your Sketcher2:
#override
bool shouldRepaint(Sketcher2 oldDelegate) {
return oldDelegate.points != points;
}
The != operator compares the objects. It will return true if and only if oldDelegate.points and points are not the same object.
In fact, I'm quite surprised it ever works. I tried your source code and it never works on my side.
1. Quick Solution
In onPanEnd, instead of clearing the points, reinitialize the list to a new []:
onPanEnd: (DragEndDetails details) {
setState(() {
if (mode == "Line") {
Offset lastPoint = points.last;
points = []; // instead of points.clear();
points.add(firstPoint);
points.add(lastPoint);
points.add(null);
} else if (mode == "FreeDraw") {
points.add(null);
}
});
},
BTW, you did it properly in onPanUpdate by using a copy of the List with points = List.from(points)..add(point);.
2. Further simplifications
You could simplify your GestureDetector quite a bit.
You firstPoint is points.first, no need to keep a separate variable for that;
Same for your lastPoints;
The details of onPanUpdate already give you the localPosition;
Why do you add null at the end of the points?
Your Page becomes:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Offset> points = <Offset>[];
String mode = 'Line';
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onPanDown: (DragDownDetails details) => setState(() => points = []),
onPanUpdate: (DragUpdateDetails details) =>
setState(() => points = [...points, details.localPosition]),
onPanEnd: (DragEndDetails details) => setState(() {
if (mode == "Line") points = [points.first, points.last];
}),
child: SketchArea(points: points),
),
);
}
}
I just changed this part:
points.add(firstPoint);
points.add(lastPoint);
to
points = List.from(points)..add(firstPoint);
points = List.from(points)..add(lastPoint);
and my problem solved.

Left and right to cancel or confirm slider 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
);
}

How to check if scroll position is at top or bottom in ListView?

I'm trying to implement a infinite scroll functionality.
I tried using a ListView inside on a NotificationListener to detect scroll events, but I can't see an event that says if the scroll has reached the bottom of the view.
Which would be the best way to achieve this?
There are generally two ways of doing it.
1. Using ScrollController
// Create a variable
final _controller = ScrollController();
#override
void initState() {
super.initState();
// Setup the listener.
_controller.addListener(() {
if (_controller.position.atEdge) {
bool isTop = _controller.position.pixels == 0;
if (isTop) {
print('At the top');
} else {
print('At the bottom');
}
}
});
}
Usage:
ListView(controller: _controller) // Assign the controller.
2. Using NotificationListener
NotificationListener<ScrollEndNotification>(
onNotification: (scrollEnd) {
final metrics = scrollEnd.metrics;
if (metrics.atEdge) {
bool isTop = metrics.pixels == 0;
if (isTop) {
print('At the top');
} else {
print('At the bottom');
}
}
return true;
},
child: ListView.builder(
physics: ClampingScrollPhysics(),
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
itemCount: 20,
),
)
You can use a ListView.builder to create a scrolling list with unlimited items. Your itemBuilder will be called as needed when new cells are revealed.
If you want to be notified about scroll events so you can load more data off the network, you can pass a controller argument and use addListener to attach a listener to the ScrollController. The position of the ScrollController can be used to determine whether the scrolling is close to the bottom.
_scrollController = new ScrollController();
_scrollController.addListener(
() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = 200.0; // or something else..
if ( maxScroll - currentScroll <= delta) { // whatever you determine here
//.. load more
}
}
);
Collin's should be accepted answer....
I would like to add example for answer provided by collin jackson. Refer following snippet
var _scrollController = ScrollController();
_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
// Perform your task
}
});
This will be only triggered when last item is visible in the list.
A more simpler aproach is like this:
NotificationListener<ScrollEndNotification>(
onNotification: onNotification,
child: <a ListView or Wrap or whatever widget you need>
)
and create a method to detect the position:
bool onNotification(ScrollEndNotification t) {
if (t.metrics.pixels >0 && t.metrics.atEdge) {
log('I am at the end');
} else {
log('I am at the start')
}
return true;
}
t.metrics.pixel is 0 when the user is with the scrol at the top, as is more then 0 when the sure scrools.
t.metrics.atEdge is true when the user is either at the top with the scrol or at the end with the scrol
the log method is from package import 'dart:developer';
I feel like this answer is a complement to Esteban's one (with extension methods and a throttle), but it's a valid answer too, so here it is:
Dart recently (not sure) got a nice feature, method extensions, which allow us to write the onBottomReach method like a part of the ScrollController:
import 'dart:async';
import 'package:flutter/material.dart';
extension BottomReachExtension on ScrollController {
void onBottomReach(VoidCallback callback,
{double sensitivity = 200.0, Duration throttleDuration}) {
final duration = throttleDuration ?? Duration(milliseconds: 200);
Timer timer;
addListener(() {
if (timer != null) {
return;
}
// I used the timer to destroy the timer
timer = Timer(duration, () => timer = null);
// see Esteban Díaz answer
final maxScroll = position.maxScrollExtent;
final currentScroll = position.pixels;
if (maxScroll - currentScroll <= sensitivity) {
callback();
}
});
}
}
Here's a usage example:
// if you're declaring the extension in another file, don't forget to import it here.
class Screen extends StatefulWidget {
Screen({Key key}) : super(key: key);
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
ScrollController_scrollController;
#override
void initState() {
super.initState();
_scrollController = ScrollController()
..onBottomReach(() {
// your code goes here
}, sensitivity: 200.0, throttleDuration: Duration(milliseconds: 500));
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Note: if you're using method extensions, you need to configure some things, see "How to enable Dart Extension Methods"
final ScrollController controller = ScrollController();
void _listener() {
double maxPosition = controller.position.maxScrollExtent;
double currentPosition = controller.position.pixels;
/// You can change this value . It's a default value for the
/// test if the difference between the great value and the current value is smaller
/// or equal
double difference = 10.0;
/// bottom position
if ( maxPosition - currentPosition <= difference )
/// top position
else
if(mounted)
setState(() {});
}
#override
void initState() {
super.initState();
controller.addListener(_listener);
}
I used different approach for infinite scrolling. I used ChangeNotifier class for variable change listener.
If there is change in variable It triggers the event and eventually hit the API.
class DashboardAPINotifier extends ChangeNotifier {
bool _isLoading = false;
get getIsLoading => _isLoading;
set setLoading(bool isLoading) => _isLoading = isLoading;
}
Initialize DashboardAPINotifier class.
#override
void initState() {
super.initState();
_dashboardAPINotifier = DashboardAPINotifier();
_hitDashboardAPI(); // init state
_dashboardAPINotifier.addListener(() {
if (_dashboardAPINotifier.getIsLoading) {
print("loading is true");
widget._page++; // For API page
_hitDashboardAPI(); //Hit API
} else {
print("loading is false");
}
});
}
Now the best part is when you have to hit the API.
If you are using SliverList, Then at what point you have to hit the API.
SliverList(delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
Widget listTile = Container();
if (index == widget._propertyList.length - 1 &&
widget._propertyList.length <widget._totalItemCount) {
listTile = _reachedEnd();
} else {
listTile = getItem(widget._propertyList[index]);
}
return listTile;
},
childCount: (widget._propertyList != null)? widget._propertyList.length: 0,
addRepaintBoundaries: true,
addAutomaticKeepAlives: true,
),
)
_reachEnd() method take care to hit the api. It trigger the `_dashboardAPINotifier._loading`
// Function that initiates a refresh and returns a CircularProgressIndicator - Call when list reaches its end
Widget _reachedEnd() {
if (widget._propertyList.length < widget._totalItemCount) {
_dashboardAPINotifier.setLoading = true;
_dashboardAPINotifier.notifyListeners();
return const Padding(
padding: const EdgeInsets.all(20.0),
child: const Center(
child: const CircularProgressIndicator(),
),
);
} else {
_dashboardAPINotifier.setLoading = false;
_dashboardAPINotifier.notifyListeners();
print("No more data found");
Utils.getInstance().showSnackBar(_globalKey, "No more data found");
}
}
Note: After your API response you need to notify the listener,
setState(() {
_dashboardAPINotifier.setLoading = false;
_dashboardAPINotifier.notifyListeners();
}
You can use the package scroll_edge_listener.
It comes with an offset and debounce time configuration which is quite useful. Wrap your scroll view with a ScrollEdgeListener and attach a listener. That's it.
ScrollEdgeListener(
edge: ScrollEdge.end,
edgeOffset: 400,
continuous: false,
debounce: const Duration(milliseconds: 500),
dispatch: true,
listener: () {
debugPrint('listener called');
},
child: ListView(
children: const [
Placeholder(),
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),
You can use any one of below conditions :
NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrices = notification.metrics;
if (metrices.atEdge && metrices.pixels == 0) {
//you are at top of list
}
if (metrices.pixels == metrices.minScrollExtent) {
//you are at top of list
}
if (metrices.atEdge && metrices.pixels > 0) {
//you are at end of list
}
if (metrices.pixels >= metrices.maxScrollExtent) {
//you are at end of list
}
return false;
},
child: ListView.builder());