One way to listen for touch events is to set a touch event listener using the following View function:
fun setOnTouchListener(l: View.OnTouchListener)
This function works the same way as setOnClickListener(View.OnClickListener). You provide an implementation of View.OnTouchListener, and your listener will be called every time a touch event happens.
However, because you are subclassing View, you can take a shortcut and override this View function instead:
override fun onTouchEvent(event: MotionEvent): Boolean
This function receives an instance of MotionEvent, a class that describes the touch event, including its location and its action. The action describes the stage of the event:
Action constants | Description |
---|---|
ACTION_DOWN
|
user touches finger on the screen |
ACTION_MOVE
|
user moves finger on the screen |
ACTION_UP
|
user lifts finger off the screen |
ACTION_CANCEL
|
a parent view has intercepted the touch event |
In your implementation of onTouchEvent(MotionEvent), you can check the value of the action by calling the MotionEvent function:
final fun getAction(): Int
Let’s get to it. In BoxDrawingView.kt, add a log tag and then an implementation of onTouchEvent(MotionEvent) that logs a message for each of the four actions.
Listing 30.3 Implementing BoxDrawingView
(BoxDrawingView.kt
)
private const val TAG = "BoxDrawingView" class BoxDrawingView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) { override fun onTouchEvent(event: MotionEvent): Boolean { val current = PointF(event.x, event.y) var action = "" when (event.action) { MotionEvent.ACTION_DOWN -> { action = "ACTION_DOWN" } MotionEvent.ACTION_MOVE -> { action = "ACTION_MOVE" } MotionEvent.ACTION_UP -> { action = "ACTION_UP" } MotionEvent.ACTION_CANCEL -> { action = "ACTION_CANCEL" } } Log.i(TAG, "$action at x=${current.x}, y=${current.y}") return true } }
Notice that you package your X and Y coordinates in a PointF object. You want to pass these two values together as you go through the rest of the chapter. PointF is a container class provided by Android that does this for you.
Run DragAndDraw and pull up Logcat. Touch the screen and drag your finger. (On the emulator, click and drag.) You should see a report of the X and Y coordinates of every touch action that BoxDrawingView receives.
BoxDrawingView is intended to draw boxes on the screen, not just log coordinates. There are a few problems to solve to get there.
First, to define a box, you need two points: the start point (where the finger was initially placed) and the end point (where the finger currently is).
To define a box, then, requires keeping track of data from more than one MotionEvent. You will store this data in a Box object.
Create a class named Box to represent the data that defines a single box.
Listing 30.4 Adding Box
(Box.kt
)
class Box(val start: PointF) { var end: PointF = start val left: Float get() = Math.min(start.x, end.x) val right: Float get() = Math.max(start.x, end.x) val top: Float get() = Math.min(start.y, end.y) val bottom: Float get() = Math.max(start.y, end.y) }
When the user touches BoxDrawingView, a new Box will be created and added to a list of existing boxes (Figure 30.3).
Back in BoxDrawingView, use your new Box object to track your drawing state.
Listing 30.5 Adding drag lifecycle functions (BoxDrawingView.kt
)
class BoxDrawingView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) { private var currentBox: Box? = null private val boxen = mutableListOf<Box>() override fun onTouchEvent(event: MotionEvent): Boolean { val current = PointF(event.x, event.y) var action = "" when (event.action) { MotionEvent.ACTION_DOWN -> { action = "ACTION_DOWN" // Reset drawing state currentBox = Box(current).also { boxen.add(it) } } MotionEvent.ACTION_MOVE -> { action = "ACTION_MOVE" updateCurrentBox(current) } MotionEvent.ACTION_UP -> { action = "ACTION_UP" updateCurrentBox(current) currentBox = null } MotionEvent.ACTION_CANCEL -> { action = "ACTION_CANCEL" currentBox = null } } Log.i(TAG, "$action at x=${current.x}, y=${current.y}") return true } private fun updateCurrentBox(current: PointF) { currentBox?.let { it.end = current invalidate() } } }
Any time an ACTION_DOWN motion event is received, you set currentBox
to be a new Box with its origin as the event’s location.
This new Box is added to the list of boxes. (In the next section, when you implement custom drawing, BoxDrawingView will draw every Box within this list to the screen.)
As the user’s finger moves around the screen, you update currentBox.end
. Then, when the touch is canceled or when the user’s finger leaves the screen, you update the current box with the final reported location and null out currentBox
to end your draw motion. The Box is complete; it is stored safely in the list but will no longer be updated about motion events.
Notice the call to invalidate() in the updateCurrentBox() function. This forces BoxDrawingView to redraw itself so that the user can see the box while dragging across the screen. Which brings you to the next step: drawing the boxes to the screen.
54.210.85.205