Building a CustomView -TicTacToe
Part 2
This is the second post of this “Building a CustomView” series, please check out the first one, if you haven’t already.
Handling Touch (Part 2)
In the last post we were able to draw text inside the first square. Actually, we could draw text inside any square we want, we just have to pick the right element from our squares
array and pass to drawTextInsideRectangle()
method. InOrder to do that we need to detect the touch on individual square sections.
Before that let’s read a little about Android Touch System.
Understanding Basics of Android Touch Event System
In the figure 1, when the user touches the View
it starts a flow of touch event which is wrapped up as a MotionEvent object. This object has information about touch location, event action, number of pointers, event time, pressure etc. We are only interested in action and location here.
- Now, the flow of touch event happens in two phase. In first phase, every one whose bounds contain the touch point coordinates is notified about the touch. This starts at the Activity and flows top down through views or viewGroups. To dispatch event down they call the
dispatchTouchEvent()
method. - The flow of the event can be broken by anyone using
onInterceptTouchEvent()
( just have to returntrue
) when they are being notified about it. - Now when the flow finally reaches the targeted view, it gets notified. If the user has implemented
OnTouchListener.OnTouch()
, this is where it gets handled else it goes toonTouchEvent
of that view. From this targeted view the handling phase starts. - If
onTouchEvent()
returnstrue
for any touch event, then the flow stops there. This is what we are going to do, overrideonTouchEvent()
and returntrue
inside our custom view. - If the targeted view does not handle the touch i.e returns
false
fromonTouchEvent()
.Then the handling flow starts from bottom to top, all the views from bottom to top are recursively given a chance to handle it. If no one handles it, the activity gets the chance to handle it and then the handling flow stops there. - If you want to go in detail check this link.
We probably did not require that much info but its good to know.
Now we know what need’s to be done. We override onTouchEvent()
and handle event actions inside our TicTacToeView
. We are not handling multi-touch actions just the basic ones.
event.x
andevent.y
are the touch coordinates. This gives us the latest touch info.- A “gesture” begins with
ACTION_DOWN
and ends withACTION_UP
. ACTION_DOWN
: This gets called when the user first time places its finger on the screen.x, y
are the touch coordinates for the same. This is the starting point of a gesture.ACTION_MOVE
: This gets called when the user starts moving his finger on the screen.This will be called multiple times giving you latestx, y
coordinates. We won’t be needing it.ACTION_UP
: This is fired when the user removes his finger from the screen. Coordinatesx, y
will give you location from where he removed his finger. This is the end point of a gesture.ACTION_CANCEL
: This is called when its parent takes the possession of the event. Like when we start a touch gesture on a view inside aRecyclerView
but instead of clicking we start a scroll. This can also be the end point and here we will have to reset things and behave like the gesture never happened.
Note: For simplicity, we are taking the touch object as finger but it could be any of this — mouse, pen, finger, trackball.
If we return
false
fromACTION_DOWN
no further action events are received.
Detecting and Highlighting the Touched Rectangle
We get the coordinate(x, y
) of touch event happening on ACTION_DOWN
, we just have to find from our squares
array that which square is currently being touched or in other words in which square the coordinate x, y
is present.
We loop through our squares
(array of rects) that we have saved in our first post and we check whether the coordinate x, y
is inside it (rect.contains(x, y)
). If we find one, we return a Pair of the index for that square.
We will use the above method inside ACTION_DOWN
.
We will also use one global variable touching
to determine whether the user is touching the screen or not. Once he removes the finger, we set it to false
, check ACTION_UP
in the below-provided code. We save our touched square’s indexes and then call invalidate(rect: Rect)
method to let the view know its time to redraw. Note that we are passing a rect area as a parameter so only that area is redrawn.
Why we need to redraw? That’s because we will highlight our touched square as long as touching
is true.
Now inside onDraw()
when we get touching
is true
we will highlight that touched square. We know the touched square as we have saved its indexes in rectIndex
. rectIndex.first
and rectIndex.second
returns row and column indexes of touched square respectively.
We have used canvas.drawRect(rect, paint)
to draw the highlight. Let’s see some result —
That wasn’t that difficult. Was it?
Now next part is to draw text(X or O) inside that square, but wait haven’t we already done that? We just have to pass that rect inside drawTextInsideRectangle(rect: Rect)
that we have created in the previous post.
Yes, that is partially correct. When we call the invalidate()
method, view redraws from scratch.The previous state is lost. Analyse the code below.
if (touching) {
drawHighlightRectangle(canvas)
}
Where is the else part for removing the highlight when the user removes his finger from the screen?.
It’s not required that’s because when the view redraws it will lose its previous state and there will be no highlight initially. Also notice that we are also redrawing our lines when invalidate()
is called.
So even if we draw the text in a square it’s going to be erased when we click the other one. What we need is something to store that state. Remember in the previous post we created one array squareData
and initialised with all empty text entries. We are going to use that array. We just have to fill that array with correct text and inside onDraw
, we have to draw everything from it using drawTextInsideRectangle(rect: Rect)
.
Registering a click and storing the state
Filling the array with correct text is done when a successful touch gesture is detected. We have used the getRectIndexesFor()
function inside ACTION_DOWN
for detecting the initial touched square and insideACTION_UP
for detecting the final touched square. If both originating square and final square are same we consider it as click else we ignore it. That means when the user presses on square one and moves his finger to square two and then lifts its finger, we will not consider it as a click.
onSquarePressed()
method is implemented in the activity and notifies it about the click on a specific square. Based on some logic the activity tell the TicTacToeView
to draw “X”or to draw “O”.
val moveX = "X"
val moveY = "O"
..
fun drawXAtPosition(x: Int, y: Int) {
squareData[x][y] = moveX
invalidate(squares[x][y])
}
fun drawOAtPosition(x: Int, y: Int) {
squareData[x][y] = moveY
invalidate(squares[x][y])
}
Above two methods are used to fill in the entries inside squareData
and after that, we tell to redraw. This way when the user clicks a square, that info is passed to the activity which in turns call the drawXAtPosition
or drawOAtPosition
methods. These methods enter data inside squareData
based on the clicked index and then calls invalidate()
on that square.
Now we just have to draw the states out inside onDraw()
.
drawSquareStates()
method loops through the squareDate array and draws all the entries out inside the onDraw()
.
Let’s see the final result of this post.
Yeah! We are almost done building TicTacToeView
.
Thank you so much for your time. Your comments and suggestions are welcome.