OpenGL ES实现TV端焦点选中动画

一、概述

在TV上,控件的选中状态是十分重要的,标示当前可以操作的位置。借助于本次学习OpenGL的时间,使用OpenGL实现了一个比较炫酷的焦点选中动画。Demo截图如下:


image.png

由于图片不能表达动画的效果,建议自己实现下功能在查看下具体效果。

二、基本工作内容

1.控件定位
2.呼吸效果
3.气泡动画
4.可视控件
5.性能优化
详细介绍之前先看下该效果在屏幕上绘制的层图。最底下是我们实现的UI控件,在控件的上层绘制了动画层。动画层有分为呼吸动画、气泡动画、可视控件。


image.png
控件定位:
  这个不是本期OpenGL的重点,每个人都有自己的方式,就不做赘述。
呼吸动画:
 呼吸动画就是蓝色的缩放动画,实现该动画就是修改OpenGL顶点着色器的显示范围。
  核心代码:
package com.stormdzh.openglanimation.customview.boder;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.stormdzh.openglanimation.R;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* @Description: 背景
* @Author: dzh
* @CreateDate: 2020-06-23 18:00
*/
public class BgRender {
private int backguardTexture = GLESTools.NO_TEXTURE;
private ShortBuffer drawIndecesBuffer;
private FloatBuffer shapeBuffer;
private FloatBuffer textrueBuffer;
//计算 物体实际的渲染位置
private float squareVertices[] = {
-0.8f, 0.8f,
-0.8f, -0.8f,
0.8f, -0.8f,
0.8f, 0.8f
};
private int glProgram;
private int glBgTextureLoc;
private int glBgPostionLoc;
private int glBgTextureCoordLoc;
private String vertexShader_filter = "" +
"attribute vec4 aBgPosition;\n" +
"attribute vec2 aBgTextureCoord;\n" +
"varying vec2 vBgTextureCoord;\n" +
"void main(){\n" +
" gl_Position= aBgPosition;\n" +
" vBgTextureCoord = aBgTextureCoord;\n" +
"}";
private String fragmentShader_filter = "" +
"precision mediump float;\n" +
"varying mediump vec2 vBgTextureCoord;\n" +
"uniform sampler2D uBgTexture;\n" +
"void main(){\n" +
" lowp vec4 c1 = texture2D(uBgTexture,vec2(vBgTextureCoord.x,1.0-vBgTextureCoord.y));\n" +
" lowp vec4 outputColor = c1;\n" +
" gl_FragColor = outputColor;\n" +
"}";
private Bitmap bitmap;
private Context mContext;
private float setp = 1.02f;
private boolean isSurfaceChanged = false;
public BgRender(Context context) {
mContext = context;
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initBuffer();
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
if (!isSurfaceChanged) {
isSurfaceChanged = true;
bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.focus_border_bg);
// bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.img_opgl_test);
backguardTexture = GLESTools.loadTexture(bitmap, GLESTools.NO_TEXTURE);
//设置环绕方式
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);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
glProgram = GLESTools.createProgram(vertexShader_filter, fragmentShader_filter);
GLES20.glUseProgram(glProgram);
glBgTextureLoc = GLES20.glGetUniformLocation(glProgram, "uBgTexture");
glBgPostionLoc = GLES20.glGetAttribLocation(glProgram, "aBgPosition");
glBgTextureCoordLoc = GLES20.glGetAttribLocation(glProgram, "aBgTextureCoord");
if (bitmap != null) {
bitmap.recycle();
bitmap = null;
}
}
}
public void onDrawFrame(GL10 gl) {
updateBgSize();
//draw bg
GLES20.glUseProgram(glProgram);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, backguardTexture);
GLES20.glUniform1i(glBgTextureLoc, 0);
GLHelper.enableVertex(glBgPostionLoc, glBgTextureCoordLoc, shapeBuffer, textrueBuffer);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawIndecesBuffer.limit(), GLES20.GL_UNSIGNED_SHORT, drawIndecesBuffer);
GLES20.glFinish();
GLHelper.disableVertex(glBgPostionLoc, glBgTextureCoordLoc);
}
public void updateBgSize() {
float v = shapeBuffer.get(0);
if (Math.abs(v) >= 2.2) {
for (int i = 0; i < 8; i++) {
shapeBuffer.put(i, squareVertices[i]);
}
} else {
for (int i = 0; i < 8; i++) {
shapeBuffer.put(i, shapeBuffer.get(i) * setp);
}
}
shapeBuffer.position(0);
}
private void initBuffer() { //初始化坐标顶点 与 纹理顶点
drawIndecesBuffer = GLHelper.getDrawIndecesBuffer();
shapeBuffer = GLHelper.getShapeVerticesBuffer();
textrueBuffer = GLHelper.getScreenTextureVerticesBuffer();
}

public void destroy() {
if (drawIndecesBuffer != null) {
drawIndecesBuffer.clear();
drawIndecesBuffer = null;
}
if (shapeBuffer != null) {
shapeBuffer.clear();
shapeBuffer = null;
}
if (textrueBuffer != null) {
textrueBuffer.clear();
textrueBuffer = null;
}
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
}
}
气泡动画:

气泡动画就是控制四周飘动的气泡。
核心代码:

package com.stormdzh.openglanimation.customview.boder;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.stormdzh.openglanimation.R;
import com.stormdzh.openglanimation.util.DeviceUtil;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* @Description: 气泡
* @Author: dzh
* @CreateDate: 2020-06-23 20:38
*/
public class BubbleRenderer {
private Context mContext;
private int ppTexture = GLESTools.NO_TEXTURE;
private ShortBuffer drawIndecesBuffer;
private FloatBuffer textrueBuffer;
//气泡的宽高 x,y
private float bubbleW, bubbleH;
private float bublePosition = 0.8f;
//泡泡 大小范围
private final float minScale = 0.5f;
private final float maxScale = 1.8f;

//气泡方向
private final int OrientationLeft = 1;
private final int OrientationRight = 2;
private final int OrientationTop = 3;
private final int OrientationBottom = 4;
//泡泡 随机飘动偏移范围 每帧
private final int Scope = 50;// dp
private int pixScope;
//相隔 多少像素 生成一个 气泡
private final int addPPUnit = 40;//dp
private int pixPPUnit;
//随机
private Random random = new Random();
//气泡列表
private List<BubbleItem> mBubbleList = new ArrayList<>();
private int mWidth, mHeight;
private Bitmap bitmap;
//变换矩阵
private int glProgram;
private int glPPTextureLoc;
private int glPPPostionLoc;
private int glPPTextureCoordLoc;
private String vertexShader_filter = "" +
"attribute vec4 aPPPosition;\n" +
"attribute vec2 aPPTextureCoord;\n" +
"varying vec2 vPPTextureCoord;\n" +
"void main(){\n" +
" gl_Position= aPPPosition;\n" +
" vPPTextureCoord = aPPTextureCoord;\n" +
"}";
private String fragmentShader_filter = "" +
"precision mediump float;\n" +
"varying mediump vec2 vPPTextureCoord;\n" +
"uniform sampler2D uPPTexture;\n" +
"void main(){\n" +
" lowp vec4 c1 = texture2D(uPPTexture,vec2(vPPTextureCoord.x,1.0-vPPTextureCoord.y));\n" +
" lowp vec4 outputColor = c1;\n" +
" gl_FragColor = outputColor;\n" +
"}";
public BubbleRenderer(Context context) {
this.mContext = context;
}
private void initBuffer() {
drawIndecesBuffer = GLHelper.getDrawIndecesBuffer();
textrueBuffer = GLHelper.getScreenTextureVerticesBuffer();
}
//生成一个气泡对象
public BubbleItem getBubbleItem(int orientation, int index) {
BubbleItem item = new BubbleItem();
item.orientation = orientation;
item.scaleSize = random.nextFloat() * (maxScale - minScale) + minScale;
item.index = index;
item.setPPLocation();
return item;
}
//设置气泡数组
private void initBubbleData() {
mBubbleList.clear();
int xNum = (int) Math.ceil((float) mWidth / (float) pixPPUnit) + 1;
int yNum = (int) Math.ceil((float) mHeight / (float) pixPPUnit) + 1;
for (int i = 0; i < yNum; i++) {
mBubbleList.add(getBubbleItem(OrientationLeft, i));
}
for (int i = 0; i < yNum; i++) {
mBubbleList.add(getBubbleItem(OrientationRight, i));
}
for (int i = 0; i < xNum; i++) {
mBubbleList.add(getBubbleItem(OrientationTop, i));
}
for (int i = 0; i < xNum; i++) {
mBubbleList.add(getBubbleItem(OrientationBottom, i));
}
}
private boolean isInit=false;
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
if(isInit) return;
isInit=true;
pixScope = DeviceUtil.dip2px(mContext, Scope);
pixPPUnit = DeviceUtil.dip2px(mContext, addPPUnit);
initBuffer();
// Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.focus_border_pp);
bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.focus_bubble_red);
ppTexture = GLESTools.loadTexture(bitmap, GLESTools.NO_TEXTURE);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
glProgram = GLESTools.createProgram(vertexShader_filter, fragmentShader_filter);
GLES20.glUseProgram(glProgram);
glPPTextureLoc = GLES20.glGetUniformLocation(glProgram, "uPPTexture");
glPPPostionLoc = GLES20.glGetAttribLocation(glProgram, "aPPPosition");
glPPTextureCoordLoc = GLES20.glGetAttribLocation(glProgram, "aPPTextureCoord");
if (bitmap != null) {
bitmap.recycle();
bitmap = null;
}
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
// bubbleW = 0.02f;
bubbleH = 0.02f;
if (mWidth != width) {
this.mWidth = width;
this.mHeight = height;
bubbleW = bubbleH * height / width;
if (mBubbleList != null) {
for (int i = 0; i < mBubbleList.size(); i++) {
mBubbleList.get(i).destroy();
}
mBubbleList.clear();
}
initBubbleData();
for (BubbleItem ppItem : mBubbleList) {
ppItem.setPPLocation();
}
}
}
public void onDrawFrame(GL10 gl) {
GLES20.glClearColor(1.0f, 1.0f, 1f, 1f);
GLES20.glUseProgram(glProgram);
GLES20.glEnableVertexAttribArray(glPPPostionLoc);
GLES20.glEnableVertexAttribArray(glPPTextureCoordLoc);
for (BubbleItem ppItem : mBubbleList) {
ppItem.drawPP();
}
GLES20.glFinish();
GLHelper.disableVertex(glPPPostionLoc, glPPTextureCoordLoc);
}
public void destroy() {
if (drawIndecesBuffer != null) {
drawIndecesBuffer.clear();
drawIndecesBuffer = null;
}
if (textrueBuffer != null) {
textrueBuffer.clear();
textrueBuffer = null;
}
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
if (random != null) {
random = null;
}
if (mBubbleList != null) {
destroyBubble();
mBubbleList.clear();
mBubbleList = null;
}
}
private void destroyBubble() {
for (int i = 0; i < mBubbleList.size(); i++) {
mBubbleList.get(i).destroy();
}
}
public class BubbleItem {
public int index;
public float scaleSize = 1.0f;
public int orientation = OrientationLeft;
private float ppVertices[];
private FloatBuffer shapeBuffer = GLHelper.getShapeVerticesBuffer();
private float x, y;
public void setPPLocation() {
//气泡顶点坐标
ppVertices = Arrays.copyOf(GLHelper.SquareVertices, GLHelper.SquareVertices.length);
ppVertices[0] = -bubbleW;
ppVertices[1] = bubbleH;
ppVertices[2] = -bubbleW;
ppVertices[3] = -bubbleH;
ppVertices[4] = bubbleW;
ppVertices[5] = -bubbleH;
ppVertices[6] = bubbleW;
ppVertices[7] = bubbleH;
bubbleScale(scaleSize);
int mideaHei = mHeight / 2;
int mideaWid = mWidth / 2;
if (orientation == OrientationLeft) {
float hy = index * pixPPUnit;
if (hy < mideaHei) {
x = -bublePosition;
y = hy / mideaHei;
if (y > bublePosition) {
y = bublePosition;
}
} else {
x = -bublePosition;
y = -(hy - mideaHei) / mideaHei;
if (y < -bublePosition) {
y = -bublePosition;
}
}
} else if (orientation == OrientationRight) {
float hy = index * pixPPUnit;
if (hy < mideaHei) {
x = bublePosition;
y = hy / mideaHei;
if (y > bublePosition) {
y = bublePosition;
}
} else {
x = bublePosition;
y = -(hy - mideaHei) / mideaHei;
if (y < -bublePosition) {
y = -bublePosition;
}
}
} else if (orientation == OrientationTop) {
float hx = index * pixPPUnit;
if (hx < mideaWid) {
x = -hx / mideaWid;
y = bublePosition;
if (x > -bublePosition) {
x = -bublePosition;
}
} else {
x = (hx - mideaWid) / mideaWid;
y = bublePosition;
if (x > bublePosition) {
x = bublePosition;
}
}
} else if (orientation == OrientationBottom) {
float hx = index * pixPPUnit;
if (hx < mideaWid) {
x = -hx / mideaWid;
y = -bublePosition;
if (x > -bublePosition) {
x = -bublePosition;
}
} else {
x = (hx - mideaWid) / mideaWid;
y = -bublePosition;
if (x > bublePosition) {
x = bublePosition;
}
}
}
bubblelocation(x, y);
shapeBuffer.clear();
shapeBuffer.put(ppVertices);
shapeBuffer.position(0);
}
private void bubbleScale(float scaleSize) {
for (int i = 0; i < ppVertices.length; i++) {
ppVertices[i] = ppVertices[i] * scaleSize;
}
}
private void bubblelocation(float x, float y) {
ppVertices[0] = ppVertices[0] + x;
ppVertices[1] = ppVertices[1] + y;
ppVertices[2] = ppVertices[2] + x;
ppVertices[3] = ppVertices[3] + y;
ppVertices[4] = ppVertices[4] + x;
ppVertices[5] = ppVertices[5] + y;
ppVertices[6] = ppVertices[6] + x;
ppVertices[7] = ppVertices[7] + y;
}
public void drawPP() {
float random = (float) (Math.random() / 500);
float randomD = (float) (Math.random() / 320);
int rd = Math.random() > 0.5 ? 1 : 0;
if (orientation == OrientationLeft) {
bubblelocation(-random, rd == 1 ? randomD : (0 - randomD));
} else if (orientation == OrientationRight) {
bubblelocation(random, rd == 1 ? randomD : (0 - randomD));
} else if (orientation == OrientationTop) {
bubblelocation(rd == 1 ? randomD : (0 - randomD), random);
} else if (orientation == OrientationBottom) {
bubblelocation(rd == 1 ? randomD : (0 - randomD), -random);
}
shapeBuffer.clear();
shapeBuffer.put(ppVertices);
shapeBuffer.position(0);
if (bubbleOut()) {
setPPLocation();
}

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, ppTexture);
GLES20.glUniform1i(glPPTextureLoc, 0);
GLHelper.updateVertex(glPPPostionLoc, glPPTextureCoordLoc, shapeBuffer, textrueBuffer);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawIndecesBuffer.limit(), GLES20.GL_UNSIGNED_SHORT, drawIndecesBuffer);

}
private boolean bubbleOut() {
for (int i = 0; i < ppVertices.length; i++) {
if (Math.abs(ppVertices[i]) >= 1) {
return true;
}
}
return false;
}

public void destroy() {
if (shapeBuffer != null) {
shapeBuffer.clear();
shapeBuffer = null;
}
ppVertices = null;
}
}
}
可视控件:

可视控件就是真实控件在OpenGL上绘制的图形。
核心代码:

public synchronized void updateRunderViewTexture() {
//drawview to Texture
Canvas canvas = rootViewSuface.lockCanvas(null);
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
drawView.draw(canvas);
rootViewSuface.unlockCanvasAndPost(canvas);
rootViewSufaceTexture.updateTexImage();
}
性能优化:

就是优化cpu和内存 ,目前cpu使用率的优化是降低onDrawFrame方法的调用次数,在测试中我把频率改为30毫秒一次,效果还行。内存优化就是需要及时的释放各种图片已经内存数据资源,需要调整方法的触发顺序和逻辑,既能满足实现功能,又能释放资源。

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

推荐阅读更多精彩内容