Tutorial 1: Handling Touch Events¶
In this tutorial, we'll see how to properly use the touch event mechanism to provide some interaction with our app.
For that, we're going to implement a simple game where red and blue rectangles appear randomly on the screen. The user can touch only the red rectangles. A counter is incremented when a red rectangle is touched.
Android Project¶
Create a new Android project or open an existing one.
Let's begin by creating an empty activity as described in the Basics.
The name of our activity will be TouchInteractionActivity and the corresponding layout file has the name
activity_touch_interaction. After that, add the DrawView
component to the layout file (see Adding the DrawView Component).
The layout looks like this now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="net.offbeatpioneer.demoapp.retrographicsengine.tutorials.TouchInteractionActivity"> <net.offbeatpioneer.retroengine.view.DrawView android:id="@+id/graphics" android:layout_width="match_parent" android:layout_height="match_parent" android:screenOrientation="landscape" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" /> <TextView android:id="@+id/text_counter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginRight="8dp" android:layout_marginTop="8dp" android:text="0" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> |
A TextView
is also added in advance which will function as a counter later.
Add the following code to the activity after it is created:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class TouchInteractionActivity extends AppCompatActivity { public static RenderThread renderThread; private DrawView drawView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_touch_interaction); drawView = findViewById(R.id.graphics); renderThread = new RenderThread(drawView); } @Override protected void onResume() { super.onResume(); if (renderThread != null) { RetroEngine.shouldWait = false; } } @Override protected void onPause() { super.onPause(); if (renderThread != null) { RetroEngine.isRunning = true; } } } |
Create the State¶
Now, we're going to create a state class and name it RectangleGameState
. Use the built in feature of
Android Studio or create a new file RectangleGameState.java
and insert:
1 2 3 4 5 6 7 8 9 10 11 12 | public class RectangleGameState extends State { private TextView counterView; private long counter = 0; @Override public void init() { counterView = manager.getParentActivity().findViewById(R.id.text_counter); } // ... code omitted } |
In the init()
method we find our counter view in the layout and save
the instance for later when we're going to update it.
Adding Rectangles¶
Before we're handling the touch events, let's start with the graphics first.
Once the activity is started, we're going to add rectangles to our current state. They will appear and after that they're going to slowly fade away. For that task, we write a method that will create our rectangle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | public AbstractSprite createRectangle(int color) { RectangleSprite sprite = new RectangleSprite(color); // create squares int size = MathUtils.getRandomBetween(20, 100); int posX = MathUtils.getRandomBetween(size, RetroEngine.W - size); int posY = MathUtils.getRandomBetween(size, RetroEngine.H - size); //fade in animation final AlphaValueTransition fadeIn = new AlphaValueTransition(0, 255, 2000); fadeIn.setListener(new IAnimationSuiteListener() { @Override public void onAnimationStart(AnimationSuite animationSuite) { } @Override public void onAnimationRepeat(AnimationSuite animationSuite) { } @Override public void onAnimationEnd(AnimationSuite animationSuite) { // start the fade out animation animationSuite.getAnimatedSprite().beginAnimation(1); } }); // fade out animation final AlphaValueTransition fadeOut = new AlphaValueTransition(255, 0, 1000); fadeOut.setListener(new IAnimationSuiteListener() { @Override public void onAnimationStart(AnimationSuite animationSuite) { } @Override public void onAnimationRepeat(AnimationSuite animationSuite) { } @Override public void onAnimationEnd(AnimationSuite animationSuite) { animationSuite.getAnimatedSprite().setActive(false); } }); // add animations sprite.addAnimations(fadeIn, fadeOut); // create the sprite return sprite.init(new PointF(posX, posY), size, size); } |
Actually we're creating squares by applying the same value for the width and height of the rectangle.
The size and position are randomly generated. We use the MathUtils
class of RetroGraphicsEngine.
The square will be place inside the area of the drawing surface
of the DrawView
component. Next, two separate animations are implemented. The first one serves the purpose
to fade the rectangle in and the second one, fades the rectangle out. This is accomplished by manipulating the
alpha value of the sprite.
The second animation should be started, when the first one ends. For that, we implemented a listener of type IAnimationSuiteListener
to
react on the end of the animation. For the fade out animation the listener will deactivate the sprite after the animation ends.
Adding the Rectangles to the State¶
Every two seconds a rectangle is added to the scene in our RectangleGameState
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private long timeNow = 0; private String[] COLORS = new String[] {"red", "blue"}; @Override public void updateLogic() { long tc = RetroEngine.getTickCount(); if(tc > timeNow + 2000) { timeNow = tc; int col = MathUtils.getRandomBetween(0, 1); AbstractSprite rect = createRectangle(Color.parseColor(COLORS[col])); addSprite(rect); rect.beginAnimation(0); } updateSprites(); } |
The color value is a color int value. The RGB values ranging from 0 to 255 for each of the components. But we can also specify the name of the color here. It will be either "red" or "blue".
To add the sprite we're using the State#addSprite()
method which is provided by the parent class.
It will automatically add this sprite to the root node in our state. Then, we start the first
animation that was added. In this case it's the fade in animation. If we would use
rect.beginAnimation();
all added animation so far would be
executed simultaneously. So we specify the index to start only the first one.
The last statement State#updateSprites()
calls the update
method of each sprite in the state.
Finally, we have to update the render
method, so that the added sprites
get actually drawn:
1 2 3 4 | @Override public void render(Canvas canvas, Paint paint, long currentTime) { drawSprites(canvas, currentTime); } |
Intercept Touch Event¶
In order to get the touch responses we have to override the State#onTouchEvent(View, MotionEvent)
method
in our state class.
Implementation Logic in onTouchEvent()
Don't do anything related to graphics in this method itself. Those events are captured
and processed in another thread by Android and the RenderThread
has no access to it.
For instance, if you want to add sprites to the root node of a state, then just save the information
about what happend and evaluate it in the State#updateLogic()
method.
The goal is now to detect if the user touches a red rectangle. The first step is
to determine the touch position. The updated onTouchEvent
method in RectangleGameState
:
1 2 3 4 5 6 7 8 9 | float x = 0; float y = 0; @Override public boolean onTouchEvent(View v, MotionEvent event) { x = event.getX(); y = event.getY(); return false; } |
That's all. We've overridden the onTouchEvent() to listen for touch events.
We don't want any more implementation logic here. We just store
the coordinates into the member variables x
and y
.
To actually do the check, we make use of
the helper class ColorTools
in the package net.offbeatpioneer.retroengine.core.util
of RetroGraphicEngine. It provides the useful method ColorTools#closeMatch(int, int, int)
.
We have to update our updateLogic()
method in RectangleGameState
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public void updateLogic() { // ... code omitted for (AbstractSprite each : ((SpriteListGroup) getRootGroup()).getChildren()) { if (each.checkColWithPoint(new PointF(x, y))) { if (ColorTools.closeMatch(each.getTexture().getPixel(2, 2), Color.parseColor("red"), 0)) { counter++; // update UI getHandler().post(new Runnable() { @Override public void run() { counterView.setText(String.format(Locale.getDefault(), "Hits: %d", counter)); } }); } } } // our previous method updateSprites(); } |
We iterate through all sprites and do a simple collision check.
If this evaluates to true
, we match the colors. For that, we retrieve
the texture of the sprite and get the upper-left pixel and match it
with red. We pass a tolerance parameter as third argument of the method.
This may be very useful in other cases, when colors should be matched approximately.
In this case the rectangles fade in and out but the closeMatch
method doesn't
take the alpha value into account. So this isn't a problem here.
Update UI
To update views in the UI we acquire the Androids Handler
which is kindly
provided by the parent class. This handler runs in a different thread which is save
to add logic here for updating the UI.
Start the App¶
Before we can start the app, we have to add our state to the RenderThread
.
Open your TouchInteractionActivity.java
activity and in the onCreate
method
of it insert this as last statement:
1 2 3 4 5 6 7 8 9 10 11 | @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_touch_interaction); RetroEngine.init(this); drawView = findViewById(R.id.graphics); renderThread = new RenderThread(drawView); renderThread.addState(new RectangleGameState()); } |
The result should look like this:
Improvements¶
As you tried the example you probably noticed that you can get more hits when swiping
over the screen. You can extend the RectangleSprite
class and add an
additional state if the rectangle was already touched. So we get the hits
only once for every rectangle.