OpenGL ES 响应触摸事件

在前面的文章中对于 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 的坐标系内容。

View 坐标系

如上图所示,Android 中 View 的坐标系的原点位于竖直屏幕的左上角,这是一个二维坐标系。我们之所以能在二维的界面上模拟出三维的效果,这是借助了 Camera 来模拟人眼观测时构建的三维坐标系。如上图所示 Camera 三维坐标系是符合左手坐标系规则的,我们的旋转其实就是基于 Camera 坐标系中的 z 轴的旋转。

Camera 模拟三维观测

这里的 Camera 要和手机硬件上的相机区分开来,那个一般叫 Image Sensor 。具体可以参考这篇文章的描述:理解 Android 相机预览方向和拍照方向

在 Android 中,关于图像模拟三维旋转的问题,其实是有很多地方讲究的。推荐学习下扔物线的 HenCoder Android 开发进阶:自定义 View 1-4 Canvas 对绘制的辅助 系列文章,可以加深你对于 Camera 辅助绘制的机制。

这里简单介绍一下文章中提到的一个知识点,就是关于使用 Camera 来做三维变换时的旋转规律问题,Android 中的实际表现跟 OpenGL 的文档描述还是有一定区别的。

WX20170821-182449@2x.png

我们要实现图像平面旋转,其实就是绕 z 轴进行旋转。从上图可知,z 轴朝屏幕里面方向为正。我们面对屏幕时,是从屏幕正面向里面看, Camera 所处的位置应该在 z 轴的负坐标上(如图中黄色小点所示)。此时如果执行旋转方法,传入的旋转度数增大,图形会进行逆时针方向旋转,而减少旋转度数则变成顺时针方向旋转。

假如 Camera 所处的位置在 z 轴的正坐标上时,增大旋转度数则会进行顺时针方向旋转。

弄清楚旋转方向控制后,我们再来看看对于触摸事件要怎么处理。

处理触摸事件

在 Android 中我们知道可以通过 setOnTouchListener 方法设置对 View 触摸事件的监听, 对于 OpenGL 的处理也一样。如果下图所示就是触摸点与屏幕和 View 之间的关系,我们需要获取触摸时相对于屏幕的位置(通过 MotionEvent 的 getX()/getY() 获取 )与上次触摸时位置进行计算,判断移动的距离和方向。

在实际开发中,假设我们想要设计手指触摸屏幕进行逆时针旋转时图形也能跟着一起旋转,根据前面的 Camera 三维变换的了解,这就需要设置 Camera 处于 z 负轴上并不断增大旋转度数。我们判断手指是不是在进行逆时针旋转,有两个判断条件:

  1. 当触摸点处于 view 水平中线以上时,x 坐标要符合从左往右移动,计算前后两次触摸点在 x 轴上距离差值 dx 就是移动距离;而当触摸点处于水平中线以下时,x 坐标要符合从右往左移动,此时 x 是减小的,所以对 dx 取负数。

  2. 当触摸点处于 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 中查看 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 1. 概述 Android通过使用Open Graphics Library(OpenGL®)提供了对高性能2D和...
    小芸论阅读 7,058评论 1 19
  • 1 前言 一直想沿着图像处理这条线建立一套完整的理论知识体系,同时积累实际应用经验。因此有了从使用AVFounda...
    RichardJieChen阅读 5,642评论 5 12
  • Android framework提供了很多的标准工具来创建有吸引力的,功能强大的图形用户界面.但是如果你想要拥有...
    keith666阅读 8,209评论 5 34
  • 只因为,那个月华如水的夜晚;只因为,那晚满树灼灼的桃花;只因为,池水边你那灿若星辰的笑脸。我便,痴痴等了几百年。 ...
    高小花0218阅读 445评论 2 1