GestureDetector
, which recognizes gestures such as tap, double tap, long press, pan, vertical drag, horizontal drag, and scale.Draggable
widget that is dragged to a DragTarget
.DragTarget
widget that receives data from a Draggable
.InkWell
and InkResponse
widgets. You will learn that InkWell
is a rectangular area that responds to touch and clips splashes within its area. You'll learn that InkResponse
responds to touch and that splashes expand outside its area.Dismissible
widget that is dismissed by dragging.In this chapter, you'll learn how to add interactivity to an app by using gestures. In a mobile application, gestures are the heart of listening to user interaction. Making use of gestures can define an app with a great UX. Overusing gestures when they don't add value or convey an action creates a poor UX. You'll take a closer look at how to find a balance by using the correct gesture for the task at hand.
The GestureDetector
widget detects gestures such as tap, double tap, long press, pan, vertical drag, horizontal drag, and scale. It has an optional child
property, and if a child
widget is specified, the gestures apply only to the child
widget. If the child
widget is omitted, then the GestureDetector
fills the entire parent instead. If you need to catch vertical drag and horizontal drag at the same time, use the pan gesture. If you need to catch a single‐axis drag, then use either the vertical drag or horizontal drag gesture.
If you try to use vertical drag, horizontal drag, and pan gestures at the same time, you'll receive an Incorrect GestureDetector Arguments
error. However, if you use either vertical or horizontal drag with a pan gesture, you'll not receive any errors. The reason you receive the error is that simultaneously having a vertical and horizontal drag gesture and a pan gesture results in the pan gesture being ignored since the other two (vertical and horizontal drag) will first catch all of the drags (Figure 11.1).
You'll get the same kind of error if you try to use vertical drag, horizontal drag, and scale gestures at the same time. However, if you use either vertical or horizontal drag with a scale gesture, you won't receive any errors.
Each pan, vertical drag, horizontal drag, and scale property has a callback for each start, update, and end drag. (See Table 11.1.) Each callback has access to the details
object containing values about the gesture, which is rich in information and provides the touch position.
TABLE 11.1: GestureDetector Callbacks
PROPERTY/CALLBACK | DETAILS OBJECT FOR CALLBACK |
onPanStart |
DragStartDetails |
onVerticalDragStart |
DragStartDetails |
onHorizontalDragStart |
DragStartDetails |
onScaleStart |
ScaleStartDetails |
onPanUpdate |
DragUpdateDetails |
onVerticalDragUpdate |
DragUpdateDetails |
onHorizontalDragUpdate |
DragUpdateDetails |
onScaleUpdate |
ScaleUpdateDetails |
onPanEnd |
DragEndDetails |
onVerticalDragEnd |
DragEndDetails |
onHorizontalDragEnd |
DragEndDetails |
onScaleEnd |
ScaleEndDetails |
For example, to check whether a user dragged on the screen from either left or right, you use the onHorizontalDragEnd
callback that has access to the DragEndDetails details
object. You use the details.primaryVelocity
value to check whether it's negative, 'Dragged Right to Left'
, or if it's positive, 'Dragged Left to Right'
.
onHorizontalDragEnd: (DragEndDetails details) {
print('onHorizontalDragEnd: $details');
if (details.primaryVelocity < 0) {
print('Dragged Right to Left: ${details.primaryVelocity}');
} else if (details.primaryVelocity > 0) {
print('Dragged Left to Right: ${details.primaryVelocity}');
}
},
// print statement results
flutter: onHorizontalDragEnd: DragEndDetails(Velocity(-2313.4, -110.3))
flutter: Dragged Right to Left: -2313.4407865184226
flutter: onHorizontalDragEnd: DragEndDetails(Velocity(3561.4, 123.2))
flutter: Dragged Left to Right: 3561.4258553699615
The following are the GestureDetector
gestures that you can listen for and take appropriate action:
onTapDown
onTapUp
onTap
onTapCancel
onDoubleTap
onLongPress
onPanStart
onPanUpdate
onPanEnd
onVerticalDragStart
onVerticalDragUpdate
onVerticalDragEnd
onHorizontalDragStart
onHorizontalDragUpdate
onHorizontalDragEnd
onScaleStart
onScaleUpdate
onScaleEnd
In this section, you'll build the gesture area that catches drag events. To make the gesture area visible, you'll use a light green color and place an alarm clock icon for visual purposes only. In the next section, you'll add another area to this app to handle dragging capabilities.
In this example, the Column
widget will vertically display a GestureDetector
listening for the onTap
, onDoubleTap
, onLongPress
, and onPanUpdate
gestures. It will also display a Draggable
widget and a DragTarget
widget. The Draggable Icon
will pass a Color
to the DragTarget
displaying a Text
widget changing to the passed Color
. In this example, to keep the widget tree shallow, you'll use methods instead of widget classes.
ch11_gestures_drag_drop
. You can follow the instructions in Chapter 4, “Creating a Starter Project Template.” For this project, you need to create only the pages
folder.main.dart
file. Change the primarySwatch
property from blue
to lightGreen
. primarySwatch: Colors.lightGreen,
home.dart
file and add to the body a SafeArea
with a SingleChildScrollView
as a child
. The reason for using a SingleChildScrollView
is to handle device rotation and automatically be able to scroll to view hidden content. Add a Column
as a child
of the SingleChildScrollView
. For the Column children
list of Widget
, add a method call to _buildGestureDetector()
, _buildDraggable()
, and _buildDragTarget()
with Divider
widgets between them.Note that you will implement _buildDraggable()
and _buildDragTarget()
in the next section. You can comment these methods out if you would like to test the project with just the GestureDetector
.
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
_buildGestureDetector(),
Divider(
color: Colors.black,
height: 44.0,
),
_buildDraggable(),
Divider(
height: 40.0,
),
_buildDragTarget(),
Divider(
color: Colors.black,
),
],
),
),
),
_buildGestureDetector() GestureDetector
method after the Widget build(BuildContext context) {…}
.GestureDetector
listening to the onTap
, onDoubleTap
, onLongPress
, and onPanUpdate
gestures.Container
as a child
of the GestureDetector
. The Container child
is a Column
displaying an Icon
and a Text
widget showing the gesture detected and pointer location on the screen. I have also added the onVerticalDragUpdate
and onHorizontalDragUpdate
gestures (properties) but commented them out for you to experiment.Remember when using the Pan
gesture that you can listen to only onHorizontalDragUpdate
or onVerticalDragUpdate
, not both, or you will receive an error (refer to Figure 11.1).
_displayGestureDetected(String gesture)
method. Each gesture passes the String
representation of the gesture. The onPanUpdate
, onVerticalDragUpdate
, and onHorizontalDragUpdate
gestures (properties) listen to DragUpdateDetails
.In this example, I'm using onPanUpdate
, but I have left the onVerticalDragUpdate
, onHorizontalDragUpdate
, and onHorizontalDragEnd
gestures (properties) commented out for you to experiment.
GestureDetector _buildGestureDetector() {
return GestureDetector(
onTap: () {
print('onTap');
_displayGestureDetected('onTap');
},
onDoubleTap: () {
print('onDoubleTap');
_displayGestureDetected('onDoubleTap');
},
onLongPress: () {
print('onLongPress');
_displayGestureDetected('onLongPress');
},
onPanUpdate: (DragUpdateDetails details) {
print('onPanUpdate: $details');
_displayGestureDetected('onPanUpdate: $details');
},
//onVerticalDragUpdate: ((DragUpdateDetails details) {
// print('onVerticalDragUpdate: $details');
// _displayGestureDetected('onVerticalDragUpdate: $details');
//}),
//onHorizontalDragUpdate: (DragUpdateDetails details) {
// print('onHorizontalDragUpdate: $details');
// _displayGestureDetected('onHorizontalDragUpdate: $details');
//},
//onHorizontalDragEnd: (DragEndDetails details) {
// print('onHorizontalDragEnd: $details');
// if (details.primaryVelocity < 0) {
// print('Dragging Right to Left: ${details.velocity}');
// } else if (details.primaryVelocity > 0) {
// print('Dragging Left to Right: ${details.velocity}');
// }
//},
child: Container(
color: Colors.lightGreen.shade100,
width: double.infinity,
padding: EdgeInsets.all(24.0),
child: Column(
children: <Widget>[
Icon(
Icons.access_alarm,
size: 98.0,
),
Text('$_gestureDetected'),
],
),
),
);
}
void _displayGestureDetected(String gesture) {
setState(() {
_gestureDetected = gesture;
});
}
The GestureDetector
listens to onTap
, onDoubleTap
, onLongPress
, and onPanUpdate
and for the app the optional onVerticalDragUpdate
and onHorizontalDragUpdate
gestures (properties). As the user taps and drags over the screen, the GestureDetector
updates where the pointer begins and ends, and while moving. To limit the area of detecting the gestures, you passed a Container
as a child
of the GestureDetector
.
To implement a drag‐and‐drop feature, you drag the Draggable
widget to a DragTarget
widget. You use the data
property to pass any custom data and use the child
property to display a widget like an Icon
and remains visible while not being dragged as long as the childWhenDragging
property is null
. Set the childWhenDragging
property to display a widget while dragging. Use the feedback
property to display a widget showing the user visual feedback
where the widget is being dragged. Once the user lifts their finger on top of the DragTarget
, the target can accept the data
. To reject accepting the data
, the user moves away from the DragTarget
without releasing touch. If you need to restrict the dragging vertically or horizontally, you optionally set the Draggable axis
property. To catch a single axis drag, you set the axis
property to either Axis.vertical
or Axis.horizontal
.
The DragTarget
widget listens for a Draggable
widget and receives data
if dropped. The DragTarget builder
property accepts three parameters: the BuildContext
, List<dynamic> acceptedData
(candidateData
), and List<dynamic>
of rejectedData
. The acceptedData
is the data
passed from the Draggable
widget, and it expects it to be a List
of values. The rejectedData
contains the List
of data
that will not be accepted.
In this section you're going to add to the previous app an additional drag area that catches drag events. You'll create two widgets: a palette Icon
widget that is draggable around the screen and a Text
widget that receives data by accepting a drag gesture. When the Text
widget receives data, it will change its text color from a light gray to a deep orange color but only if the drag is released on top of it.
To keep the example clean, the Draggable data
property passes the deep color orange as an integer value
. The DragTarget
accepting an integer value
checks whether a Draggable
is over it. If not, a default label shows the message “Drag To and see the color change.” If Draggable
is over it and data
has a value, you see a label with the data
passed. The ternary operator (conditional statement) is used to check whether the data
is passed.
Continuing with the previous gestures project, let's add the Draggable
and DragTarget
methods.
_buildDraggable()
method, which returns a Draggable integer
. The Draggable child
is a Column
with the children
list of Widget
consisting of an Icon
and a Text
widget. The feedback
property is an Icon
, and the data
property passes the Color
as an integer value
. The data
property can be any custom data
needed, but to keep it simple, you are passing the integer value
of the Color
.Draggable<int> _buildDraggable() {
return Draggable(
child: Column(
children: <Widget>[
Icon(
Icons.palette,
color: Colors.deepOrange,
size: 48.0,
),
Text(
'Drag Me below to change color',
),
],
),
childWhenDragging: Icon(
Icons.palette,
color: Colors.grey,
size: 48.0,
),
feedback: Icon(
Icons.brush,
color: Colors.deepOrange,
size: 80.0,
),
data: Colors.deepOrange.value,
);
}
_buildDragTarget()
method, which returns a DragTarget integer
. To accept data
, set the DragTarget onAccept
property value
to colorValue
and set the _paintedColor
variable to the Color(colorValue)
. The Color(colorValue)
constructs (converts) the integer value
to a color.builder
property to accept three parameters: BuildContext
, List<dynamic> acceptedData
, and List<dynamic>
of rejectedData
. Note that the data
is a List
, and to obtain the Color integer value
, you read the first‐row value by using acceptedData[0]
.Use the arrow syntax
with the ternary operator to check for acceptedData.isEmpty
. If it's empty, it means no dragging over the DragTarget
and shows a Text
widget instructing the user to “Drag To and see color change.” Otherwise, if there is dragging over, the DragTarget
shows the Text
widget displaying the 'Painting Color: $acceptedData
'. For the same Text
widget, set the color style
property to the Color(acceptedData[0])
giving the user visual feedback of the color.
DragTarget<int> _buildDragTarget() {
return DragTarget<int>(
onAccept: (colorValue) {
_paintedColor = Color(colorValue);
},
builder: (BuildContext context, List<dynamic> acceptedData, List<dynamic> rejectedData) => acceptedData.isEmpty
? Text(
'Drag To and see color change',
style: TextStyle(color: _paintedColor),
)
: Text(
'Painting Color: $acceptedData',
style: TextStyle(
color: Color(acceptedData[0]),
fontWeight: FontWeight.bold,
),
),
);
}
The DragTarget
widget listens for a Draggable
widget to drop. If the user releases over the DragTarget
, the onAccept
is called, as long as the data
is acceptable. The builder
property accepts three parameters: BuildContext
, List<dynamic> acceptedData
(candidateData
), and List<dynamic>
of rejectedData
. Using the arrow syntax
with a ternary operator, you check for acceptedData.isEmpty
. If it's empty, no data
is passed, and a Text
widget displays instructions. If data
is valid and accepted, a Text
widget is displayed with the data
value and uses the style
property to set the appropriate color.
Now you'll build upon what you learned in the “Setting Up GestureDetector: The Basics” section, and by taking a more in‐depth look, you'll learn how to scale widgets by using single or multitouch gestures. The goal is to learn how to implement multitouch scaling of an image by zooming in/out, double tap to increase zoom, and long press to reset the image to original size. The GestureDetector
gives you the ability to accomplish scaling by using onScaleStart
and onScaleUpdate
. Use onDoubleTap
to increase the zoom, and use onLongPress
to reset the zoom to the original default size.
When the user taps the image, the image can be dragged around to change position or scaled by zooming in/out. To accomplish both requirements, you'll use the Transform
widget. Use the Transform.scale
constructor to resize the image, and use the Transform.translate
constructor to move the image (Figure 11.2).
The Transform
widget applies a transformation before the child
is painted. Using the Transform default
constructor, the transform
argument is set by using the Matrix4
(4D Matrix) class, and this transformation matrix is applied to the child
during painting. The benefits of using the default constructor are to use the Matrix4
to execute multiple cascading (..scale()..translate()
) transformations. The double dots (..
) are used to cascade multiple transformations. In Chapter 3, “Learning Dart Basics,” you learned that the cascade notation allows you to make a sequence of operations on the same object. The following sample code shows how to use the Matrix4
class with the cascade notation to apply a scale
and a translate
transform to the same object:
Matrix4.identity()
..scale(1.0, 1.0)
..translate(30, 30);
The Transform
widget has four different constructors.
Transform
: Default constructor taking a Matrix4
for the transform argument.Transform.rotate
: Constructor to rotate a child
widget around the center by using an angle
. The angle
argument rotates clockwise by radians. To rotate counterclockwise, pass a negative radian.Transform.scale
: Constructor to evenly scale a child
widget on the x‐axis and y‐axis. The widget is scaled by its center alignment. The scale
argument value of 1.0 is the original widget size. Any values above 1.0 scale the widget larger, and values below 1.0 scale the widget smaller. A value of 0.0 makes the widget invisible.Transform.translate
: Constructor to move/position a child
widget by using a translation, an offset
. The offset
argument takes the Offset(double dx, double dy)
class by positioning the widget on the x‐axis and y‐axis.In this example, the app presents a page showing an image that's the full device width. Imagine if this is a journal app and the user navigated to this page to view the selected image with the ability to zoom for more details. The image is moved by a single touch drag and can be zoomed in/out (pinching) by using multitouch. Double tapping allows the image to zoom in at the tapped location, and a single long press resets the image location and zoom level back to default values.
This example is the first part of the app where you lay out the widgets that handle the gestures moving and scaling the image. In the next exercise, you'll concentrate on adding the logic to handle the calculations necessary to keep track of the location and scale of the image.
The GestureDetector
is the body
(property) base widget and is listening for the onScaleStart
, onScaleUpdate
, onDoubleTap
, and onLongPress
gestures (properties). The GestureDetector child
is a Stack
that shows the image and a gesture status bar display. To apply the moving and scaling of the image, use the Transform
widget. To show different ways to apply changes to a widget, you make use of three different Transform
constructors: default
, scale
, and translate
.
You'll take a look at two techniques to accomplish the same moving and scaling results. The first technique (beginning with step 13) involves nesting the scale
and translate
constructors. The second technique (starting at step 16) uses the default
constructor with the Matrix4
to apply transformations.
Note in this example to keep the widget tree shallow, you'll use methods instead of widget classes.
ch11_gestures_scale
. As usual, you can follow the instructions in Chapter 4. For this project, you need to create only the pages
and assets/images
folders.Home Class
as a StatefulWidget
since the data (state) requires changes.pubspec.yaml
file to add resources. Under the assets
section, add the assets/images/
folder.# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
flutter packages get
; once finished, it will show a message of Process finished with exit code 0
. If it does not automatically run the command for you, open the Terminal window (located at the bottom of your editor) and type flutter packages get
.assets
and subfolder images
at the project's root, and then copy the elephant.jpg
file to the images
folder.home.dart
file and add to the body
a call to the _buildBody(context)
method. The context
is received by the _buildBody()
method as a parameter for the method's MediaQuery
to obtain the device width
. body: _buildBody(context),
class _HomeState extends State<Home>
and above @override
, add the variables for _startLastOffset
, _lastOffset
, _currentOffset
, _lastScale
, and _currentScale
.The _startLastOffset
, _lastOffset
, and _currentOffset
variables are of type Offset
initialized to a value of Offset.zero
. The Offset.zero
is the same as Offset(0.0, 0.0)
, meaning the default position for the image. These variables are used to keep track of the image position while it's dragged.
The _lastScale
and _currentScale
variables are of type double
. They are initialized to a value of 1.0
, which is normal zoom size. These variables are used to keep track of the image while it's scaled. Values greater than 1.0
scale the image bigger, and values less than 1.0
scale the image smaller.
class _HomeState extends State<Home> {
Offset _startLastOffset = Offset.zero;
Offset _lastOffset = Offset.zero;
Offset _currentOffset = Offset.zero;
double _lastScale = 1.0;
double _currentScale = 1.0;
@override
Widget build(BuildContext context) {
_buildBody(BuildContext context) Widget
method after the Widget build(BuildContext context) {…}
. Return a GestureDetector
with the child
as a Stack
. Note that the GestureDetector
is at the root of the body
property to intercept all gestures anywhere on the screen.To show the image and the top gesture status bar display, you'll use the Stack
widget.
Stack fit
property to StackFit.expand
to expand to the biggest size allowed.Stack children
list of Widget
, add three methods and name them _transformScaleAndTranslate()
, _transformMatrix4()
, and _positionedStatusBar(context)
. The _positionedStatusBar
method passes the context
for the MediaQuery
to obtain the full width of the device._transformMatrix4()
method since you will be testing with the _transformScaleAndTranslate()
first.GestureDetector
the onScaleStart
, onScaleUpdate
, onDoubleTap
, and onLongPress
gestures (properties) to listen for each gesture. Respectively pass the _onScaleStart
, _onScaleUpdate
, _onDoubleTap
, and _onLongPress
methods.Widget _buildBody(BuildContext context) {
return GestureDetector(
child: Stack(
fit: StackFit.expand,
children: <Widget>[
_transformScaleAndTranslate(),
//_transformMatrix4(),
_positionedStatusBar(context),
],
),
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
onDoubleTap: _onDoubleTap,
onLongPress: _onLongPress,
);
}
scale
and translate
constructors. Add the _transformScaleAndTranslate() Transform
method after Widget build(BuildContext context) {…}
. Return a Transform
by nesting the scale
and translate
constructors.Transform.scale
constructor's scale
argument, enter the variable _currentScale
and set the child
argument to the Transform.translate
constructor.Transform.translate
constructor's offset
argument, enter the variable _current_offset
and set the child
argument to an Image
. This child
widget is the image that is dragged around and scaled by nesting the two Transform
widgets.
Transform _transformScaleAndTranslate() {
return Transform.scale(
scale: _currentScale,
child: Transform.translate(
offset: _currentOffset,
child: Image(
image: AssetImage('assets/images/elephant.jpg'),
),
),
);
}
default
constructor. Add the _transformMatrix4() Transform
method. Return a Transform
by using the default
constructor.Transform
constructor's transform
argument, use the Matrix4
. Using the Matrix4.identity()
creates the matrix from zero and sets default values. It is actually making a call to Matrix4.zero()..setIdentity()
. From the identity
constructor, use the double dots to cascade the scale and translate transformations. With this technique, there's no need to use multiple Transform
widgets—you use only one and execute multiple transformations.scale
method, pass the _currentScale
for both the x‐ and y‐axes. The x‐axis is mandatory, but the y‐axis is optional. Since the image is scaled proportionally, both the x‐axis and y‐
axis values are utilized.translate
method, pass the _currentOffset.dx
for the x‐axis and _currentOffset.dy
for the y‐axis. The x‐axis is mandatory, but the y‐axis is optional. In this app, the image is being dragged (moved) without restrictions, and both the x‐axis and y‐axis values are utilized.alignment
property to use the FractionalOffset.center
. If the center alignment is not used while the image scales, the translate _currentOffset
moves the image. By keeping the image in the same location during scaling, it creates a great UX. If the image is moving away from the current location during scaling, that would not be a good UX.Set the child
property to an Image
widget. The image
property uses the AssetImage elephant.jpg
.
Transform _transformMatrix4() {
return Transform(
transform: Matrix4.identity()
..scale(_currentScale, _currentScale)
..translate(_currentOffset.dx, _currentOffset.dy,),
alignment: FractionalOffset.center,
child: Image(
image: AssetImage('assets/images/elephant.jpg'),
),
);
}
_positionedStatusBar(BuildContext context) Positioned
method. Return a Positioned
by using the default
constructor. The purpose of this Positioned
widget is to show a gesture status bar display on the top of the screen with the current scale and position.top
property to 0.0
to position it on the top of the screen in the Stack
widget. Set the width
property by using the MediaQuery width
to expand the full width of the device. The child
is a Container
with the color
property set to a shade of Colors.white54
. Set the Container height
property to 50.0
. Set the Container child
to a Row
with the mainAxisAlignment
of MainAxisAlignment.spaceAround
. For the Row children
list of Widget
, use two Text
widgets. The first Text
widget shows the current scale by using the _currentScale
variable. To show only up to four decimal points precision, use _currentScale.toStringAsFixed(4)
. The second Text
widget shows the current location by using the _currentOffset
variable.Positioned _positionedStatusBar(BuildContext context) {
return Positioned(
top: 0.0,
width: MediaQuery.of(context).size.width,
child: Container(
color: Colors.white54,
height: 50.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text(
'Scale: ${_currentScale.toStringAsFixed(4)}',
),
Text(
'Current: $_currentOffset',
),
],
),
),
);
}
The GestureDetector
listens to the onScaleStart
, onScaleUpdate
, onDoubleTap
, and onLongPress
gestures (properties). As the user taps and drags over the screen, the GestureDetector
updates where the pointer begins and ends, as well as updating its location while moving. To maximize the gesture detecting area, the GestureDetector
fills the entire screen by setting the child
Stack widget fit
property to StackFit.expand
to expand to the biggest size allowed. Note that the GestureDetector
fills the entire screen only if no child
widget is used.
The _startLastOffset
, _lastOffset
, and _currentOffset
variables are of type Offset
and are used to track the image position. The _lastScale
and _currentScale
variables are of type double
and are used to track the image scale.
Using the Stack
widget allows you to place the Image
and a Positioned
widget to overlap each other. The Positioned
widget is placed as the last widget in the Stack
to allow the gesture status bar display to stay over the Image
.
The Transform
widget is used to move and scale the Image
by implementing two different techniques. The first technique makes use of nesting the Transform.scale
and Transform.translate
constructors. The second technique uses the Transform default
constructor by using the Matrix4
to apply transformations. Both methods accomplish the same results.
Continue with the previous moving and scaling project and start adding the logic and calculations to handle the GestureDetector
gestures. The onScaleStart
and onScaleUpdate
are responsible for handling moving and scaling gestures. The onDoubleTap
is responsible for handling the double tapping gesture to increase the zoom. The onLongPress
is responsible for handling a long press gesture to reset the zoom to the original default value.
_onScaleStart(ScaleStartDetails details)
method. This gesture is called once when the user starts to move or scale the image. This method populates the _startLastOffset
, _lastOffset
, and _lastScale
variables, and the image position and scale calculations are based on these values.void _onScaleStart(ScaleStartDetails details) {
print('ScaleStartDetails: $details');
_startLastOffset = details.focalPoint;
_lastOffset = _currentOffset;
_lastScale = _currentScale;
}
_onScaleUpdate(ScaleUpdateDetails details)
method. This gesture is called when the user is either moving or scaling the image.By using the details
(ScaleUpdateDetails
from callback) object, different values such as the scale
, rotation
, and focalPoint
(Offset
of the contact position on the device screen) can be checked. The goal is to check whether the user is either moving or scaling the image by checking the details.scale
value.
void _onScaleUpdate(ScaleUpdateDetails details) {
print('ScaleUpdateDetails: $details - Scale: ${details.scale}');
}
if
statement to check whether the user is scaling the image by evaluating details.scale != 1.0
. If the details.scale
is greater or less than 1.0
, it means the image is scaling. With values greater than 1.0
, the image is scaling larger, and with values less than 1.0
, the image is scaling smaller.void _onScaleUpdate(ScaleUpdateDetails details) {
print('ScaleUpdateDetails: $details - Scale: ${details.scale}');
if (details.scale != 1.0) {
// Scaling
}
}
currentScale
of type double
and calculate the value by taking the _lastScale * details.scale
. The _lastScale
was previously calculated from the _onScaleStart()
method, and the details.scale
is the current scale as the user continues to zoom the image. double currentScale = _lastScale * details.scale;
currentScale
is less than 0.5
(half the original size) and reset the value to 0.5
.if (currentScale < 0.5) {
currentScale = 0.5;
}
Image
widget refresh the current zoom, add the setState()
method and populate the _currentScale
value with the local currentScale
variable. Remember that the setState()
method tells the Flutter framework that the widget should redraw because the state has changed. It is recommended to place calculations that do not need state changes outside the setState()
method. This best practice is why you created a local variable called currentScale
.setState(() {
_currentScale = currentScale;
});
The following is the current _onScaleUpdate()
method:
void _onScaleUpdate(ScaleUpdateDetails details) {
print('ScaleUpdateDetails: $details - Scale: ${details.scale}');
if (details.scale != 1.0) {
// Scaling
double currentScale = _lastScale * details.scale;
if (currentScale < 0.5) {
currentScale = 0.5;
}
setState(() {
_currentScale = currentScale;
});
print('_scale: $_currentScale - _lastScale: $_lastScale');
}
}
_onScaleUpdate()
method the logic to move the image around the screen.else if
statement to check whether the details.scale
is equal to 1.0
. If the scaling is 1.0
, it means the image is moving, not scaling. } else if (details.scale == 1.0) {
Before the image can be moved, you need to take into account the current scale since it affects the Offset
(position).
offsetAdjustedForScale
variable as of type Offset
. This variable holds the last offset by taking into consideration the scale factor. Take the _startLastOffset
and subtract the _lastOffset
; then divide the result by the _lastScale
.// Calculate offset depending on current Image scaling.
Offset offsetAdjustedForScale = (_startLastOffset - _lastOffset) / _lastScale;
currentOffset
variable of type Offset
. This variable holds the current offset (position) of the image. The currentOffset
is calculated by taking the details.focalPoint
minus the offsetAdjustedForScale
times the _currentScale
. Offset currentOffset = details.focalPoint - (offsetAdjustedForScale * _currentScale);
Image
widget refresh the current position, add the setState()
method and populate the _currentOffset
value with the local currentOffset
variable.setState(() {
_currentOffset = currentOffset;
});
The following is the full _onScaleUpdate()
method:
void _onScaleUpdate(ScaleUpdateDetails details) {
print('ScaleUpdateDetails: $details - Scale: ${details.scale}');
if (details.scale != 1.0) {
// Scaling
double currentScale = _lastScale * details.scale;
if (currentScale < 0.5) {
currentScale = 0.5;
}
setState(() {
_currentScale = currentScale;
});
print('_scale: $_currentScale - _lastScale: $_lastScale');
} else if (details.scale == 1.0) {
// We are not scaling but dragging around screen
// Calculate offset depending on current Image scaling.
Offset offsetAdjustedForScale = (_startLastOffset - _lastOffset) / _lastScale;
Offset currentOffset = details.focalPoint - (offsetAdjustedForScale * _currentScale);
setState(() {
_currentOffset = currentOffset;
});
print('offsetAdjustedForScale: $offsetAdjustedForScale - _currentOffset: $_currentOffset');
}
}
_onDoubleTap()
method. This gesture is called when the user is double tapping the screen. When a double tap is detected, the image is scaled twice as large.currentScale
variable of type double
. The currentScale
is calculated by taking the _lastScale
times 2.0
(twice the size). double currentScale = _lastScale * 2.0;
if
statement that checks whether the currentScale
is greater than 16.0
(16 times the original size); then reset currentScale
to 1.0
. Add a call to the _resetToDefaultValues()
(created in step 19) method that resets all variables to their default values. The result is that the image is recentered and scaled to the original size.if
statement, set the _lastScale
variable to the local currentScale
variable.Image
widget refresh the current scale, add the setState()
method and populate the _currentScale
value with the local currentScale
variable.void _onDoubleTap() {
print('onDoubleTap');
// Calculate current scale and populate the _lastScale with currentScale
// if currentScale is greater than 16 times the original image, reset scale to default, 1.0
double currentScale = _lastScale * 2.0;
if (currentScale > 16.0) {
currentScale = 1.0;
_resetToDefaultValues();
}
_lastScale = currentScale;
setState(() {
_currentScale = currentScale;
});
}
_onLongPress()
method. This gesture is called when the user is pressing and holding on the screen. When a long press is detected, the image is reset to its original position and scale.Image
widget refresh to the default position and scale, add the setState()
method and call the _resetToDefaultValues()
method.void _onLongPress() {
print('onLongPress');
setState(() {
_resetToDefaultValues();
});
}
_resetToDefaultValues()
method. The purpose of this method is to reset all values to default with the Image
widget centered on the screen and scaled back to original size. Creating this method is a great way to share code reuse from anywhere in the page.void _resetToDefaultValues() {
_startLastOffset = Offset.zero;
_lastOffset = Offset.zero;
_currentOffset = Offset.zero;
_lastScale = 1.0;
_currentScale = 1.0;
}
The _onScaleStart()
method is called when a touch first begins to move or scale the image. The _startLastOffset
, _lastOffset
, and _lastScale
variables are populated with values needed to properly position and scale the image by the _onScaleUpdate()
method.
The _onScaleUpdate()
method is called when the user is either moving or scaling the image. The details (ScaleUpdateDetails
) object contains values such as the scale
, rotation
, and focalPoint
. By using the details.scale
value, you check whether the image is moving or scaling. If the details.scale
is greater than 1.0
, the image is scaling larger, and if the details.scale
is less than 1.0
, the image is scaling smaller. If the details.scale
is equal to 1.0
, the image is moving.
The _onDoubleTap()
method is called when the user is double tapping the screen. With each double tap, the image is scaled twice as large, and once it reaches 16 times the original size, the scale is reset to 1.0
, the original size.
The _onLongPress()
method is called when the user is pressing and holding on the screen. When this gesture is detected, the image is reset to its original position and scale by calling the _resetToDefaultValues()
method.
The _resetToDefaultValues()
method is responsible for resetting all values back to default. The Image
widget is moved back to the center of the screen and scaled to the original size.
Both the InkWell
and InkResponse
widgets are Material Components that respond to touch gestures. The InkWell
class extends
(subclass) the InkResponse
class. The InkResponse
class extends a StatefulWidget
class.
For the InkWell
, the area that responds to touch is rectangular in shape and shows a “splash” effect—though it really looks like a ripple. The splash effect is clipped to the rectangular area of the widget (so as not go outside it). If you need to expand the splash effect outside the rectangular area, the InkResponse
has a configurable shape. By default, the InkResponse
shows a circular splash effect that can expand outside its shape (Figure 11.3).
Tapping the InkWell
shows how the splash effect incrementally appears (Figure 11.4). In this scenario, the user tapped the left button, and it shows the splash effect gradually changing the button color from gray to a blue. The splash color stays inside the button's rectangular area.
Tapping the InkResponse
shows how the splash effect incrementally appears (Figure 11.5). In this scenario the user tapped the right button, and it shows the circular splash effect gradually changing the button color from gray to a blue. The splash color circularly expands outside the button's rectangular area.
The following are the InkWell
and InkResponse
gestures that you can listen for and take appropriate action. The gestures captured are taps on the screen except for the onHighlightChanged
property, which is called when part of the material starts or stops being highlighted.
onTap
onTapDown
onTapCancel
onDoubleTap
onLongPress
onHighlightChanged
Continuing with the previous gestures project to compare how each widget performs, you'll add an InkWell
and InkResponse
to the current gesture status bar display.
_buildBody(BuildContext context)
method, add a call to the _positionedInkWellAndInkResponse(context)
method. Place this call after the _positionedStatusBar(context)
method.Widget _buildBody(BuildContext context) {
return GestureDetector(
child: Stack(
fit: StackFit.expand,
children: <Widget>[
//_transformScaleAndTranslate(),
_transformMatrix4(),
_positionedStatusBar(context),
_positionedInkWellAndInkResponse(context),
],
),
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
onDoubleTap: _onDoubleTap,
onLongPress: _onLongPress,
);
}
_positionedInkWellAndInkResponse(BuildContext context) Positioned
method after the _positionedStatusBar(BuildContext context)
method. Return a Positioned
by using the default
constructor. The purpose of this Positioned
widget is to add to the current gesture status bar display the InkWell
and InkResponse
widgets.top
property to 50.0
to position it below the previous Positioned
widget (gesture status bar display) in the Stack
widget.width
property by using the MediaQuery width
to expand the full width of the device. The child
is a Container
with the color
property set to a shade of Colors.white54
.Container height
property to 56.0
. Set the Container child
to a Row
with the mainAxisAlignment
of MainAxisAlignment.spaceAround
.Positioned _positionedInkWellAndInkResponse(BuildContext context) {
return Positioned(
top: 50.0,
width: MediaQuery.of(context).size.width,
child: Container(
color: Colors.white54,
height: 56.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
],
),
),
);
}
Row children
list of Widget
an InkWell
and InkResponse
. Since both widgets have the same properties, follow the same instructions for the InkWell
and the InkResponse
.child
property to a Container
with the height
property set to 48.0
, the width
property set to 128.0
, and the color
property set to a light shade of Colors.black12
. Set the child
property to the Icons.touch_app
with the size
property of 32.0
.splashColor
property to Colors.lightGreenAccent
and the highlightColor
property to Colors.lightBlueAccent
. The splashColor
is displayed where the pointer first touched the screen, and the highlightColor
is the splash (ripple) effect.InkWell
and InkResponse
the onTap
, onDoubleTap
, and onLongPress
to listen for each gesture. Respectively pass the _setScaleSmall
, _setScaleBig
, and _onLongPress
methods.Positioned _positionedInkWellAndInkResponse(BuildContext context) {
return Positioned(
top: 50.0,
width: MediaQuery.of(context).size.width,
child: Container(
color: Colors.white54,
height: 56.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
InkWell(
child: Container(
height: 48.0,
width: 128.0,
color: Colors.black12,
child: Icon(
Icons.touch_app,
size: 32.0,
),
),
splashColor: Colors.lightGreenAccent,
highlightColor: Colors.lightBlueAccent,
onTap: _setScaleSmall,
onDoubleTap: _setScaleBig,
onLongPress: _onLongPress,
),
InkResponse(
child: Container(
height: 48.0,
width: 128.0,
color: Colors.black12,
child: Icon(
Icons.touch_app,
size: 32.0,
),
),
splashColor: Colors.lightGreenAccent,
highlightColor: Colors.lightBlueAccent,
onTap: _setScaleSmall,
onDoubleTap: _setScaleBig,
onLongPress: _onLongPress,
),
],
),
),
);
}
_setScaleSmall()
and _setScaleBig()
methods after the _resetToDefaultValues()
method. For the _setScaleSmall()
method, add a setState()
to modify the _currentScale
variable to 0.5
. When the onTap
gesture is captured, it will decrease the image's size to half of the original size.
For the _setScaleBig()
method, add a setState()
to modify the _currentScale
variable to 16.0
. When the onDoubleTap
gesture is captured, it will increase the image size to 16 times the original size.
void _setScaleSmall() {
setState(() {
_currentScale = 0.5;
});
}
void _setScaleBig() {
setState(() {
_currentScale = 16.0;
});
}
Both the InkWell
and InkResponse
widgets listen to the same gesture callbacks. The widgets capture the onTap
, onDoubleTap
, and onLongPress
gestures (properties).
When a single tap is captured, the onTap
calls the _setScaleSmall()
method to scale the image to half the original size.
When a double tap is captured, the onDoubleTap
calls the _setScaleBig()
method to scale the image to 16 times the original size.
When a long press is captured, the onLongPress
calls the _onLongPress()
method to reset all values to the original positions and sizes.
The main benefits of using the InkWell
and InkResponse
are to capture taps on the screen and have a beautiful splash. This kind of reaction makes for a good UX, correlating an animation to a user's action.
The Dismissible
widget is dismissed by a dragging gesture. The direction of the drag can be changed by using DismissDirection
for the direction
property. (See Table 11.2 for DismissDirection
options.) The Dismissible child
widget slides out of view and automatically animates the height or width (depending on dismiss direction) down to zero. This animation happens in two steps; first the Dismissible child
slides out of view, and second, the size animates down to zero. Once the Dismissible
is dismissed, you can use the onDismissed
callback to perform any necessary actions such as removing a data record from the database or marking a to‐do item complete (Figure 11.6). If you do not handle the onDismissed
callback, you'll receive the error “A dismissed Dismissible
widget is still part of the tree.” For example, if you use a List
of items, once the Dismissible
is removed you need to remove the item from the List
by implementing the onDismissed
callback. You'll take a detailed look at how to handle this in step 9 of the next exercise.
TABLE 11.2: DismissDirection
Dismiss Options
DIRECTION | DISMISSED WHEN… |
startToEnd |
Dragging left to right.* |
endToStart |
Dragging right to left.* |
horizontal |
Dragging either left or right. |
up |
Dragging up. |
down |
Dragging down. |
vertical |
Dragging either up or down. |
* Assuming reading direction is left to right; when reading direction is right to left, these work the opposite ways. |
In this example, you'll build a list of vacation trips, and when you drag from left to right, the Dismissible
shows a checkbox icon with a green background to mark the trip completed. When swiping from right to left, the Dismissible
shows a delete icon with a red background to remove the trip.
The Dismissible
handles all the animations such as sliding, resizing to remove the selected row, and sliding the next item on the list upward.
You'll create a Trip
class to hold the vacation details with an id
, tripName
, and tripLocation
variables. You load a List
to track the vacations by adding individual Trip
details. The body
property uses a ListView.builder
that returns a Dismissible
widget. You set the Dismissible child
property to a ListTile
, and the background
and secondaryBackground
properties return a Container
with the child
as a Row
with a children
list of Widget
with an Icon
.
Note that in this example to keep the widget tree shallow, you'll use methods instead of widget classes.
ch11_dismissible
. Again, you can follow the instructions from Chapter 4. For this project, you only need to create the pages
and classes
folders. Create the Home Class
as a StatefulWidget
since the data (state) requires changes.home.dart
file and add to the body
a ListView.builder()
. body: ListView.builder(),
trip.dart
package that you'll create next.import 'package:flutter/material.dart';
import 'package:ch11_dismissible/classes/trip.dart';
classes
folder. Right‐click the classes
folder, select New ➪ Dart File, enter trip.dart
, and click the OK button to save.Trip Class
. The Trip Class
holds the vacation details with an id
, tripName
, and tripLocation String
variables. Create the Trip
constructor with named parameters by entering the variable names this.id
, this.tripName
, and this.tripLocation
inside the curly brackets ({}
).class Trip {
String id;
String tripName;
String tripLocation;
Trip({this.id, this.tripName, this.tripLocation});
}
home.dart
file and after the class _HomeState extends State<Home>
and before @override
, add the List
variable _trips
initialized by an empty Trip List
. List _trips = List<Trip>();
initState()
to initialize the _trips List
. You are going to add 11 items to the _trips List
. Usually, this data would be read from a local database or a web server.@override
void initState() {
super.initState();
_trips..add(Trip(id: '0', tripName: 'Rome', tripLocation: 'Italy'))
..add(Trip(id: '1', tripName: 'Paris', tripLocation: 'France'))
..add(Trip(id: '2', tripName: 'New York', tripLocation: 'USA - New York'))
..add(Trip(id: '3', tripName: 'Cancun', tripLocation: 'Mexico'))
..add(Trip(id: '4', tripName: 'London', tripLocation: 'England'))
..add(Trip(id: '5', tripName: 'Sydney', tripLocation: 'Australia'))
..add(Trip(id: '6', tripName: 'Miami', tripLocation: 'USA - Florida'))
..add(Trip(id: '7', tripName: 'Rio de Janeiro', tripLocation: 'Brazil'))
..add(Trip(id: '8', tripName: 'Cusco', tripLocation: 'Peru'))
..add(Trip(id: '9', tripName: 'New Delhi', tripLocation: 'India'))
..add(Trip(id: '10', tripName: 'Tokyo', tripLocation: 'Japan'));
}
Trip
item completed or deleted in the database. Create the _markTripCompleted()
and _deleteTrip()
methods that act as placeholders to write to a database.void _markTripCompleted() {
// Mark trip completed in Database or web service
}
void _deleteTrip() {
// Delete trip from Database or web service
}
ListView.builder
constructor with the itemCount
argument set to _trips.length
, which is the number of rows in the _trips List
. For the itemBuilder
argument, it takes the BuildContext
and the widget index
as an int
value. itemCount: _trips.length,
The itemBuilder
returns a Dismissible
with the key
property as Key(_trips[index].id)
. The Key
is the identifier for each widget and must be unique, which is why you use the _trips id
item. The child
property is set to the _buildListTile(index)
method, which passes the current widget index
.
key: Key(_trips[index].id),
Dismissible
has a background
(drag left to right) and the secondaryBackground
(drag left to right) properties. Set the background
property to the _buildCompleteTrip()
method and set the secondaryBackground
to the _buildRemoveTrip()
method. Note that the Dismissible
has an optional direction
property that can set the restrictions on which direction to use.child: _buildListTile(index),
background: _buildCompleteTrip(),
secondaryBackground: _buildRemoveTrip(),
The onDismissed
callback (property) is called when the widget is dismissed, providing a function to run code by removing the dismissed widget item from the _trips List
. In a real‐world scenario, you would also update the database.
It's important that once the item is dismissed, it is removed from the _trips List
or it will cause an error.
// A dismissed Dismissible widget is still part of the tree.
// Make sure to implement the onDismissed handler and to immediately remove the Dismissible
// widget from the application once that handler has fired.
This makes sense since the item has been dismissed and removed. All of this is possible by using the unique key
property.
The onDismissed
passes the DismissDirection
where you check with a ternary operator whether the direction is startToEnd
and call the _markTripCompleted()
method or otherwise call the _deleteTrip()
method. The next step is to use the setState
to remove the dismissed item from the _trips List
by using the _trips.removeAt(index)
.
body: ListView.builder(
itemCount: _trips.length,
itemBuilder: (BuildContext context, int index) {
return Dismissible(
key: Key(_trips[index].id),
child: _buildListTile(index),
background: _buildCompleteTrip(),
secondaryBackground: _buildRemoveTrip(),
onDismissed: (DismissDirection direction) {
direction == DismissDirection.startToEnd ? _markTripCompleted() : _deleteTrip();
// Remove item from List
setState(() {
_trips.removeAt(index);
});
},
);
},
),
_buildListTile(int index) Widget
method after the Widget build(BuildContext context) {…}
. Return a ListTile
and set the title
, subtitle
, leading
, and trailing
properties.title
property as a Text
widget that shows the tripName
and set the subtitle
to tripLocation
. Set the leading
and trailing
properties as Icons
.ListTile _buildListTile(int index) {
return ListTile(
title: Text('${_trips[index].tripName}'),
subtitle: Text(_trips[index].tripLocation),
leading: Icon(Icons.flight),
trailing: Icon(Icons.fastfood),
);
}
_buildCompleteTrip() Widget
method to return a Container
with the color
as green
and the child
property as a Padding
. The Padding child
is a Row
with the alignment set to start
(on the left side for left‐to‐right languages) with a children
list of Widget
of an Icon
.The background
property is revealed when the user drags the item, and it's important to convey what action will take place. In this case, you are completing a trip, and you show a done
(checkbox) Icon
with a green background convening the action.
Container _buildCompleteTrip() {
return Container(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Icon(
Icons.done,
color: Colors.white,
),
],
),
),
);
}
_buildRemoveTrip() Widget
method to return a Container
with the color
as red
and the child
property as a Padding
. The Padding child
is a Row
with the alignment set to end
(on the right side for left‐to‐right languages) with a children
list of Widget
of an Icon
.The secondaryBackground
property is revealed when the user drags the item, and it's important to convey what action will take place. In this case, you are deleting a trip, and you show a delete
(trash can) Icon
with a red background convening the action.
Container _buildRemoveTrip() {
return Container(
color: Colors.red,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(
Icons.delete,
color: Colors.white,
),
],
),
),
);
}
You used a ListView
to build a list of Trip
details. The ListView itemBuilder
returns a Dismissible
with a key
property set by using the Key
class as a unique identifier for each widget. Using the key
property is extremely important because the Dismissible
uses it when it replaces one widget with another in the widget tree.
To convey the appropriate action when the user is dragging (left to right), you customized the background
property to reveal a green
background with a done Icon
. When the user is dragging (right to left), you customized the secondaryBackground
property to reveal a red
background with a delete Icon
.
You used the onDismissed
callback (property) to check for the DismissDirection
and take appropriate action. By using the ternary operator, you checked whether the direction was from startToEnd
, and you called the _markTripCompleted()
method; otherwise, you called the _deleteTrip()
method. Next, you used the setState
to remove the current item from the _trips List
.
In this chapter, you learned how to use a GestureDetector
to handle onTap
, onDoubleTap
, onLongPress
, and onPanUpdate
gestures. The onPanUpdate
is suitable to use when you need to track dragging in any direction. You took an in‐depth look at using the GestureDetector
to move, scale by zooming in/out, double tap to increase zoom, and long press to reset the elephant image to original size. For example, these techniques would be applied to a journaling app when a user selected an image and wanted to look closer at details. To accomplish this goal, you used the onScaleStart
and onScaleUpdate
to scale the image. Use the onDoubleTap
to increase the zoom and onLongPress
to reset the zoom to the original default size.
You learned two different techniques to scale and move the image when a gesture is detected. With the first technique, you used the Transform
widget by nesting the Transform.scale
constructor to resize the image and the Transform.translate
constructor to move the image. For the second technique, you used the Transform default
constructor by using the Matrix4
to apply the transformations. By using the Matrix4
, you executed multiple cascading transformations (..scale()..translate()
) without the need to nest multiple Transform
widgets.
You used the InkWell
and InkResponse
to respond to touch gestures like tap, double tap, and long press. Both widgets are Material Components (Flutter material design widgets) that display a splashing effect when tapped.
You implemented the drag‐and‐drop feature by using the Draggable
and DragTarget
widgets. These widgets are used in conjunction. The Draggable
widget has a data
property that passes information to the DragTarget
widget. The DragTarget
widget can accept or refuse the data, which gives you the ability to check for the correct data format. In this example, you dragged the paint palette Icon
(Draggable
) over the Text
(DragTarget
) widget, and once you let go, the Text
color changes to red.
The Dismissible
widget listens to vertical and horizontal dragging gestures. By using the DismissDirection
for the direction property, you can limit which dragging gestures you listen to, such as restricting to horizontal‐only gestures. In this example, you created a List
of Trip
items that are displayed with the ListView.builder
. When the user drags an item on the list from left to right, a green background with a checkbox Icon
is revealed to convey the action that is about to be performed, completing the trip. But if the user drags on the list item from right to left, a red background with a trash can Icon
is revealed to convey the action that the trip is going to be deleted. How does the Dismissible
know which item to delete? By using the Dismissible key
property, you passed a unique identifier for each item on the list, and once the onDismissed
(callback) property is called, you checked the direction of the drag and took appropriate action. You then used the setState
to make sure the dismissed item is removed from the _trips
list. It is important to handle the onDismissed
callback or you will receive the error “A dismissed Dismissible widget is still part of the tree.”
In the next chapter, you'll learn how to write iOS and Android platform‐specific code. You'll be using Swift for iOS and Kotlin for Android. These platform channels give you the ability to use native features such as accessing the device GPS location, local notifications, local file system, sharing, and many more.
TOPIC | KEY CONCEPTS |
Implementing GestureDetector |
This widget recognizes gestures such as tap, double tap, long press, pan, vertical drag, horizontal drag, and scale. |
Implementing Draggable |
This widget is dragged to a DragTarget . |
Implementing DragTarget |
This widget receives data from a Draggable . |
Implementing InkWell and InkResponse |
The InkWell is a rectangular area that responds to touch and clips splashes within its area. The InkResponse responds to touch, and the splashes expand outside its area. |
Implementing Dismissible |
This widget is dismissed by dragging. |