Adding interactions

Let's start by using the code from the Example25-GLDrawing. Processing user interactions is quite simple, as we have already seen in our previous examples. We don't have to do anything different than before, just override the onTouchEvent() method in our class extending GLSurfaceView and react properly to the different MotionEvents we will receive. For instance, if we don't return true when we receive a MotionEvent.ACTION_DOWN, we will not receive any further events, as we are basically saying that we are not handling the event.

Once we have the source code of the example, let's add a simple implementation of the onTouchEvent() that tracks drag events:

private float dragX; 
private float dragY; 
 
@Override 
public boolean onTouchEvent(MotionEvent event) { 
   switch(event.getAction()) { 
       case MotionEvent.ACTION_DOWN: 
           dragX = event.getX(); 
           dragY = event.getY(); 
 
           getParent().requestDisallowInterceptTouchEvent(true); 
           return true; 
 
       case MotionEvent.ACTION_UP: 
           getParent().requestDisallowInterceptTouchEvent(false); 
           return true; 
 
       case MotionEvent.ACTION_MOVE: 
           float newX = event.getX(); 
           float newY = event.getY(); 
 
           angleTarget -= (dragX - newX) / 3.f; 
 
           dragX = newX; 
           dragY = newY; 
           return true; 
       default: 
           return false; 
   } 
} 

We'll use the drag amount to change the angle of rotation of the cube, as we will see in the following code snippets. In addition, later in this chapter, we will see how we can do this animation using a scroller class, but, for the moment, let's use a fixed time-step mechanism:

private float angle = 0.f; 
private float angleTarget = 0.f; 
private float angleFr = 0.f; 
 
private void animateLogic() { 
    long currentTime = SystemClock.elapsedRealtime(); 
    accTime += currentTime - timeStart; 
    timeStart = currentTime; 
 
    while (accTime > TIME_THRESHOLD) { 
        angle += (angleTarget - angle) / 4.f; 
        accTime -= TIME_THRESHOLD; 
    } 
 
    float factor = ((float) accTime) / TIME_THRESHOLD; 
    float nextAngle = angle + (angleTarget - angle) / 4.f; 
 
    angleFr = angle * (1.f - factor) + nextAngle * factor; 
} 

It uses the same principles as what we have been doing in previous examples, execute a single tick of logic every TIME_THRESHOLD milliseconds. The cube angle value will be interpolated between the current state and the next state depending on the time remaining to the execution of the next logic tick. This interpolated value will be stored on the angleFr variable.

We have also done some changes to the onSurfaceChanged to use the perspective projection mode instead of using Matrix.frustrumM. The latter defines the six clipping planes: near, far, top, bottom, left, and right. However, using Matrix.perspective allows us to define the projection matrix in terms of the camera field of view angle and two clipping planes: near and far. It might be handier in some situations, but at the end of the day, both methods achieve the same objective:

@Override 
public void onSurfaceChanged(GL10 unused, int width, int height) { 
    GLES20.glViewport(0, 0, width, height); 
 
    float ratio = (float) width / height; 
    Matrix.perspectiveM(mProjectionMatrix, 0, 90, ratio, 0.1f, 7.f); 
} 

Finally, we have got to do some changes to the onDrawFrame() method:

@Override 
public void onDrawFrame(GL10 unused) { 
animateLogic(); GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f); Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
Matrix.rotateM(mMVPMatrix, 0, angleFr, 0.f, 1.f, 0.f);
Matrix.rotateM(mMVPMatrix, 0, 5.f, 1.f, 0.f, 0.f); GLES20.glUseProgram(shaderProgram); int positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition"); GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer); int texCoordHandle = GLES20.glGetAttribLocation(shaderProgram, "aTex"); GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, texBuffer); int mMVPMatrixHandle = GLES20.glGetUniformLocation(shaderProgram,
"uMVPMatrix"); GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0); int texHandle = GLES20.glGetUniformLocation(shaderProgram, "sTex"); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); GLES20.glUniform1i(texHandle, 0); GLES20.glEnable(GLES20.GL_DEPTH_TEST); GLES20.glEnableVertexAttribArray(texHandle); GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glDrawElements( GLES20.GL_TRIANGLES, index.length, GLES20.GL_UNSIGNED_SHORT, indexBuffer); GLES20.glDisableVertexAttribArray(positionHandle); GLES20.glDisableVertexAttribArray(texHandle); GLES20.glDisable(GLES20.GL_DEPTH_TEST); }

Basically, the changes we have got to make are to call the animateLogic() method to execute any pending logic tick and use the interpolated angleFr variable for the rotation angle. If we run this example, we will get the same cube we had in Example25 but, in this case, we can control the animation by dragging horizontally on the screen. We have also got to remember, that there is no need to call invalidate or postInvalidate as when extending our class from GLSurfaceView and, unless specifically indicated, the screen will be constantly redrawn.