11
Applying Interactivity

WHAT YOU WILL LEARN IN THIS CHAPTER

  • How to use GestureDetector, which recognizes gestures such as tap, double tap, long press, pan, vertical drag, horizontal drag, and scale.
  • How to use the Draggable widget that is dragged to a DragTarget.
  • How to use the DragTarget widget that receives data from a Draggable.
  • How to use the 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.
  • How to use the 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.

SETTING UP GESTUREDETECTOR: THE BASICS

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

Depiction of vertical, horizontal, and pan gestures error

FIGURE 11.1: Vertical, horizontal, and pan gestures error

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:

  • Tap
    • onTapDown
    • onTapUp
    • onTap
    • onTapCancel
  • Double tap
    • onDoubleTap
  • Long press
    • onLongPress
  • Pan
    • onPanStart
    • onPanUpdate
    • onPanEnd
  • Vertical drag
    • onVerticalDragStart
    • onVerticalDragUpdate
    • onVerticalDragEnd
  • Horizontal drag
    • onHorizontalDragStart
    • onHorizontalDragUpdate
    • onHorizontalDragEnd
  • Scale
    • onScaleStart
    • onScaleUpdate
    • onScaleEnd

TRY IT OUT   Creating the Gesture, Drag‐and‐Drop App

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.

“Building the gesture area that catches drag events. To make the gesture area visible, an alarm clock icon is placed.”

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.

  1. Create a new Flutter project and name it 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.
  2. Open the main.dart file. Change the primarySwatch property from blue to lightGreen.
      primarySwatch: Colors.lightGreen,
  3. Open the 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,
          ),
         ],
        ),
       ),
      ),
  4. Add the _buildGestureDetector() GestureDetector method after the Widget build(BuildContext context) {…}.
  5. Return a GestureDetector listening to the onTap, onDoubleTap, onLongPress, and onPanUpdate gestures.
  6. To view captured gestures, add a 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).

  7. To update the screen with the pointer location and to have code reuse, create the _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;
       });
      }

“Screenshot of an example where 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.”

HOW IT WORKS

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.


IMPLEMENTING THE DRAGGABLE AND DRAGTARGET WIDGETS

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.


TRY IT OUT   Gestures: Adding Drag and Drop

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.

“Screenshot of 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.” ”

Continuing with the previous gestures project, let's add the Draggable and DragTarget methods.

  1. Create the _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,
       );
      }
  2. Create the _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.
  3. Set the 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,
         ),
        ),
       );
      }
    “Screenshot of creating the 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.”

HOW IT WORKS

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.


USING THE GESTUREDETECTOR FOR MOVING AND SCALING

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

“Screenshot of moving and scaling an image”

FIGURE 11.2: Moving and scaling an image

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.

TRY IT OUT   Creating an App with Gestures for Moving and Scaling

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.

“Screenshot of an example where 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.

  1. Create a new Flutter project and name it 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.
  2. Create the Home Class as a StatefulWidget since the data (state) requires changes.
  3. Open the 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/
  4. Click the Save button, and depending on the Editor you are using, it automatically runs the 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.
  5. Add the folder assets and subfolder images at the project's root, and then copy the elephant.jpg file to the images folder.
  6. Open the 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),
  7. Below the 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) {
  8. Add the _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.

  9. Set the Stack fit property to StackFit.expand to expand to the biggest size allowed.
  10. For the 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.
  11. Comment out the _transformMatrix4() method since you will be testing with the _transformScaleAndTranslate() first.
  12. Add to the 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,
       );
      }
  13. In this step you implement the first technique (moving and scaling) by nesting the scale and translate constructors. Add the _transformScaleAndTranslate() Transform method after Widget build(BuildContext context) {…}. Return a Transform by nesting the scale and translate constructors.
  14. For the Transform.scale constructor's scale argument, enter the variable _currentScale and set the child argument to the Transform.translate constructor.
  15. For the 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'),
           ),
          ),
         );
      }
  16. In this step, you implement the second technique (moving and scaling) by using the default constructor. Add the _transformMatrix4() Transform method. Return a Transform by using the default constructor.
  17. For the 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.
  18. For the 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 yaxis values are utilized.
  19. For the 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.
  20. To keep the image center‐aligned during scaling, set the 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'),
        ),
       );
      }
  21. Add the _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.
  22. Set the 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',
           ),
          ],
         ),
        ),
       );
      }

HOW IT WORKS

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.



TRY IT OUT   Adding Logic and Calculations to the Moving and Scaling Project

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.

  1. Create the _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;
      }
  2. Create the _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}');
      }
  3. Set up an 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
       }
      }
  4. To calculate the current scale, create a local variable called 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;
  5. To restrict the image not to scale down to more than half its size, check whether the currentScale is less than 0.5 (half the original size) and reset the value to 0.5.
       if (currentScale < 0.5) {
       currentScale = 0.5;
      }
  6. To have the 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');
       }
      }
  7. Continue adding to the _onScaleUpdate() method the logic to move the image around the screen.
  8. Add an 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).

  9. Create the local 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;
  10. Create the local 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);
  11. To have the 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');
       }
      }
  12. Create the _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.
  13. Create the local currentScale variable of type double. The currentScale is calculated by taking the _lastScale times 2.0 (twice the size).
      double currentScale = _lastScale * 2.0;
  14. Add an 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.
  15. After the if statement, set the _lastScale variable to the local currentScale variable.
  16. To have the 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;
       });
      }
  17. Create the _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.
  18. To have the Image widget refresh to the default position and scale, add the setState() method and call the _resetToDefaultValues() method.
      void _onLongPress() {
       print('onLongPress');
    
       setState(() {
        _resetToDefaultValues();
       });
      }
  19. Create the _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;
      }
“Screenshot of the onScaleStart and onScaleUpdate 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.”

HOW IT WORKS

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.


USING THE INKWELL AND INKRESPONSE GESTURES

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

Screenshot of InkWell and InkResponse splash.

FIGURE 11.3: InkWell and InkResponse splash

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.

Screenshot of InkWell gradual splash inside rectangular area.

FIGURE 11.4: InkWell gradual splash inside 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.

Screenshot of InkResponse gradual splash outside rectangular area.

FIGURE 11.5: InkResponse gradual splash outside 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.

  • Tap
    • onTap
    • onTapDown
    • onTapCancel
  • Double tap
    • onDoubleTap
  • Long press
    • onLongPress
  • Highlight changed
    • onHighlightChanged

TRY IT OUT   Adding InkWell and InkResponse to the Gesture Status Bar

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.

“Screenshot of comparing how each widget performs. An InkWell and an InkResponse are added to the current gesture status bar display.”
  1. In the _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,
       );
      }
  2. Create the _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.
  3. Set the top property to 50.0 to position it below the previous Positioned widget (gesture status bar display) in the Stack widget.
  4. 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.
  5. Set the 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>[
    
          ],
         ),
        ),
       );
      }
  6. Add to the 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.
  7. Set the 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.
  8. To customize the splash color, set the 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.
  9. Add to the 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,
           ),
          ],
         ),
        ),
       );
      }
  10. Create the _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;
       });
      }

    “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.”

HOW IT WORKS

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.


USING THE DISMISSIBLE WIDGET

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.
Screenshot of dismissible widget showing the swiped row dismissed animation to complete item

FIGURE 11.6: Dismissible widget showing the swiped row dismissed animation to complete the item


TRY IT OUT   Creating the Dismissible App

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.

“Screenshot of building a list of vacation trips, and when you drag from left to right, the Dismissible depicts 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.

  1. Create a new Flutter project and name it 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.
  2. Open the home.dart file and add to the body a ListView.builder().
      body: ListView.builder(),
  3. Add to the top of the file the import trip.dart package that you'll create next.
      import 'package:flutter/material.dart';
      import 'package:ch11_dismissible/classes/trip.dart';
  4. Create a new Dart file under the classes folder. Right‐click the classes folder, select New ➪ Dart File, enter trip.dart, and click the OK button to save.
  5. Create the 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});
      }
  6. Edit the 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>();
  7. Override the 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'));
      }
  8. Create two methods that simulate marking a 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
      }
  9. Set the 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),
  10. The 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);
          });
         },
        );
       },
      ),
  11. Add the _buildListTile(int index) Widget method after the Widget build(BuildContext context) {…}. Return a ListTile and set the title, subtitle, leading, and trailing properties.
  12. Set the 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),
          );
      }
  13. Add the _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.

    “Screenshot of adding the 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 .”
      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,
              ),
             ],
            ),
           ),
          );
      }
  14. Add the _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.

    “Screenshot of adding the 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 .”
     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,
              ),
             ],
            ),
           ),
          );
      }
    “Screenshot of the secondaryBackground property that 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.”

HOW IT WORKS

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.


SUMMARY

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.

image WHAT YOU LEARNED IN THIS CHAPTER

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.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset