概述

前几天发现QQ音乐有个好玩的功用,为用户供给了多种 播映器主题,其间 原神 的主题让我眼前一亮:

中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器

当然,比如 换肤、主题 类的功用现已屡见不鲜,但这类沉溺式播映器的听歌体会的确不错。

见猎心喜,正好中秋立刻就到,我也测验整个 中秋主题音乐播映器 试试水。

全体思路有2点:

首要是技能方面,朴实的 ImageView 图层堆砌来完结,烘托效率太低OpenGL 是一个不错的技能方案(QQ应该也是这么完结的),顺便复习下图形学的常识。

其次是玩法上,干脆在基础的功用上加一些 更好玩的,比如为播映页设计多个图层,经过陀螺仪+图层联动完结的 裸眼3D 的视觉作用,边听歌边玩。后续还能够考虑经过制定 设计规范,让不同图层的UI元素,达成更多新奇好玩的 联动作用

说了这么多,最终作用如下所示,左侧展示录屏作用,右侧是裸眼3D作用:

中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器
中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器

详细完结

1. 裸眼3D原理

2年前自若的 《自若客APP裸眼3D作用的完结》 一文引发了社区的火热评论和实践,本着不重复造轮子的原则,这儿简单对原理介绍,感兴趣的读者可参阅上述链接。

裸眼 3D 作用的实质是——将整个图片结构分为 3 层:上层、中层、以及底层。在手机左右上下旋转时,上层和底层的图片呈相反的方向进行移动,中层则不动,在视觉上给人一种 3D 的感觉:

中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器

本文的作用是由以下四张图,由底至顶,顺次制作而成的:

布景 中景月亮 中景歌曲封面 远景
中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器
中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器
中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器
中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器

接下来,怎么感应手机的旋转状态,并将4层图片进行对应的移动呢?当然是使用设备自身供给的 传感器 了,经过传感器不断回调获取设备的旋转状态,对 UI 进行对应地烘托即可。

2. 为何挑选 OpenGL

GPU 更适合图形、图画的处理,裸眼3D作用中有很多的 旋转缩放位移 操作,都可在 java 层经过一个 矩阵 对几许改换进行描绘,经过 shader 小程序中交给 GPU 处理 ——理论上 OpenGL 的烘托性能比原生的 ImageView 更好。

凭借OpenGLAPI,烘托性能也符合预期,翻开 布局鸿沟GPU过渡制作 选项后,播映页烘托性能也仍然安稳,更不会增加布局层级的复杂度,直接证明了该方案 具备应用到实际生产项目的可行性

中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器

3.代码完结

本文重点是描绘 OpenGL 制作时的思路描绘,因而下文仅展示部分核心代码,对详细完结感兴趣的读者可参阅文末的链接。

3.1 制作静态图片

首要需求将4张图片(图片素材来源)顺次进行静态制作,这儿涉及很多 OpenGL API 的使用,不熟悉的读可略读本小节,以捋清思路为主。

首要看一下极点和片元着色器的 shader 代码,其界说了图画纹路是怎么在GPU中处理烘托的:

// 极点着色器代码
// 极点坐标
attribute vec4 av_Position;
// 纹路坐标
attribute vec2 af_Position;
uniform mat4 u_Matrix;
varying vec2 v_texPo;
void main() {
    v_texPo = af_Position;
    gl_Position =  u_Matrix * av_Position;
}
// 片元着色器代码
precision mediump float;
// 纹路坐标
varying vec2 v_texPo;
uniform sampler2D sTexture;
void main() {
    gl_FragColor=texture2D(sTexture, v_texPo);
}

界说好了 Shader ,接下来在 GLSurfaceView (能够了解为 OpenGL 中的画布) 创建时,初始化Shader小程序,并将图画纹路顺次加载到GPU中:

public class ZQRenderer implements GLSurfaceView.Renderer {
  @Override
  public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // 1.加载shader小程序
      mProgram = loadShaderWithResource(mContext, R.raw.projection_vertex_shader, R.raw.projection_fragment_shader);
      //...
      // 2. 顺次将切图纹路传入GPU
      this.texImageInner(R.drawable.icon_player_bg, mBackTextureId);
      this.texImageInner(R.drawable.icon_player_moon, mMidTextureId);
      this.texImageInner(R.drawable.icon_album_cover_nocturne, mCoverTextureId);
      this.texImageInner(R.drawable.icon_player_text, mFrontTextureId);
  }
}

接下来是界说视口以及投影矩阵,由于切图的比例各不相同,为了确保视觉作用,需求针对不同层级的图片,设置不同的正交投影战略。

public class ZQRenderer implements GLSurfaceView.Renderer {
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //设置巨细方位
        GLES20.glViewport(0, 0, width, height);
        Matrix.setIdentityM(mBgProjectionMatrix, 0);
        Matrix.setIdentityM(mMoonProjectionMatrix, 0);
        Matrix.setIdentityM(mCoverProjectionMatrix, 0);
        // 核算宽高比
        boolean isVertical = width < height;
        float screenRatio = (float) width / (float) height;
        // 设置投影矩阵
        // 1.深色布景图的投影矩阵,只需求铺全屏
        // 2.月亮和装修图的投影矩阵
        float ratio = (float) 1080 / (float) 1528;
        if (isVertical) {
            Matrix.orthoM(mMoonProjectionMatrix, 0, -1f, 1f, -1f / ratio, 1f / ratio, -1f, 1f);
        } else {
            Matrix.orthoM(mMoonProjectionMatrix, 0, -ratio, ratio, -1f, 1f, -1f, 1f);
        }
        // 3.歌曲封面图投影矩阵
        if (isVertical) {
            Matrix.orthoM(mCoverProjectionMatrix, 0, -1f, 1f, -1f / screenRatio, 1f / screenRatio, -1f, 1f);
        } else {
            Matrix.orthoM(mCoverProjectionMatrix, 0, -screenRatio, screenRatio, -1f, 1f, -1f, 1f);
        }
    }
}

最终就是 制作,读者需求了解,对于4层图画的烘托,其逻辑是基本共同的,差异仅仅有2点:图画本身不同 以及 图画的几许改换不同

public class ZQRenderer implements GLSurfaceView.Renderer {
  @Override
     public void onDrawFrame(GL10 gl) {
         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
         GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
         GLES20.glUseProgram(mProgram);
         this.updateMatrix();
         this.drawLayerInner(mBackTextureId, mTextureBuffer, mBackMatrix);  // 画布景
         this.drawLayerInner(mMidTextureId, mTextureBuffer, mMoonMatrix);   // 画月亮
         this.drawLayerInner(mCoverTextureId, mTextureBuffer, mCoverMatrix);  // 画封面
         this.drawLayerInner(mFrontTextureId, mTextureBuffer, mFrontMatrix);  // 画远景装修
     }
     private void texImageInner(@DrawableRes int drawableRes, int textureId) {
         //绑定纹路
         GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
         //盘绕方式
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
         //过滤方式
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
         GLES20.glEnable(GLES20.GL_BLEND);
         GLES20.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
         Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), drawableRes);
         GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
         bitmap.recycle();
     }
}

现在咱们完结了图画的 静态制作,接下来咱们需求接入 传感器,并引进不同层级图片各自的几许改换, 让图片动起来

3.2 让图片动起来

首要咱们需求对 Android 平台上的传感器进行注册,监听手机的旋转状态,并拿到手机 xy 轴的旋转视点。

// 2.1 注册传感器
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
mAcceleSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mSensorManager.registerListener(mSensorEventListener, mAcceleSensor, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(mSensorEventListener, mMagneticSensor, SensorManager.SENSOR_DELAY_GAME);
// 2.2 不断承受旋转状态
private final SensorEventListener mSensorEventListener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {
        // ... 省掉详细代码
        float[] values = new float[3];
        float[] R = new float[9];
        SensorManager.getRotationMatrix(R, null, mAcceleValues, mMageneticValues);
        SensorManager.getOrientation(R, values);
        // x轴的偏转视点
        float degreeX = (float) Math.toDegrees(values[1]);
        // y轴的偏转视点
        float degreeY = (float) Math.toDegrees(values[2]);
        // z轴的偏转视点
        float degreeZ = (float) Math.toDegrees(values[0]);
        // 拿到 xy 轴的旋转视点,进行矩阵改换
        updateMatrix(degreeX, degreeY);
    }
};

注意,由于咱们只需操控图画的左右和上下移动,因而,咱们只需关注设备本身 x 轴和 y 轴的偏转视点。但假如将图片直接进行位移操作,将会由于位移后图画的另一侧没有纹路数据,导致烘托结果有黑边现象,为了避免这个问题,咱们需求将图画默许从中心点进行扩大,确保图画移动的过程中,不会超出自身的鸿沟。

也就是说,咱们一开始进入时,看到的必定仅仅图片的部分区域。给每一个图层设置 scale,将图片进行扩大。显现窗口是固定的,那么一开始只能看到图片的正中方位。(中层能够不用,由于中层本身是不移动的,所以也不必扩大)

中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器

这儿的处理参阅自 Nayuta 的 这篇文章,内部现已将思路阐述的十分清晰,强烈建议读者进行阅读。

了解了这一点,咱们就能了解,裸眼 3D 的作用实际上就是对 不同层级的图画 进行 缩放位移 的改换,下面是别离获取几许改换的代码:

public class ZQRenderer implements GLSurfaceView.Renderer {
  private float[] mBgProjectionMatrix = new float[16];
  private float[] mMoonProjectionMatrix = new float[16];
  private float[] mCoverProjectionMatrix = new float[16];
  private float[] mBackMatrix = new float[16];
  private float[] mMoonMatrix = new float[16];
  private float[] mCoverMatrix = new float[16];
  private float[] mFrontMatrix = new float[16];
  // 封面图旋转一圈的时刻,单位秒.
  private static final long ROTATE_TIME = 20L;
  public static final long DELAY_INTERVAL = 1000 / (360 / ROTATE_TIME);
  /**
   * 陀螺仪数据回调,更新各个层级的改换矩阵.
   *
   * @param degreeX x轴旋转视点,图片应该上下移动
   * @param degreeY y轴旋转视点,图片应该左右移动
   */
   private void updateMatrix() {
       //  ----------  布景-蓝色底图  ----------
       Matrix.setIdentityM(mBackMatrix, 0);
       // 1.最大位移量
       float maxTransXY = MAX_VISIBLE_SIDE_BACKGROUND - 1f;
       // 2.本次的位移量
       float transX = ((maxTransXY) / MAX_TRANS_DEGREE_Y) * -mCurDegreeY;
       float transY = ((maxTransXY) / MAX_TRANS_DEGREE_X) * -mCurDegreeX;
       float[] backMatrix = new float[16];
       // 蓝色底图的投影矩阵,需求铺展全屏.
       Matrix.setIdentityM(mBgProjectionMatrix, 0);
       Matrix.setIdentityM(backMatrix, 0);
       Matrix.translateM(backMatrix, 0, transX, transY, 0f);                    // 2.平移
       Matrix.scaleM(backMatrix, 0, SCALE_BACK_GROUND, SCALE_BACK_GROUND, 1f);  // 1.缩放
       Matrix.multiplyMM(mBackMatrix, 0, mBgProjectionMatrix, 0, backMatrix, 0);  // 3.正交投影
       //  ----------  布景 -月亮  ----------
       Matrix.setIdentityM(mMoonMatrix, 0);
       float[] midMatrix = new float[16];
       Matrix.setIdentityM(midMatrix, 0);
//        Matrix.translateM(midMatrix, 0, transX, transY, 0f);                      // 2.平移,这行注释解开后,手机摇一摇,封面图和月亮也会有位移偏差.
       Matrix.scaleM(midMatrix, 0, SCALE_MOON_GROUND, SCALE_MOON_GROUND, 1.0f);  // 1.缩放
       Matrix.multiplyMM(mMoonMatrix, 0, mMoonProjectionMatrix, 0, midMatrix, 0);  // 3.正交投影
       // ---------  中景-歌曲封面  ----------
       Matrix.setIdentityM(mCoverMatrix, 0);
       float[] rotateMatrix = new float[16];
       float[] tranAndScale = new float[16];
       float[] coverMatrix = new float[16];
       Matrix.setIdentityM(rotateMatrix, 0);
       Matrix.setIdentityM(tranAndScale, 0);
       Matrix.setIdentityM(coverMatrix, 0);
       Matrix.scaleM(tranAndScale, 0, 0.565f, 0.58f, 1.0f);                   // 3.缩放,这儿的缩放参数是开发时,即时调整的,确保歌曲封面和月亮的巨细共同
       Matrix.translateM(tranAndScale, 0, 0.05f, 1.41f, 0f);                 // 2.平移,这儿的位移参数是开发时,即时调整的,确保歌曲封面和月亮的center方位在一起
       Matrix.setRotateM(rotateMatrix, 0, 360 - mCoverDegree, 0.0f, 0.0f, 1.0f);    // 1.旋转,顺时针
       Matrix.multiplyMM(coverMatrix, 0, tranAndScale, 0, rotateMatrix, 0);
       Matrix.multiplyMM(mCoverMatrix, 0, mCoverProjectionMatrix, 0, coverMatrix, 0);  // 4.正交投影
       //  ----------  远景-装修  ----------
       Matrix.setIdentityM(mFrontMatrix, 0);
       // 1.最大位移量
       maxTransXY = MAX_VISIBLE_SIDE_FOREGROUND - 1f;
       // 2.本次的位移量
       transX = ((maxTransXY) / MAX_TRANS_DEGREE_Y) * -mCurDegreeY;
       transY = ((maxTransXY) / MAX_TRANS_DEGREE_X) * -mCurDegreeX;
       float[] frontMatrix = new float[16];
       Matrix.setIdentityM(frontMatrix, 0);
       Matrix.translateM(frontMatrix, 0, -transX, -transY, 0f);         // 2.平移
       Matrix.scaleM(frontMatrix, 0, SCALE_FORE_GROUND, SCALE_FORE_GROUND, 1f);    // 1.缩放
       Matrix.multiplyMM(mFrontMatrix, 0, mMoonProjectionMatrix, 0, frontMatrix, 0);  // 3.正交投影
   }
}

布景、月亮、远景都很简单,只有 中景的歌曲封面 麻烦一些,首要歌曲封面要伴着歌曲进展做 旋转动画,其次,由于图片素材尺寸的原因,中心点要 位移 到和月亮相同的方位,最终 缩放 到和月亮相同的巨细完结重合。

小结

现在,咱们完结了图示作用的开发。

限于篇幅,文中代码以捋清思路为主,部分细节(如使用ExoPlayer播映《夜曲》,使用 Handler 不断发消息完结旋转动画、增加 低通滤波器 防止颤动等)没展示出来,感兴趣的小伙伴能够点击 这儿 查看源码。

关于我

Hello,我是 却把清梅嗅 ,假如您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 GitHub。

  • 我的Android学习系统
  • 关于文章纠错
  • 关于常识付费
  • 关于《反思》系列
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。