在前面的文章中对于 OpenGL ES 在 Android 应用中的视图绘制整体流程和视图动作添加进行大致地介绍,如果你对这些概念还有些不熟悉,可以回头再看一下前面的文章。文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。
上篇文章中我们让一个三角形进行旋转动作,这是让 OpenGL 视图根据预设的程序进行动作。但是如果想要让 OpenGL ES 的图形对象响应用户的行为,就必须让 OpenGL ES 应用可以支持触控交互。为了响应用户的 touch 事件,就必须要在 GLSurfaceView 中实现 onTouchEvent() 方法来监听处理触摸事件。
这篇文章将介绍如何监听触控事件,让用户可以手动控制旋转一个 OpenGL ES 图形对象。
配置触摸监听器
为了让我们的 OpenGL ES 应用响应触控事件,我们必须实现 GLSurfaceView 类中的 onTouchEvent() 方法。下面的例子展示了如何监听 MotionEvent.ACTION_MOVE 事件,并将事件转换为形状旋转的角度:
public class MyGLSurfaceView05 extends GLSurfaceView {
// 旋转变换的比例因子
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
...
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
// getX/getY 触摸点相对于其所在view组件坐标系的坐标(以触摸点所在 view 的左上角为原点的坐标系)
// getRawX/getRawY 触摸点相对于屏幕默认坐标系的坐标(以屏幕的左上角为原点的坐标系)
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX; // 从左往有滑动时: x 值增大,dx 为正;反之则否。
float dy = y - mPreviousY; // 从上往下滑动时: y 值增大,dy 为正;反之则否。
// OpenGL 绕 z 轴的旋转符合左手定则,即 z 轴朝屏幕里面为正。
// 用户面对屏幕时,是从正面向里看(此时 camera 所处的 z 坐标位置为负数),当旋转度数增大时会进行逆时针旋转。
// 逆时针旋转判断条件1:触摸点处于 view 水平中线以下时,x 坐标应该要符合从右往左移动,此时 x 是减小的,所以 dx 取负数。
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// 逆时针旋转判断条件2:触摸点处于 view 竖直中线以左时,y 坐标应该要符合从下往上移动,此时 y 是减小的,所以 dy 取负数。
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(mRenderer.getAngle() + ((dx + dy) * TOUCH_SCALE_FACTOR));
// 在计算旋转角度后,调用requestRender()来告诉渲染器现在可以进行渲染了
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
这里为了便于理解触摸事件和图形旋转角度两者的关系,我们来看看这个过程到底做了什么,在此之前先来回顾一下 Android 中 View 的坐标系内容。
如上图所示,Android 中 View 的坐标系的原点位于竖直屏幕的左上角,这是一个二维坐标系。我们之所以能在二维的界面上模拟出三维的效果,这是借助了 Camera 来模拟人眼观测时构建的三维坐标系。如上图所示 Camera 三维坐标系是符合左手坐标系规则的,我们的旋转其实就是基于 Camera 坐标系中的 z 轴的旋转。
这里的 Camera 要和手机硬件上的相机区分开来,那个一般叫 Image Sensor 。具体可以参考这篇文章的描述:理解 Android 相机预览方向和拍照方向
在 Android 中,关于图像模拟三维旋转的问题,其实是有很多地方讲究的。推荐学习下扔物线的 HenCoder Android 开发进阶:自定义 View 1-4 Canvas 对绘制的辅助 系列文章,可以加深你对于 Camera 辅助绘制的机制。
这里简单介绍一下文章中提到的一个知识点,就是关于使用 Camera 来做三维变换时的旋转规律问题,Android 中的实际表现跟 OpenGL 的文档描述还是有一定区别的。
我们要实现图像平面旋转,其实就是绕 z 轴进行旋转。从上图可知,z 轴朝屏幕里面方向为正。我们面对屏幕时,是从屏幕正面向里面看, Camera 所处的位置应该在 z 轴的负坐标上(如图中黄色小点所示)。此时如果执行旋转方法,传入的旋转度数增大,图形会进行逆时针方向旋转,而减少旋转度数则变成顺时针方向旋转。
假如 Camera 所处的位置在 z 轴的正坐标上时,增大旋转度数则会进行顺时针方向旋转。
弄清楚旋转方向控制后,我们再来看看对于触摸事件要怎么处理。
处理触摸事件
在 Android 中我们知道可以通过 setOnTouchListener 方法设置对 View 触摸事件的监听, 对于 OpenGL 的处理也一样。如果下图所示就是触摸点与屏幕和 View 之间的关系,我们需要获取触摸时相对于屏幕的位置(通过 MotionEvent 的 getX()/getY() 获取 )与上次触摸时位置进行计算,判断移动的距离和方向。
在实际开发中,假设我们想要设计手指触摸屏幕进行逆时针旋转时图形也能跟着一起旋转,根据前面的 Camera 三维变换的了解,这就需要设置 Camera 处于 z 负轴上并不断增大旋转度数。我们判断手指是不是在进行逆时针旋转,有两个判断条件:
当触摸点处于 view 水平中线以上时,x 坐标要符合从左往右移动,计算前后两次触摸点在 x 轴上距离差值 dx 就是移动距离;而当触摸点处于水平中线以下时,x 坐标要符合从右往左移动,此时 x 是减小的,所以对 dx 取负数。
当触摸点处于 view 竖直中线以有时,y 坐标要符合从上往下移动,计算前后两次触摸点在 y 轴上距离差值 dy 就是移动距离。而当触摸点处于 view 竖直中线以左时,y 坐标应该要符合从下往上移动,此时 y 是减小的,所以对 dy 取负数。
计算出正确的触摸滑动距离后,我们再设计一个旋转度数因子与之相乘,以达到实际旋转时的角度大小。注意在计算旋转角度后,要调用 requestRender()来告诉渲染器现在可以进行渲染了。这种办法对于这个例子来说是最有效的,因为图形并不需要重新绘制,除非有一个旋转角度的变化。当然,为了能够真正实现执行效率的提高,记得使用 setRenderMode() 方法以保证渲染器仅在数据发生变化时才会重新绘制图形,所以确认在代码中配置了下面的渲染模式:
public MyGLSurfaceView05(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
设置旋转角度
要实现跟随触摸手势实现动态改变图形旋转角度,我们需要在渲染器中添加一个 public 成员变量。由于渲染器代码运行在一个独立的线程中(非主UI线程),我们必须同时将该变量声明为 volatile (使其多线程可见)。
public class MyGLRenderer5 implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
应用旋转效果
同上面文章提到的处理方式一样,把自动旋转时的度数换成上面的设置 mAngle 即可。
@Override
public void onDrawFrame(GL10 gl) {
// 每次先清除已有绘制内容,避免旋转时绘制内容叠加
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
float[] scratch = new float[16];
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Create a rotation transformation for the triangle
// 设置 Camera 位置(0, 0, -1.0f),此时处于 z 负轴上,模拟人眼一样从屏幕正面看向里面
// 设置旋转角度 mAngle,此时如果增大旋转角度图形应该是逆时针绕 z 轴旋转
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw shape
mTriangle.draw(scratch);
}
当完成了上述步骤,我们就可以运行这个程序,并通过手指在屏幕上的滑动旋转三角形了:
上图中可以看到,当手势进行顺时针旋转时,图形却在进行逆时针旋转。这不是代码出错了,而是设置 Camera 位置变化后的正常效果,不信你可以将渲染器中的代码改成:Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);
后再试试效果。如果你还没有理解这是为什么,建议可以再好好地回顾一下本篇文章中提到的内容。
关于 Android 中实现 OpenGL ES 基础图形绘制的学习到这篇文章为止就算到此为止了,这个系列主要是参考了Google 官方的学习教程 Displaying Graphics with OpenGL ES,有兴趣的可以去看看原版内容加深理解,后续的文章会尽可能结合实际开发中的问题进行探讨学习。
文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。