編輯:關於Android編程
在當前很多直播應用中,擁有給主播送禮物的功能,當用戶點擊贈送禮物後,視頻界面上會出現比較炫酷的禮物特效。這些特效,有的是用粒子效果做成的,但是更多的時用播放逐幀動畫實現的,本篇博客將會講解在Android下如何利用OpenGLES流暢的播放逐幀動畫。在本篇博客中的動畫素材,是從花椒直播中“借”出來的(只做學習交流用,應該不構成侵權吧:-D)。
有些朋友看到逐幀動畫可能會想,逐幀動畫還不容易嗎?Android中的動畫本來就支持逐幀動畫啊,不是分分鐘就能實現麼?沒錯,用Android的Animation的確很容易就實現了逐幀動畫。但是用Android的Animation實現動畫,當圖片要求較高時,播放會比較卡。為什麼呢?
Png圖片並不能在被直接用來播放動畫,它需要先被解碼成Bitmap,才能被繪制到屏幕上。而這個解碼是一個比較耗時的工作。而且解碼時間與手機、CPU工作狀態、Png圖片內容都有很大的關系。當圖片較小時,播放出來的逐幀動畫效果還不錯,但是當圖片較大時,比如720*720,解碼時間就往往需要100多ms,甚至會達到200ms以上。這個時間讓我們很難以接受。
那麼怎麼辦呢?限制動畫的是PNG解碼時間,而不是渲染時間,用OpenGL做渲染又有什麼用呢?是的,用OpenGL來播放PNG逐幀動畫,雖然比用Animation會有一些改善,但是並不能解決動畫播放卡頓的問題。(當初天真的以為Animation播放動畫是因為Animation用CPU繪制導致卡頓,然後改成用GPU來做,發現然並卵,這才把視線放到PNG解碼上了。)
既然是PNG解碼占用時間,那麼能不能直接用BMP格式存儲圖片,來做動畫呢?這樣解碼的時間就基本可以忽略了。那麼問題又來了,BMP是不進過壓縮的,一張720*720的PNG圖片大小轉成BMP就為720*720*4/1024=2025kb,那麼一秒25幀動畫,就要二十四五兆了。顯然是難以讓人接受的。那麼怎麼辦呢?以下為Android下OpenGLES實現逐幀動畫的方案比較:
根據上述分析,在Android中使用OpenGLES加載動畫:
方案4和方案5由於支持問題,直接排除了。 方案1可以使用 當前Android市場Android2.2以下設備基本不沒有了,Android2.2及以上到Android4.3下,占比15%左右。所以方案2與方案3之中,取方案2。選擇方案1與方案2進行對比。
針對測試用的60張png煙花圖片動畫進行量化分析(圖片大小為720*720,手機360F4):
PNG圖片總大小為4.88M,ETC總大小29.6M。 PNG IO+解碼耗時為15-40ms之間,與單張圖片大小有關。ETC不在CPU中解碼,只有IO時間,為4-10ms之間。(IO及解碼時間與CPU能力及狀態有關) 渲染時間二者基本一致。方案2文件總大小太大,針對這個問題,可采用zip壓縮紋理,加載時直接加載zip中的紋理文件。數據如下:
總大小7.05M IO+解碼時間為4-16ms。 渲染時間同不進行壓縮的ETC注:不同手機不同環境時間數據不同,此數據僅為PNG加載和壓縮紋理方式加載的對比。
這種方式,主要是針對PNG透明區域比較多的圖片,這樣壓縮紋理會比PNG大很多,ZIP壓縮一下可以壓縮的和PNG大小差不多。先直接說在實現過程中踩到的坑吧。
ETC1Util.createTexture(InputStream in)方法有坑。具體問題,後面貼代碼的時候說。
實現
壓縮紋理的加載,OpenGLES 提供了
GLES10.glCompressedTexImage2D(int target,int level,int internalformat,int width,int height, int border,int imageSize,java.nio.Buffer data) 方法,但是在Android中,可以用工具類ETC1Util提供的
loadTexture(int target, int level, int border,int fallbackFormat, int fallbackType, ETC1Texture texture) 方法來更簡單的使用。
這樣,我們就需要先得到一個ETC1Texture,而ETC1Util又提供了創建ETC1Texture的方法,上面說過,這個方法在使用中有點小坑,其源碼為:
public static ETC1Texture createTexture(InputStream input) throws IOException {
int width = 0;
int height = 0;
byte[] ioBuffer = new byte[4096];
{
if (input.read(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE) != ETC1.ETC_PKM_HEADER_SIZE) {
throw new IOException("Unable to read PKM file header.");
}
ByteBuffer headerBuffer = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE)
.order(ByteOrder.nativeOrder());
headerBuffer.put(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE).position(0);
if (!ETC1.isValid(headerBuffer)) {
throw new IOException("Not a PKM file.");
}
width = ETC1.getWidth(headerBuffer);
height = ETC1.getHeight(headerBuffer);
}
int encodedSize = ETC1.getEncodedDataSize(width, height);
ByteBuffer dataBuffer = ByteBuffer.allocateDirect(encodedSize).order(ByteOrder.nativeOrder());
for (int i = 0; i < encodedSize; ) {
int chunkSize = Math.min(ioBuffer.length, encodedSize - i);
if (input.read(ioBuffer, 0, chunkSize) != chunkSize) {
throw new IOException("Unable to read PKM file data.");
}
dataBuffer.put(ioBuffer, 0, chunkSize);
i += chunkSize;
}
dataBuffer.position(0);
return new ETC1Texture(width, height, dataBuffer);
}
修改為:
ETC1Util.ETC1Texture createTexture(InputStream input) throws IOException {
int width = 0;
int height = 0;
byte[] ioBuffer = new byte[4096];
{
if (input.read(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE) != ETC1.ETC_PKM_HEADER_SIZE) {
throw new IOException("Unable to read PKM file header.");
}
if(headerBuffer==null){
headerBuffer = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE)
.order(ByteOrder.nativeOrder());
}
headerBuffer.put(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE).position(0);
if (!ETC1.isValid(headerBuffer)) {
throw new IOException("Not a PKM file.");
}
width = ETC1.getWidth(headerBuffer);
height = ETC1.getHeight(headerBuffer);
}
int encodedSize = ETC1.getEncodedDataSize(width, height);
ByteBuffer dataBuffer = ByteBuffer.allocateDirect(encodedSize).order(ByteOrder.nativeOrder());
int len;
while ((len =input.read(ioBuffer))!=-1){
dataBuffer.put(ioBuffer,0,len);
}
dataBuffer.position(0);
return new ETC1Util.ETC1Texture(width, height, dataBuffer);
}
這個方法,是通過InputStream得到一個ETC1Texture,所以我們直接讀取Zip下的文件生成ETC1Texture就算完成了一大半工作了。讀取Zip下的文件代碼網上很容易找到,這裡直接貼出Demo中的ZipPkmReader:
public class ZipPkmReader {
private String path;
private ZipInputStream mZipStream;
private AssetManager mManager;
private ZipEntry mZipEntry;
private ByteBuffer headerBuffer;
public ZipPkmReader(Context context){
this(context.getAssets());
}
public ZipPkmReader(AssetManager manager){
this.mManager=manager;
}
public void setZipPath(String path){
Log.e("wuwang",path+" set");
this.path=path;
}
public boolean open(){
Log.e("wuwang",path+" open");
if(path==null)return false;
try {
if(path.startsWith("assets/")){
InputStream s=mManager.open(path.substring(7));
mZipStream=new ZipInputStream(s);
}else{
File f=new File(path);
Log.e("wuwang",path+" is File exists->"+f.exists());
mZipStream=new ZipInputStream(new FileInputStream(path));
}
return true;
} catch (IOException e) {
Log.e("wuwang","eee-->"+e.getMessage());
e.printStackTrace();
return false;
}
}
public void close(){
if(mZipStream!=null){
try {
mZipStream.closeEntry();
mZipStream.close();
} catch (Exception e) {
e.printStackTrace();
}
if(headerBuffer!=null){
headerBuffer.clear();
headerBuffer=null;
}
}
}
private boolean hasElements(){
try {
if(mZipStream!=null){
mZipEntry=mZipStream.getNextEntry();
if(mZipEntry!=null){
return true;
}
Log.e("wuwang","mZip entry null");
}
} catch (IOException e) {
Log.e("wuwang","err dd->"+e.getMessage());
e.printStackTrace();
}
return false;
}
public InputStream getNextStream(){
if(hasElements()){
return mZipStream;
}
return null;
}
public ETC1Util.ETC1Texture getNextTexture(){
if(hasElements()){
try {
ETC1Util.ETC1Texture e= createTexture(mZipStream);
return e;
} catch (IOException e1) {
Log.e("wuwang","err->"+e1.getMessage());
e1.printStackTrace();
}
}
return null;
}
private ETC1Util.ETC1Texture createTexture(InputStream input) throws IOException {
int width = 0;
int height = 0;
byte[] ioBuffer = new byte[4096];
{
if (input.read(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE) != ETC1.ETC_PKM_HEADER_SIZE) {
throw new IOException("Unable to read PKM file header.");
}
if(headerBuffer==null){
headerBuffer = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE)
.order(ByteOrder.nativeOrder());
}
headerBuffer.put(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE).position(0);
if (!ETC1.isValid(headerBuffer)) {
throw new IOException("Not a PKM file.");
}
width = ETC1.getWidth(headerBuffer);
height = ETC1.getHeight(headerBuffer);
}
int encodedSize = ETC1.getEncodedDataSize(width, height);
ByteBuffer dataBuffer = ByteBuffer.allocateDirect(encodedSize).order(ByteOrder.nativeOrder());
int len;
while ((len =input.read(ioBuffer))!=-1){
dataBuffer.put(ioBuffer,0,len);
}
dataBuffer.position(0);
return new ETC1Util.ETC1Texture(width, height, dataBuffer);
}
}
Shader直接使用Mali 官網上方法2提供的Shader即可,然後在開啟一個定時器,定時requestRender,加載下一幀壓縮紋理。動畫播放就基本完成了。為了簡便,Demo中直接在在GL線程中Sleep然後requestRender的。
這裡也貼上Shader的代碼吧。
頂點Shader:
attribute vec4 vPosition;
attribute vec2 vCoord;
varying vec2 aCoord;
uniform mat4 vMatrix;
void main(){
aCoord = vCoord;
gl_Position = vMatrix*vPosition;
}
片元Shader:
precision mediump float;
varying vec2 aCoord;
uniform sampler2D vTexture;
uniform sampler2D vTextureAlpha;
void main() {
vec4 color=texture2D( vTexture, aCoord);
color.a=texture2D(vTextureAlpha,aCoord).r;
gl_FragColor = color;
}
可以看到,在片元著色器中,我們需要兩個Texture,一個包含著原來PNG圖片的RGB信息,一個包含著原PNG圖片的Alpha信息。這些信息並不是完全和原PNG信息相同的,壓縮紋理在色彩上會有一些損失。
片元著色器中用到了兩個采樣器,紋理傳入的代碼為:
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[0]);
ETC1Util.loadTexture(GLES20.GL_TEXTURE_2D,0,0,GLES20.GL_RGB,GLES20
.GL_UNSIGNED_SHORT_5_6_5,t);
GLES20.glUniform1i(mHTexture,0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[1]);
ETC1Util.loadTexture(GLES20.GL_TEXTURE_2D,0,0,GLES20.GL_RGB,GLES20
.GL_UNSIGNED_SHORT_5_6_5,tAlpha);
GLES20.glUniform1i(mGlHAlpha,1);
其他地方就和之前渲染圖片差不多了。
源碼
所有的代碼全部在一個項目中,托管在Github上——Android OpenGLES 2.0系列博客的Demo
GalleryPick 是 Android 自定義相冊,實現了拍照、圖片選擇(單選/多選)、裁剪、ImageLoader無綁定 任由開發者選擇圖片展示 Gif展示 Ga
其實對於apk包的安裝,4.4和之前版本沒大的差別。Android中app安裝主要有以下幾種情況:系統啟動時安裝,adb命令安裝,Google
在Android開發中,大部分控件都有visibility這個屬性,其屬性有3個分別為“visible ”、“invisible”、“gone”。主要用來設置控制控件的顯
ViewPager + Fragment + TabPageIndicator 實現標簽欄主界面。效果圖:1、頭部的布局文件,這個很簡單: android:la