【OpenGL ES】基于ValueAnimator的旋转、平移、缩放动效

1 前言

ValueAnimator 基于 Choreographer 的 frame callback 机制,周期性(约16.7ms,与屏幕帧率相关)执行其 doAnimationFrame() 方法,回调监听器中相应方法,刷新 UI 实现动画效果。

ValueAnimator 是 Android 系统提供的时钟回调类,可以为其提供插值器,并注册 AnimatorUpdateListener 监听器、AnimatorUpdateListener 监听器。当调用 ValueAnimator 的 start() 方法后,系统会周期性执行 AnimatorUpdateListener 中的 onAnimationUpdate() 方法,用户可以通过重写 onAnimationUpdate() 方法实现动画每一帧的刷新。

读者如果对 OpenGL ES 不太熟悉,请回顾以下内容:

本文完整代码资源见→基于ValueAnimator的旋转、平移、缩放动效

项目目录如下:

2 案例

MainActivity.java

package com.zhyan8.anim.activity;

import android.animation.Animator;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import com.zhyan8.anim.R;
import com.zhyan8.anim.anim.MyAnimation;

public class MainActivity extends AppCompatActivity {
 MyAnimation mAnimation;
 ImageView mStarBtn;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 mStarBtn = findViewById(R.id.btn_star);
 }

 public void onClick(View view) {
 mAnimation = new MyAnimation(this);
 mAnimation.startAnimation(listener);
 }

 private Animator.AnimatorListener listener = new Animator.AnimatorListener() {
 @Override
 public void onAnimationStart(Animator animation) {
 mStarBtn.setVisibility(View.INVISIBLE);
 }

 @Override
 public void onAnimationEnd(Animator animation) {
 mStarBtn.setVisibility(View.VISIBLE);
 }

 @Override
 public void onAnimationCancel(Animator animation) {

 }

 @Override
 public void onAnimationRepeat(Animator animation) {

 }
 };
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 tools:context=".activity.MainActivity">
 <ImageView
 android:id="@+id/btn_star"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_gravity="center"
 android:src="@raw/star"
 android:onClick="onClick"/>
</FrameLayout>

MyAnimation.java

package com.zhyan8.anim.anim;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import com.zhyan8.anim.opengl.MyGLSurfaceView;
import com.zhyan8.anim.opengl.MyRender;

public class MyAnimation {
 private Context mContext;
 private ValueAnimator mValueAnimator;
 private WindowManager mWindowManager;
 private FrameLayout mRootView;
 private MyGLSurfaceView mAnimView;
 private WindowManager.LayoutParams mRootLayoutParams;
 private MyRender mRender;

 public MyAnimation(Context context) {
 mContext = context;
 mValueAnimator = ValueAnimator.ofFloat(0f, 1f);
 mValueAnimator.setInterpolator(new LinearInterpolator());
 mValueAnimator.addUpdateListener(mAnimatorUpdateListener);
 mValueAnimator.addListener(mAnimatorListener);
 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
 createView();
 }

 public void startAnimation(Animator.AnimatorListener listener) {
 mValueAnimator.addListener(listener);
 mValueAnimator.setDuration(2000);
 mValueAnimator.start();
 }

 private void createView() {
 mRootView = new FrameLayout(mContext);
 mRootView.setVisibility(View.INVISIBLE);
 getRootLayoutParams();
 getAnimView();
 mWindowManager.addView(mRootView, mRootLayoutParams);
 }

 private void getRootLayoutParams() {
 mRootLayoutParams = new WindowManager.LayoutParams();
 mRootLayoutParams.x = 0;
 mRootLayoutParams.y = 0;
 mRootLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
 mRootLayoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
 // 需要申请 android.permission.SYSTEM_ALERT_WINDOW 权限,并在设置中允许权限
 mRootLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 }

 private void getAnimView() {
 mAnimView = new MyGLSurfaceView(mContext);
 mRender = new MyRender(mContext.getResources());
 mAnimView.init(mRender);
 mAnimView.setVisibility(View.INVISIBLE);
 mRootView.addView(mAnimView);
 }

 private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
 @Override
 public void onAnimationUpdate(ValueAnimator animation) {
 float fraction = mValueAnimator.getAnimatedFraction();
 mRender.setScale(1 - fraction);
 mAnimView.requestRender();
 }
 };

 private Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() {
 @Override
 public void onAnimationStart(Animator animation) {
 mRootView.setVisibility(View.VISIBLE);
 mAnimView.setVisibility(View.VISIBLE);
 }

 @Override
 public void onAnimationEnd(Animator animation) {
 mRootView.removeView(mAnimView);
 mWindowManager.removeView(mRootView);
 mAnimView = null;
 mRootView = null;
 }

 @Override
 public void onAnimationCancel(Animator animation) {}

 @Override
 public void onAnimationRepeat(Animator animation) {}
 };
}

注意:由于创建的 mRootView 的 type 为 TYPE_APPLICATION_OVERLAY,需要在 AndroidManifest.xml 中 <manifest> 标签下申请如下权限,并且需要在 “设置>应用管理>权限” 中允许权限。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

MyGLSurfaceView.java

package com.zhyan8.anim.opengl;

import android.content.Context;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;

public class MyGLSurfaceView extends GLSurfaceView {
 public MyGLSurfaceView(Context context) {
 super(context);
 setEGLContextClientVersion(3);
 }

 public MyGLSurfaceView(Context context, AttributeSet attrs) {
 super(context, attrs);
 setEGLContextClientVersion(3);
 }

 public void init(MyRender render) {
 setEGLConfigChooser(8, 8, 8, 8, 16, 0);
 getHolder().setFormat(PixelFormat.TRANSLUCENT);
 setZOrderOnTop(true);
 setRenderer(render);
 setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
 }
}

MyRender.java

package com.zhyan8.anim.opengl;

import android.content.res.Resources;
import android.opengl.GLES30;
import android.opengl.GLSurfaceView;
import com.zhyan8.anim.model.Model;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class MyRender implements GLSurfaceView.Renderer {
 private Model mModel;

 public MyRender(Resources resources) {
 mModel = new Model(resources);
 }

 @Override
 public void onSurfaceCreated(GL10 gl, EGLConfig eglConfig) {
 //设置背景颜色
 GLES30.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 //启动深度测试
 gl.glEnable(GLES30.GL_DEPTH_TEST);
 //创建程序id
 mModel.onModelCreate();
 }

 @Override
 public void onSurfaceChanged(GL10 gl, int width, int height) {
 //设置视图窗口
 GLES30.glViewport(0, 0, width, height);
 mModel.onModelChange(width, height);
 }

 @Override
 public void onDrawFrame(GL10 gl) {
 //将颜色缓冲区设置为预设的颜色
 GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);
 //启用顶点的数组句柄
 GLES30.glEnableVertexAttribArray(0);
 GLES30.glEnableVertexAttribArray(1);
 //绘制模型
 mModel.onModelDraw();
 //禁止顶点数组句柄
 GLES30.glDisableVertexAttribArray(0);
 GLES30.glDisableVertexAttribArray(1);
 }

 public void setScale(float scale) {
 mModel.setScale(scale);
 }
}

Model.java

package com.zhyan8.anim.model;

import android.content.res.Resources;
import android.graphics.Point;
import android.opengl.GLES30;
import com.zhyan8.anim.R;
import com.zhyan8.anim.utils.ArraysUtils;
import com.zhyan8.anim.utils.ShaderUtils;
import com.zhyan8.anim.utils.TextureUtils;
import java.nio.FloatBuffer;

public class Model {
 private static final int TEXTURE_DIMENSION = 2; // 纹理坐标维度
 private static final int VERTEX_DIMENSION = 3; // 顶点坐标维度
 private Resources mResources;
 private MyTransform mTransform;
 private FloatBuffer mVertexBuffer;
 private FloatBuffer mTexturesBuffer;
 private int mTextureId;
 private int mProgramId;

 public Model(Resources resources) {
 mResources = resources;
 mTransform = new MyTransform();
 }

 // 模型创建
 public void onModelCreate() {
 mProgramId = ShaderUtils.createProgram(mResources, R.raw.vertex_shader, R.raw.fragment_shader);
 mTextureId = TextureUtils.loadTexture(mResources, R.raw.star);
 mTransform.onTransformCreate(mProgramId);
 getFloatBuffer();
 }

 // 模型参数变化
 public void onModelChange(int width, int height) {
 mTransform.onTransformChange(width, height);
 }

 // 模型绘制
 public void onModelDraw() {
 GLES30.glUseProgram(mProgramId);
 mTransform.onTransformExecute();
 GLES30.glVertexAttribPointer(0, VERTEX_DIMENSION, GLES30.GL_FLOAT, false, 0, mVertexBuffer);
 GLES30.glVertexAttribPointer(1, TEXTURE_DIMENSION, GLES30.GL_FLOAT, false, 0, mTexturesBuffer);
 //激活纹理
 GLES30.glActiveTexture(GLES30.GL_TEXTURE);
 //绑定纹理
 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId);
 //绘制贴图
 GLES30.glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, 4);
 }

 // 设置模型缩放比
 public void setScale(float scale) {
 mTransform.setScale(scale);
 }

 private float[] mVertex = new float[] {
 1f, 1f, 0f,     //V0
 -1f, 1f, 0f,    //V1
 -1f, -1f, 0f,   //V2
 1f, -1f, 0f     //V3
 };
 private float[] mTexture = new float[] {
 1f, 0f,     //V0
 0f, 0f,     //V1
 0f, 1.0f,   //V2
 1f, 1.0f    //V3
 };

 private void getFloatBuffer() {
 mVertexBuffer = ArraysUtils.getFloatBuffer(mVertex);
 mTexturesBuffer = ArraysUtils.getFloatBuffer(mTexture);
 }
}

MyTransform.java

package com.zhyan8.anim.model;

import android.opengl.GLES30;
import android.opengl.Matrix;

public class MyTransform {
 private int mProgramId;
 private int mMvpMatrixHandle;
 private float[] mModelMatrix;
 private float[] mViewMatrix;
 private float[] mProjectionMatrix;
 private float[] mMvpMatrix;
 private float mRatio;
 private float mScale = 1f;
 private float mRotate = 0f;
 private float[] mTranslate = new float[] {0f, 0f, 0f};
 private float mTranslateAngle = 0;

 // 变换创建
 public void onTransformCreate(int programId) {
 mProgramId = programId;
 mMvpMatrixHandle = GLES30.glGetUniformLocation(mProgramId, "mvpMatrix");
 mViewMatrix = getIdentityMatrix(16, 0);
 mMvpMatrix = getIdentityMatrix(16, 0);
 Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 6.4f, 0, 0, 0, 0, 1, 0);
 }

 // 变换参数变换
 public void onTransformChange(int width, int height) {
 mRatio = 1.0f * width / height;
 mProjectionMatrix = getIdentityMatrix(16, 0);
 Matrix.frustumM(mProjectionMatrix, 0, -mRatio, mRatio, -1, 1, 3, 20);
 }

 // 变换执行
 public void onTransformExecute() {
 setRotate();
 setTranslate();
 mModelMatrix = getIdentityMatrix(16, 0);
 Matrix.scaleM(mModelMatrix, 0, mScale, mScale, mScale);
 Matrix.translateM(mModelMatrix, 0, mTranslate[0], mTranslate[1], mTranslate[2]);
 Matrix.rotateM(mModelMatrix, 0, mRotate, 0, 0, 1);
 //计算MVP变换矩阵: mvpMatrix = projectionMatrix * viewMatrix * modelMatrix
 float[] tempMatrix = new float[16];
 Matrix.multiplyMM(tempMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
 Matrix.multiplyMM(mMvpMatrix, 0, mProjectionMatrix, 0, tempMatrix, 0);
 GLES30.glUniformMatrix4fv(mMvpMatrixHandle, 1, false, mMvpMatrix, 0);
 }

 // 设置模型缩放比
 public void setScale(float scale) {
 mScale = scale;
 }

 // 设置模型旋转比
 private void setRotate() {
 mRotate = (mRotate + 10f) % 360;
 }

 // 设置模型平移
 private void setTranslate() {
 mTranslateAngle = (mTranslateAngle + 0.2f) % 360;
 mTranslate[0] = (float) (Math.cos(mTranslateAngle));
 mTranslate[1] = (float) (Math.sin(mTranslateAngle));
 }

 private float[] getIdentityMatrix(int size, int offset) {
 float[] matrix = new float[size];
 Matrix.setIdentityM(matrix, offset);
 return matrix;
 }
}

ShaderUtils.java

package com.zhyan8.anim.utils;

import android.content.res.Resources;
import android.opengl.GLES30;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class ShaderUtils {
 //创建程序id
 public static int createProgram(Resources resources, int vertexShaderResId, int fragmentShaderResId) {
 final int vertexShaderId = compileShader(resources, GLES30.GL_VERTEX_SHADER, vertexShaderResId);
 final int fragmentShaderId = compileShader(resources, GLES30.GL_FRAGMENT_SHADER, fragmentShaderResId);
 return linkProgram(vertexShaderId, fragmentShaderId);
 }

 //通过外部资源编译着色器
 private static int compileShader(Resources resources, int type, int shaderId){
 String shaderCode = readShaderFromResource(resources, shaderId);
 return compileShader(type, shaderCode);
 }

 //通过代码片段编译着色器
 private static int compileShader(int type, String shaderCode){
 int shader = GLES30.glCreateShader(type);
 GLES30.glShaderSource(shader, shaderCode);
 GLES30.glCompileShader(shader);
 return shader;
 }

 //链接到着色器
 private static int linkProgram(int vertexShaderId, int fragmentShaderId) {
 final int programId = GLES30.glCreateProgram();
 //将顶点着色器加入到程序
 GLES30.glAttachShader(programId, vertexShaderId);
 //将片元着色器加入到程序
 GLES30.glAttachShader(programId, fragmentShaderId);
 //链接着色器程序
 GLES30.glLinkProgram(programId);
 return programId;
 }

 //从shader文件读出字符串
 private static String readShaderFromResource(Resources resources, int shaderId) {
 InputStream is = resources.openRawResource(shaderId);
 BufferedReader br = new BufferedReader(new InputStreamReader(is));
 String line;
 StringBuilder sb = new StringBuilder();
 try {
 while ((line = br.readLine()) != null) {
 sb.append(line);
 sb.append("\n");
 }
 br.close();
 } catch (Exception e) {
 e.printStackTrace();
 }
 return sb.toString();
 }
}

TextureUtils.java

package com.zhyan8.anim.utils;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES30;
import android.opengl.GLUtils;

public class TextureUtils {
 //加载纹理贴图
 public static int loadTexture(Resources resources, int resourceId) {
 BitmapFactory.Options options = new BitmapFactory.Options();
 options.inScaled = false;
 Bitmap bitmap = BitmapFactory.decodeResource(resources, resourceId, options);
 final int[] textureIds = new int[1];
 // 生成纹理id
 GLES30.glGenTextures(1, textureIds, 0);
 // 绑定纹理到OpenGL
 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]);
 GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
 GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
 // 加载bitmap到纹理中
 GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0);
 // 生成MIP贴图
 GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);
 // 取消绑定纹理
 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
 return textureIds[0];
 }
}

ArraysUtils.java

package com.zhyan8.anim.utils;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

public class ArraysUtils {
 public static FloatBuffer getFloatBuffer(float[] floatArr) {
 FloatBuffer fb = ByteBuffer.allocateDirect(floatArr.length * Float.BYTES)
 .order(ByteOrder.nativeOrder())
 .asFloatBuffer();
 fb.put(floatArr);
 fb.position(0);
 return fb;
 }
}

vertex_shader.glsl

#version 300 es
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec2 aTextureCoord;
uniform mat4 mvpMatrix;
out vec2 vTexCoord;
void main() {
 gl_Position  = mvpMatrix * vPosition;
 vTexCoord = aTextureCoord;
}

fragment_shader.glsl

#version 300 es
precision mediump float;
uniform sampler2D uTextureUnit;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {
 fragColor = texture(uTextureUnit,vTexCoord);
}

3 运行效果

声明:本文转自【OpenGL ES】基于ValueAnimator的旋转、平移、缩放动效

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

推荐阅读更多精彩内容