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

当然,比如 换肤、主题 类的功用现已屡见不鲜,但这类沉溺式播映器的听歌体会的确不错。
见猎心喜,正好中秋立刻就到,我也测验整个 中秋主题音乐播映器 试试水。
全体思路有2点:
首要是技能方面,朴实的 ImageView
图层堆砌来完结,烘托效率太低,OpenGL
是一个不错的技能方案(QQ
应该也是这么完结的),顺便复习下图形学的常识。
其次是玩法上,干脆在基础的功用上加一些 更好玩的,比如为播映页设计多个图层,经过陀螺仪+图层联动完结的 裸眼3D 的视觉作用,边听歌边玩。后续还能够考虑经过制定 设计规范,让不同图层的UI
元素,达成更多新奇好玩的 联动作用。
说了这么多,最终作用如下所示,左侧展示录屏作用,右侧是裸眼3D作用:


详细完结
1. 裸眼3D原理
2年前自若的 《自若客APP裸眼3D作用的完结》 一文引发了社区的火热评论和实践,本着不重复造轮子的原则,这儿简单对原理介绍,感兴趣的读者可参阅上述链接。
裸眼 3D
作用的实质是——将整个图片结构分为 3
层:上层、中层、以及底层。在手机左右上下旋转时,上层和底层的图片呈相反的方向进行移动,中层则不动,在视觉上给人一种 3D
的感觉:

本文的作用是由以下四张图,由底至顶,顺次制作而成的:
布景 | 中景月亮 | 中景歌曲封面 | 远景 |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
接下来,怎么感应手机的旋转状态,并将4层图片进行对应的移动呢?当然是使用设备自身供给的 传感器 了,经过传感器不断回调获取设备的旋转状态,对 UI
进行对应地烘托即可。
2. 为何挑选 OpenGL
GPU
更适合图形、图画的处理,裸眼3D作用中有很多的 旋转、缩放 和 位移 操作,都可在 java
层经过一个 矩阵 对几许改换进行描绘,经过 shader
小程序中交给 GPU
处理 ——理论上 OpenGL
的烘托性能比原生的 ImageView
更好。
凭借OpenGL
的API
,烘托性能也符合预期,翻开 布局鸿沟 和 GPU过渡制作 选项后,播映页烘托性能也仍然安稳,更不会增加布局层级的复杂度,直接证明了该方案 具备应用到实际生产项目的可行性:

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
,将图片进行扩大。显现窗口是固定的,那么一开始只能看到图片的正中方位。(中层能够不用,由于中层本身是不移动的,所以也不必扩大)

这儿的处理参阅自 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学习系统
- 关于文章纠错
- 关于常识付费
- 关于《反思》系列