Android上使用OpenGLES2.0顯示YUV資料
樓主收到這樣的任務,在Android上用OpenGLES來顯示YUV影象,之所以這樣做,是因為:
1.Android本身也不能直接顯示YUV影象,YUV轉成RGB還是必要的;
2.YUV手動轉RGB會佔用大量的CPU資源,如果以這樣的形式播放視訊,手機會很熱,所以我們儘量讓GPU來做這件事;
3.OpenGLES是Android整合到自身框架裡的第三方庫,它有很多的可取之處。
博主的C/C++不是很好,所以整個過程是在Java層實現的,大家見笑,我主要參考(但不限於)以下文章,十分感謝這些朋友的分享:
1. http://blog.csdn.net/xiaoguaihai/article/details/8672631
2.http://chenshun87.blog.163.com/blog/static/18859389201232011727615/
3.http://blog.csdn.net/ypist/article/details/8950903
4.http://blog.csdn.net/wanglang3081/article/details/8480281
5.http://blog.csdn.net/xdljf/article/details/7178620
一、首先我先說一下這個解決方案是怎麼執行的,給大家一個概念
1.顯示在哪 -> GLSurfaceVIew
2.誰來把資料貼到GLSurfaceVIew上 -> Renderer
3.誰來負責YUV資料轉換成RGB -> GL中的Program/Shader
一句話說明白就是:GL的Program/Shader把使用者傳過來的YUV資料,轉換成RGB資料後,通過Renderer貼在GLSurfaceView上。
二、怎麼檢查你的手機是不是支援GLES2.0呢,使用下面的程式碼段就行了:
一般的手機,都是會支援GLES2.0的,大家不必擔心。public static boolean detectOpenGLES20(Context context) { ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo info = am.getDeviceConfigurationInfo(); return (info.reqGlEsVersion >= 0x20000); }
三、開搞
A 先要有一個GLSurfaceView,把它放入你的佈局中就好了。
找到這個傢伙,對它進行簡單的設定,併為它設定一個Renderer。
Renderer的作用就是在GLSurfaceView上畫出影象。
mGLSurface = (GLFrameSurface) findViewById(R.id.glsurface);
mGLSurface.setEGLContextClientVersion(2);
mGLFRenderer = new GLFrameRenderer(this, mGLSurface);
mGLSurface.setRenderer(mGLFRenderer);
B 再就是看下GLFrameRenderer怎麼來寫了
public class GLFrameRenderer implements Renderer {
private ISimplePlayer mParentAct; //請無視之
private GLSurfaceView mTargetSurface;
private GLProgram prog = new GLProgram(0);
private int mVideoWidth = -1, mVideoHeight = -1;
private ByteBuffer y;
private ByteBuffer u;
private ByteBuffer v;
public GLFrameRenderer(ISimplePlayer callback, GLSurfaceView surface) {
mParentAct = callback; //請無視之
mTargetSurface = surface;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Utils.LOGD("GLFrameRenderer :: onSurfaceCreated");
if (!prog.isProgramBuilt()) {
prog.buildProgram();
Utils.LOGD("GLFrameRenderer :: buildProgram done");
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Utils.LOGD("GLFrameRenderer :: onSurfaceChanged");
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
synchronized (this) {
if (y != null) {
// reset position, have to be done
y.position(0);
u.position(0);
v.position(0);
prog.buildTextures(y, u, v, mVideoWidth, mVideoHeight);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
prog.drawFrame();
}
}
}
/**
* this method will be called from native code, it happens when the video is about to play or
* the video size changes.
*/
public void update(int w, int h) {
Utils.LOGD("INIT E");
if (w > 0 && h > 0) {
if (w != mVideoWidth && h != mVideoHeight) {
this.mVideoWidth = w;
this.mVideoHeight = h;
int yarraySize = w * h;
int uvarraySize = yarraySize / 4;
synchronized (this) {
y = ByteBuffer.allocate(yarraySize);
u = ByteBuffer.allocate(uvarraySize);
v = ByteBuffer.allocate(uvarraySize);
}
}
}
mParentAct.onPlayStart(); //請無視之
Utils.LOGD("INIT X");
}
/**
* this method will be called from native code, it's used for passing yuv data to me.
*/
public void update(byte[] ydata, byte[] udata, byte[] vdata) {
synchronized (this) {
y.clear();
u.clear();
v.clear();
y.put(ydata, 0, ydata.length);
u.put(udata, 0, udata.length);
v.put(vdata, 0, vdata.length);
}
// request to render
mTargetSurface.requestRender();
}
}
程式碼很簡單,Renderer主要處理這麼幾個事:
1.Surface create的時候,我初始化了一些需要用到的Program/Shader,因為馬上就要用到它們了;
2.Surface change的時候,重置一下畫面;
3.onDrawFrame()時,把資料真正地“畫”上去;
4.至於兩個update方法,是用來把影象的寬高/資料傳過來的。
C 看GLProgram是怎麼寫的,它的作用是向Renderer提供計算單元,你所有對資料的處理,都在這兒了。
public boolean isProgramBuilt() {
return isProgBuilt;
}
public void buildProgram() {
createBuffers(_vertices, coordVertices);
if (_program <= 0) {
_program = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
}
Utils.LOGD("_program = " + _program);
/*
* get handle for "vPosition" and "a_texCoord"
*/
_positionHandle = GLES20.glGetAttribLocation(_program, "vPosition");
Utils.LOGD("_positionHandle = " + _positionHandle);
checkGlError("glGetAttribLocation vPosition");
if (_positionHandle == -1) {
throw new RuntimeException("Could not get attribute location for vPosition");
}
_coordHandle = GLES20.glGetAttribLocation(_program, "a_texCoord");
Utils.LOGD("_coordHandle = " + _coordHandle);
checkGlError("glGetAttribLocation a_texCoord");
if (_coordHandle == -1) {
throw new RuntimeException("Could not get attribute location for a_texCoord");
}
/*
* get uniform location for y/u/v, we pass data through these uniforms
*/
_yhandle = GLES20.glGetUniformLocation(_program, "tex_y");
Utils.LOGD("_yhandle = " + _yhandle);
checkGlError("glGetUniformLocation tex_y");
if (_yhandle == -1) {
throw new RuntimeException("Could not get uniform location for tex_y");
}
_uhandle = GLES20.glGetUniformLocation(_program, "tex_u");
Utils.LOGD("_uhandle = " + _uhandle);
checkGlError("glGetUniformLocation tex_u");
if (_uhandle == -1) {
throw new RuntimeException("Could not get uniform location for tex_u");
}
_vhandle = GLES20.glGetUniformLocation(_program, "tex_v");
Utils.LOGD("_vhandle = " + _vhandle);
checkGlError("glGetUniformLocation tex_v");
if (_vhandle == -1) {
throw new RuntimeException("Could not get uniform location for tex_v");
}
isProgBuilt = true;
}
/**
* build a set of textures, one for Y, one for U, and one for V.
*/
public void buildTextures(Buffer y, Buffer u, Buffer v, int width, int height) {
boolean videoSizeChanged = (width != _video_width || height != _video_height);
if (videoSizeChanged) {
_video_width = width;
_video_height = height;
Utils.LOGD("buildTextures videoSizeChanged: w=" + _video_width + " h=" + _video_height);
}
// building texture for Y data
if (_ytid < 0 || videoSizeChanged) {
if (_ytid >= 0) {
Utils.LOGD("glDeleteTextures Y");
GLES20.glDeleteTextures(1, new int[] { _ytid }, 0);
checkGlError("glDeleteTextures");
}
// GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
checkGlError("glGenTextures");
_ytid = textures[0];
Utils.LOGD("glGenTextures Y = " + _ytid);
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, _ytid);
checkGlError("glBindTexture");
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, _video_width, _video_height, 0,
GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, y);
checkGlError("glTexImage2D");
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
// building texture for U data
if (_utid < 0 || videoSizeChanged) {
if (_utid >= 0) {
Utils.LOGD("glDeleteTextures U");
GLES20.glDeleteTextures(1, new int[] { _utid }, 0);
checkGlError("glDeleteTextures");
}
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
checkGlError("glGenTextures");
_utid = textures[0];
Utils.LOGD("glGenTextures U = " + _utid);
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, _utid);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, _video_width / 2, _video_height / 2, 0,
GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, u);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
// building texture for V data
if (_vtid < 0 || videoSizeChanged) {
if (_vtid >= 0) {
Utils.LOGD("glDeleteTextures V");
GLES20.glDeleteTextures(1, new int[] { _vtid }, 0);
checkGlError("glDeleteTextures");
}
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
checkGlError("glGenTextures");
_vtid = textures[0];
Utils.LOGD("glGenTextures V = " + _vtid);
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, _vtid);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, _video_width / 2, _video_height / 2, 0,
GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, v);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
}
/**
* render the frame
* the YUV data will be converted to RGB by shader.
*/
public void drawFrame() {
GLES20.glUseProgram(_program);
checkGlError("glUseProgram");
GLES20.glVertexAttribPointer(_positionHandle, 2, GLES20.GL_FLOAT, false, 8, _vertice_buffer);
checkGlError("glVertexAttribPointer mPositionHandle");
GLES20.glEnableVertexAttribArray(_positionHandle);
GLES20.glVertexAttribPointer(_coordHandle, 2, GLES20.GL_FLOAT, false, 8, _coord_buffer);
checkGlError("glVertexAttribPointer maTextureHandle");
GLES20.glEnableVertexAttribArray(_coordHandle);
// bind textures
GLES20.glActiveTexture(_textureI);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, _ytid);
GLES20.glUniform1i(_yhandle, _tIindex);
GLES20.glActiveTexture(_textureII);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, _utid);
GLES20.glUniform1i(_uhandle, _tIIindex);
GLES20.glActiveTexture(_textureIII);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, _vtid);
GLES20.glUniform1i(_vhandle, _tIIIindex);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glFinish();
GLES20.glDisableVertexAttribArray(_positionHandle);
GLES20.glDisableVertexAttribArray(_coordHandle);
}
/**
* create program and load shaders, fragment shader is very important.
*/
public int createProgram(String vertexSource, String fragmentSource) {
// create shaders
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
// just check
Utils.LOGD("vertexShader = " + vertexShader);
Utils.LOGD("pixelShader = " + pixelShader);
int program = GLES20.glCreateProgram();
if (program != 0) {
GLES20.glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
GLES20.glAttachShader(program, pixelShader);
checkGlError("glAttachShader");
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
Utils.LOGE("Could not link program: ", null);
Utils.LOGE(GLES20.glGetProgramInfoLog(program), null);
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
/**
* create shader with given source.
*/
private int loadShader(int shaderType, String source) {
int shader = GLES20.glCreateShader(shaderType);
if (shader != 0) {
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
Utils.LOGE("Could not compile shader " + shaderType + ":", null);
Utils.LOGE(GLES20.glGetShaderInfoLog(shader), null);
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
/**
* these two buffers are used for holding vertices, screen vertices and texture vertices.
*/
private void createBuffers(float[] vert, float[] coord) {
_vertice_buffer = ByteBuffer.allocateDirect(vert.length * 4);
_vertice_buffer.order(ByteOrder.nativeOrder());
_vertice_buffer.asFloatBuffer().put(vert);
_vertice_buffer.position(0);
if (_coord_buffer == null) {
_coord_buffer = ByteBuffer.allocateDirect(coord.length * 4);
_coord_buffer.order(ByteOrder.nativeOrder());
_coord_buffer.asFloatBuffer().put(coord);
_coord_buffer.position(0);
}
}
private void checkGlError(String op) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Utils.LOGE("***** " + op + ": glError " + error, null);
throw new RuntimeException(op + ": glError " + error);
}
}
private static float[] squareVertices = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, }; // fullscreen
private static float[] coordVertices = { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, };// whole-texture
private static final String VERTEX_SHADER = "attribute vec4 vPosition;\n" + "attribute vec2 a_texCoord;\n"
+ "varying vec2 tc;\n" + "void main() {\n" + "gl_Position = vPosition;\n" + "tc = a_texCoord;\n" + "}\n";
private static final String FRAGMENT_SHADER = "precision mediump float;\n" + "uniform sampler2D tex_y;\n"
+ "uniform sampler2D tex_u;\n" + "uniform sampler2D tex_v;\n" + "varying vec2 tc;\n" + "void main() {\n"
+ "vec4 c = vec4((texture2D(tex_y, tc).r - 16./255.) * 1.164);\n"
+ "vec4 U = vec4(texture2D(tex_u, tc).r - 128./255.);\n"
+ "vec4 V = vec4(texture2D(tex_v, tc).r - 128./255.);\n" + "c += V * vec4(1.596, -0.813, 0, 0);\n"
+ "c += U * vec4(0, -0.392, 2.017, 0);\n" + "c.a = 1.0;\n" + "gl_FragColor = c;\n" + "}\n";
這裡面程式碼比較複雜,我在這裡稍作解釋:
1.首先,buildProgram()目的要生成一個program,作用是用來將YUV->RGB,其中用到了2個shader(shader就相當於一個小運算器,它執行一段程式碼),第1個shader執行VERTEX_SHADER裡的程式碼,目的是將座標作為引數傳入第2個shader;第2個shader來做YUV->RGB的運算。
2.buildTextures()是要生成3個貼圖,分別為了顯示R/G/B資料,三個貼圖重合在一起,顯示出來的就是彩色的圖片。
3.drawFrame()是使用program來做運算,並真正去做畫這個動作了。
至此,就可以將YUV圖片也好,視訊也可,給顯示在Android上了,而且速度不慢哦!希望能幫到大家。
相關程式碼下載連結:
http://download.csdn.net/detail/ueryueryuery/7144851
木有分的同學,可以發郵件至[email protected],寫明需要哪份程式碼,LOL
========================================================
2014-07-03
以上程式碼只是關鍵類的程式碼,也搭者樓主懶,一直沒有寫一個完整的demo。
不過,只要大家多從網上查查,再結合樓主的類,一定可以自己搞出來的!
相關推薦
Android上使用OpenGLES2.0顯示YUV資料
樓主收到這樣的任務,在Android上用OpenGLES來顯示YUV影象,之所以這樣做,是因為: 1.Android本身也不能直接顯示YUV影象,YUV轉成RGB還是必要的; 2.YUV手動轉RGB會佔用大量的CPU資源,如果以這樣的形式播放視訊,手機會很熱,所以我們儘量讓
Android用surface直接顯示yuv資料(一)
轉自:http://blog.csdn.net/tung214/article/details/36887041 研究了一段時間Android的surface系統,一直執著地認為所有在surface或者螢幕上顯示的畫面,必須要轉換成RGB才能顯示,yuv資料也要通過顏色
Android用surface直接顯示yuv資料(二)
上一篇文章主要是參照AwesomePlayer直接用SoftwareRenderer類來顯示yuv,為了能用到這個類,不惜依賴了libstagefright、libstagefright_color_conversion等動態靜態庫,從而造成程式具有很高的耦合度,也不便
Android Camera2 Opengles2.0 影象實時濾鏡 顯示 視訊編碼
在博文”Android Camera2 Opengles2.0 預覽影象實時濾鏡 視訊編碼” http://blog.csdn.net/keen_zuxwang/article/details/78366598 的基礎上新增FBO實時濾鏡、回撥顯示—其中
Android Camera2 Opengles2.0 實時濾鏡(冷暖色/放大鏡/模糊/美顏)
https://blog.csdn.net/keen_zuxwang/article/details/78363464 demo: http://download.csdn.net/download/keen_zuxwang/10041423 1、建立頂點位置、紋理陣列 2、建立、編譯、載入shader程
IOS opengl es2.0顯示YUV RGB
簡介 opengl es (OpenGL for Embedded Systems)是OpenGL針對嵌入式系統的圖形API. Android, IOS, 以及PC, 都支援這個規範. opengl es 2.0 API文件 https://www.khronos.org/registr
Android O(8.0)音訊write資料流程變化(HIDL)
簡單回顧下,Audio write資料流程, AudioTrack->write AudioFlinger::PlaybackThread::threadLoop_write() mNormalSink->write 而mNormalSink
讓輸入框在Android上全屏顯示
1. 問題 關於Cocos2d-x在Android上的輸入框,我忍了好久了,一直沒去折騰它,覺得無關緊要。(小若:等等,你倒底想說什麼?) 我想說的是,預設情況下,我們在Android上開啟的輸入框是這樣的: (小若:很好啊,完美~) 才怪啊~!我本來也以為
Android中攝像頭獲取的YUV資料轉Opencv的Mat
背景 在onPreviewFrame方法中獲取的byte[] data資料為420sp格式,排列順序為width*height個Y(亮度資訊,就是我們常見的灰度影象),後面是UV(顏色資訊),4個Y共享一個U和V,故byte陣列的總大小是width*height
手把手教你在Android Studio 3.0上分析內存泄漏
pan style http name ans tle andro edi ont 手把手教你在Android Studio 3.0上分析內存泄漏手把手教你在Android Studio 3.0上分析內存泄漏
android顯示RGB565資料影象
本人近期做了一個專案,是關於類似於一個視訊監控,主要的是用了一個開發板,把一幀一幀的影象資料通過socket傳出來,然後在Android客戶端進行監聽資料;當獲取到資料之後,在imageview上顯示出來,最核心的部分是看了老外的一篇帖子才解決的問題。 帖子是這樣的,老外遇到了和我一樣的問題
BWA0.7+Samtools1.5+GATK4.0在小資料集上的試驗
試驗資料 chr14_1.fastq chr14_2.fastq (1.47G each one .gz) chr14.fasta (28M .gz) chr14.fastq檔案可以在GAGE下載 chr14.fasta檔案可以在UCSC下載 軟體的版本: bwa-0.7.
BWA0.7+Samtools1.5+GATK4.0在大資料集上的試驗
試驗資料 fasta:hg38.fa檔案可以在UCSC下載 (hg38.fa.gz 938M) fastq非公開檔案 KY18011403DNA_DHG18153-V_AHHVVHCCXY_L7_1.fq 35G KY18011403DNA_DHG18153-V_AHHVVHCCX
Android studio 3.0上進行多渠道打包
1.生成簽名檔案 點選 Build -> Generate Signed APK: 2.建立一個簽名 3.多渠道打包 作用:就是根據不同的渠道值,去具體分析每個渠道的使用者情況 步驟一: 3.1在AndroidManifest.xml的application
android,遙控器控制CCT燈,APP上無法同步顯示調整
預置條件:遙控器控制CCT燈 操作步驟:變化色溫顯示 預期結果:APP上色溫條會同步顯示變化 實際結果:APP上色溫條無反應 一看到這個bug就知道是資料問題,首先我找了個遙控器跟燈,試了一下,發現亮度是會有同步,而色溫卻沒有同步。 解決思路:1先找到對應頁面 2斷點檢視下為什麼沒有值
Android camera2 回撥imagereader 從Image拿到YUV資料轉化成RGB,生成bitmap並儲存
ImageUtil.java import android.graphics.ImageFormat; import android.media.Image; import android.os.Build; import android.support.annotation.RequiresApi;
Android Studio 在res中新建資料夾不顯示
工作需要,要學習Android。於是就拿了本電子書進行學習。 1.問題描述 根據書上的例子在 res資料夾下建 layout_large檔案,居然不顯示 但是在 當前目錄下確實建立了。 2.問題思考 難道是沒有重新整理?使用File->Sync With Fi
Android上傳圖片到伺服器並顯示(後臺用Java處理)
Android上傳圖片(Android Studio) Fragment介面: private String img_src; /** * 從相簿選取圖片 */ public void selectImg() { Intent intent = new
android mtp模式下連線PC後只顯示指定資料夾
轉載請註明文章出錯及作者 作者:Xandy 出處:http://blog.csdn.net/xl19862005 一、mtp概述 android在3.0以後的版本加入了mtp的支援,相對於mass storage模式,由於mtp優越性,現在幾乎所有的手機連線PC後都是以mtp的方式進
Android進階:網路與資料儲存—步驟1:Android網路與通訊(第3小節:ListView上)
內容概要: 一、課程介紹 二、ListView的準備工作 ListView簡介 ListView的實現步驟 三、ListView簡單應用 Adapter的資料繫結 最簡單ListView效果演示 獲取系統已安裝應用列表 優化效能 一、課程介紹 什麼是List