Demo中的小姐姐来自于微博@好梦梦露,如有版权问题请联系本萌~
在这篇文章里,我们介绍一个最简的固定渲染管线的Android OpenGLES代码框架和一个最简的Shader程序用于基本的画面绘制。
在开始之前,我们要了解一些注意事项:
- OpenGLES的所有API 只能在GLSurfaceView中的GLThread线程中调用才有用
- 不要试图在MainThread中调用OpenGLES的API,在UI使用硬件加速且没有独立的RenderThread的Android版本中,在MainThread中调用OpenGLES的API会扰乱UI的正常绘制
- Renderer上的三个接口(onSurfaceCreated / onSurfaceChanged / onDrawFrame)是执行在使用这个渲染器的GLSurfaceView的GLThread中的
- GLSurfaceView.queue(Runnable)方法也可以使传入的Runnable执行在它的GLThread中

编程
流程说明

这是一个简单的绘制图片的渲染流程,需要注意的是不是所有OpenGLES渲染流程都是这样,但大概的流程还是具有相似性的。
OpenGLES本身是个状态机,所以对OpenGLES的所有API的调用,都要注意当前设置的OpenGLES状态。比如,你需要先绑定一个纹理后,后续对纹理绘制效果的设置,才是针对已绑定的这个纹理生效。而对于模型的绘制,你需要在绘制之前,配置好所有绘制这个模型所需要的功能开关,然后设置好所有绘制这个模型所需要的参数和素材,一切就绪后,再调用“绘制”,这样才能正常完成模型的绘制。
总体代码
布局:res/layout/main_acitivity.xml
<RelativeLayout 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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<android.opengl.GLSurfaceView
android:id="@+id/GLSurfaceView_GLImage"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
Activity:MainActivity.java
package com.cocoonshu.example.gl_image;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.app.Activity;
/**
* Main content page
* @author Cocoonshu
* @date 2016-06-29
*/
public class MainActivity extends Activity {
private static final int OpenGLES_1_1 = 1; // 使用OpenGLES 1.1的API
private static final int OpenGLES_2_0 = 2; // 使用OpenGLES 2.0的API
private GLSurfaceView mGlvOpenGLImage = null; // 承载OpenGLES的控件
private ImageRenderer mImageRenderer = null; // 使用OpenGLES API的渲染器
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initializeOpenGLComponents();
}
private void initializeOpenGLComponents() {
mGlvOpenGLImage = (GLSurfaceView) findViewById(R.id.GLSurfaceView_GLImage);
mImageRenderer = new ImageRenderer();
mGlvOpenGLImage.setEGLConfigChooser(5, 6, 5, 0, 16, 8); // 设置OpenGLES中画布中各个buffer的位数
mGlvOpenGLImage.setEGLContextClientVersion(OpenGLES_1_1); // 设置OpenGLES API版本
mGlvOpenGLImage.setRenderer(mImageRenderer); // 设置渲染器
mGlvOpenGLImage.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 设置OpenGLES的渲染驱动模式
}
}
这种设置方法,不需要去继承GLSurfaceView来自定义一个新控件出来,适合对GLSurfaceView依赖不高的需求。
设置GLSurfaceView的几个方法需要说明一下:
-
setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize)
- 用于设置OpenGLES绘制的颜色缓存的位深,深度缓冲的位深和模板缓冲的位深
- [redSize, greenSize, int blueSize, int alphaSize]如果是[8, 8, 8, 8],则可以理解为是对应的ARGB_8888
- [redSize, greenSize, int blueSize, int alphaSize]如果是[5, 6, 5, 0],则可以理解为是对应的RGB_565
- [depthSize]对应的是OpenGLES中的深度缓冲的位数,深度缓冲可以认为是用来标记物体到摄像机的距离的数据结构
- [stencilSize]对应的是OpenGLES中的模板缓冲的位数,模板缓冲是用来标记哪些位置应该被保护起来不被绘制的数据结构
-
setEGLContextClientVersion(int version)
- 是用来配置要使用的OpenGLES API的版本
- 这里的version号也需要在AndroidManifest.xml里配置对应的feature
- version = 1,则使用OpenGLES 1.x的API,AndroidManifest.xml里配置应配置<uses-feature android:glEsVersion="0x00010000" />
- version = 2,则使用OpenGLES 2.x的API,AndroidManifest.xml里配置应配置<uses-feature android:glEsVersion="0x00020000" />
- version = 3,则使用OpenGLES 3.x的API,AndroidManifest.xml里配置应配置<uses-feature android:glEsVersion="0x00030000" />
-
setRenderMode(int renderMode)
- 用来配置OpenGLES的渲染驱动模式,有两个值
- GLSurfaceView.RENDERMODE_WHEN_DIRTY, 在这个模式下,只有当调用了GLSurfaceView.requestRender()后,才会绘制下一帧
- GLSurfaceView.RENDERMODE_CONTINUOUSLY,在这个模式下,当一帧绘制完成后,下一帧会自动被调用,无论有没有画面更新的需要。所以这个驱动模式会比较耗电,适合在播放视频画面或者一直有持续动画的场景中使用
- 用来配置OpenGLES的渲染驱动模式,有两个值
渲染器: ImageRenderer.java
package com.cocoonshu.example.glimage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES11;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;
import android.os.AsyncTask;
import android.util.Log;
/**
* A image renderer for GLSurfaceView
* @author Cocoonshu
* @date 2016-06-29
*/
public class ImageRenderer implements Renderer {
protected static final String TAG = "ImageRenderer";
public static final int IMAGE_POSITION_LEFT = 0; // 左边图片的索引
public static final int IMAGE_POSITION_CENTER = 1; // 中间图片的索引
public static final int IMAGE_POSITION_RIGHT = 2; // 右边图片的索引
public static final int SCALE_WIDTH = 0; // 图片模型的宽度缩放值索引
public static final int SCALE_HEIGHT = 1; // 图片模型的高度缩放值索引
public static final float EXPECTED_SCALE = 15.0f; // 希望图片被放大到对角线为15.0f个单位的大小
private static final String[] ImagePathes = new String[] { // 要显示的图片的地址
"eGE1VW53c0Qv.jpg",
"IdzdYT1pRPT0.jpg",
"Q2tVRjlhVn5t.jpg"
};
private GLSurfaceView mHostView = null; // 使用此渲染器的GLSurfaceView
private BitmapLoader mBitmapLoader = null; // 图片异步加载器
private FloatBuffer mRectVertexBuffer = null; // 矩形的顶点
private FloatBuffer mRectTexcoordsBuffer = null; // 矩形的贴图坐标
private ShortBuffer mRectIndiceBuffer = null; // 矩形的顶点索引
private int[] mTextureIDs = new int[3]; // 图片的纹理ID集合
private float[][] mTextureScalers = new float[3][2]; // 纹理根据图片的缩放比
public ImageRenderer(GLSurfaceView hostView) {
// 我们传入使用此渲染器的GLSurfaceView引用,主要是为了能够
// GLSurfaceView的queue(Runnable)方法,这个方法能够把Runnable
// 放置在OpenGLES所在的GLThread线程中执行
mHostView = hostView;
mBitmapLoader = new BitmapLoader(mHostView.getResources(), this);
// 准备模型数据
buildMesh();
}
/**
* 创建模型数据
*/
private void buildMesh() {
// 顶点数组
// 创建长宽为单位1.0f的矩形
float[] vertex = new float[] {
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, +0.5f, 0.0f, // 左上角
+0.5f, -0.5f, 0.0f, // 右下角
+0.5f, +0.5f, 0.0f // 右上角
};
// 贴图坐标数组
float[] texcoords = new float[] {
0.0f, 1.0f, // 左下角
0.0f, 0.0f, // 左上角
1.0f, 1.0f, // 右下角
1.0f, 0.0f // 右上角
};
// 顶点索引
short[] indices = new short[] {
0, 1, 2, // 左下三角形
2, 1, 3 // 右上三角形
};
// 组装成Buffer
{// 顶点Buffer
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(vertex.length * Float.SIZE / 8);
byteBuffer.order(ByteOrder.nativeOrder());
mRectVertexBuffer = byteBuffer.asFloatBuffer();
mRectVertexBuffer.put(vertex);
mRectVertexBuffer.rewind();
}
{// 贴图坐标Buffer
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(texcoords.length * Float.SIZE / 8);
byteBuffer.order(ByteOrder.nativeOrder());
mRectTexcoordsBuffer = byteBuffer.asFloatBuffer();
mRectTexcoordsBuffer.put(texcoords);
mRectTexcoordsBuffer.rewind();
}
{// 顶点索引Buffer
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(indices.length * Short.SIZE / 8);
byteBuffer.order(ByteOrder.nativeOrder());
mRectIndiceBuffer = byteBuffer.asShortBuffer();
mRectIndiceBuffer.put(indices);
mRectIndiceBuffer.rewind();
}
}
/**
* 初始化OpenGLES: 设置OpenGLES中的各种开关和初始值
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 功能性设置
GLES11.glEnable(GLES11.GL_DEPTH_TEST); // 开启深度测试,如果我们绘制的东西有远近层次之分,就开启它
GLES11.glDisable(GLES11.GL_ALPHA_TEST); // 关闭透明测试,如果我们需要通过对比模型的透明度来觉得是否绘制它,就开启它
GLES11.glDisable(GLES11.GL_STENCIL_TEST); // 关闭模板测试,如果我们需要用蒙版来遮盖某些绘制部分,就开启它
GLES11.glDisable(GLES11.GL_BLEND); // 关闭颜色混合,如果我们需要使绘制的半透明模型有颜色的混合效果,就开启它
GLES11.glEnable(GLES11.GL_DITHER); // 开启颜色抖动,如果是要显示图片,开启它,显示的颜色数量会更丰富
GLES11.glEnable(GLES11.GL_TEXTURE_2D); // 开启贴图功能,如果我们要使用贴图纹理,就开启它
GLES11.glDisable(GLES11.GL_LIGHTING); // 关闭光照效果,如果想要在模型表面呈现出光照的明暗变化,就开启它
GLES11.glDisable(GLES11.GL_FOG); // 关闭雾霾效果,如果想要在场景中绘制出雾霾的效果,就开启它
// 默认值设置
GLES11.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // 设置清除颜色缓冲的色值,它会是视窗的清屏颜色
GLES11.glClearDepthf(1.0f); // 设置清除深度缓冲的深度值,它会是深度缓冲的默认深度值
GLES11.glDepthRangef(0.1f, 100.0f); // 设置深度缓冲的深度范围
// 绘图效果设置
GLES11.glHint(GLES11.GL_PERSPECTIVE_CORRECTION_HINT, GLES11.GL_NICEST); // 设置透视矫正配置为:质量最好
GLES11.glHint(GLES11.GL_POINT_SMOOTH_HINT, GLES11.GL_NICEST); // 设置点绘制平滑度配置为:质量最好
GLES11.glHint(GLES11.GL_LINE_SMOOTH_HINT, GLES11.GL_NICEST); // 设置线条绘制平滑度配置为:质量最好
GLES11.glHint(GLES11.GL_POLYGON_SMOOTH_HINT, GLES11.GL_DONT_CARE); // 设置模型绘制平滑度配置为:自动
// 开始加载图片并上传纹理
mBitmapLoader.execute(ImagePathes);
}
/**
* 配置绘图窗口尺寸:重新根据新的尺寸来配置投影矩阵、视窗和绘图区域
*/
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置视窗
GLES11.glViewport(0, 0, width, height); // 设置视窗尺寸为控件大小
// 设置投影矩阵
float fovy = (float) Math.toRadians(60); // 视野角度为120°
float zNear = 0.1f; // 摄像机的最近端剪裁距离
float zFar = 100f; // 摄像机的最远端剪裁距离
float aspectRatio = (float) width / (float) height; // 计算视窗的显示比例
float horizontalVolume = (float) (zNear * Math.tan(fovy * 0.5f)); // 计算视景体的宽度
float verticalVolume = horizontalVolume / aspectRatio; // 计算视景体的高度
GLES11.glMatrixMode(GLES11.GL_PROJECTION); // 把当前的操作矩阵切换到投影矩阵
GLES11.glLoadIdentity(); // 把当前的操作矩阵重置为单位矩阵
GLES11.glFrustumf( // 设置投影矩阵为透视投影:
-horizontalVolume, horizontalVolume, // - 透视视景体的左右边位置
-verticalVolume, verticalVolume, // - 透视视景体的上下边位置
zNear, zFar); // - 透视视景体的前后边位置
}
/**
* 绘制一帧画面:相当于View.onDraw(Canvas),不过这里使用OpenGLES的API来绘制
*/
@Override
public void onDrawFrame(GL10 gl) {
// 重置颜色缓存和深度缓冲
GLES11.glClear(GLES11.GL_COLOR_BUFFER_BIT | GLES11.GL_DEPTH_BUFFER_BIT);
// 设置视图矩阵
GLES11.glMatrixMode(GLES11.GL_MODELVIEW); // 把当前的操作矩阵切换到模型视图矩阵
GLES11.glLoadIdentity(); // 把当前的操作矩阵重置为单位矩阵
GLU.gluLookAt(gl, // 设置摄像机的姿态:
0.0f, 4.0f, 15.0f, // - 摄像机的位置
0.0f, 5.0f, 0.0f, // - 摄像机拍摄的点
0.0f, 1.0f, 0.0f); // - 摄像机顶部的朝向
{// 摆放并绘制模型,模型应该从远及近地绘图
// 开启OpenGLES客户端指定网格数据的操作方式
// 以便从OpenGLES客户端指定网格数据来绘制模型
GLES11.glEnableClientState(GL10.GL_VERTEX_ARRAY); // 启用OpenGLES客户端指定顶点数组的操作方式
GLES11.glVertexPointer(3, GL10.GL_FLOAT, 0, mRectVertexBuffer); // 指定顶点数组,每3个数作为一个顶点坐标
GLES11.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // 启用OpenGLES客户端指定贴图坐标数组的操作方式
GLES11.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mRectTexcoordsBuffer); // 指定贴图坐标数组,每2个数作为一个顶点坐标
// 激活贴图纹理单元,用于纹理绘制
// 纹理单元的数量是有限的,可以用过glGetIntegerv(GL_MAX_TEXTURE_UNITS, maxTextureUnits, 0);来查询
// - OpenGLES 1.0最少有1个纹理单元
// - OpenGLES 1.1最少有2个纹理单元
// 如果一个模型只需要贴一层纹理,那么我们激活一个纹理就足够了
GLES11.glActiveTexture(GLES11.GL_TEXTURE0); // 激活#0纹理单元
{// 摆放并绘制模型
{// 绘制左边的图片
GLES11.glPushMatrix();
// 设置模型矩阵:
// - 1. 绕(0.0f, 1.0f, 0.0f)轴旋转40°
// - 2. 把长宽为(1.0f, 1.0f)的矩形片缩放到图片尺寸的宽高比
// - 3. 把矩形移动到左边靠后的位置
GLES11.glTranslatef(-10.0f, mTextureScalers[IMAGE_POSITION_LEFT][SCALE_HEIGHT] * 0.5f, -10.0f);
GLES11.glScalef(mTextureScalers[IMAGE_POSITION_LEFT][SCALE_WIDTH], mTextureScalers[IMAGE_POSITION_LEFT][SCALE_HEIGHT], 1);
GLES11.glRotatef(-40.0f, 0.0f, 1.0f, 0.0f);
// 绑定要贴到矩形上的纹理
GLES11.glBindTexture(GLES11.GL_TEXTURE_2D, mTextureIDs[IMAGE_POSITION_LEFT]);
// 绘制这个模型
GLES11.glDrawElements(GLES11.GL_TRIANGLES, mRectIndiceBuffer.capacity(), GLES11.GL_UNSIGNED_SHORT, mRectIndiceBuffer);
GLES11.glPopMatrix();
}
{// 绘制中间的图片
GLES11.glPushMatrix();
// 设置模型矩阵:
// - 1. 绕(0.0f, 1.0f, 0.0f)轴旋转0°
// - 2. 把长宽为(1.0f, 1.0f)的矩形片缩放到图片尺寸的宽高比
// - 3. 把矩形移动到原点的位置
GLES11.glTranslatef(0.0f, mTextureScalers[IMAGE_POSITION_CENTER][SCALE_HEIGHT] * 0.5f, 0.0f);
GLES11.glScalef(mTextureScalers[IMAGE_POSITION_CENTER][SCALE_WIDTH], mTextureScalers[IMAGE_POSITION_CENTER][SCALE_HEIGHT], 1);
GLES11.glRotatef(0.0f, 0.0f, 1.0f, 0.0f);
// 绑定要贴到矩形上的纹理
GLES11.glBindTexture(GLES11.GL_TEXTURE_2D, mTextureIDs[IMAGE_POSITION_CENTER]);
// 绘制这个模型
GLES11.glDrawElements(GLES11.GL_TRIANGLES, mRectIndiceBuffer.capacity(), GLES11.GL_UNSIGNED_SHORT, mRectIndiceBuffer);
GLES11.glPopMatrix();
}
{// 绘制右边的图片
GLES11.glPushMatrix();
// 设置模型矩阵:
// - 1. 绕(0.0f, 1.0f, 0.0f)轴旋转40°
// - 2. 把长宽为(1.0f, 1.0f)的矩形片缩放到图片尺寸的宽高比
// - 3. 把矩形移动到右边靠后原点的位置
GLES11.glTranslatef(10.0f, mTextureScalers[IMAGE_POSITION_RIGHT][SCALE_HEIGHT] * 0.5f, -10.0f);
GLES11.glScalef(mTextureScalers[IMAGE_POSITION_RIGHT][SCALE_WIDTH], mTextureScalers[IMAGE_POSITION_RIGHT][SCALE_HEIGHT], 1);
GLES11.glRotatef(40.0f, 0.0f, 1.0f, 0.0f);
// 绑定要贴到矩形上的纹理
GLES11.glBindTexture(GLES11.GL_TEXTURE_2D, mTextureIDs[IMAGE_POSITION_RIGHT]);
// 绘制这个模型
GLES11.glDrawElements(GLES11.GL_TRIANGLES, mRectIndiceBuffer.capacity(), GLES11.GL_UNSIGNED_SHORT, mRectIndiceBuffer);
GLES11.glPopMatrix();
}
}
// 关闭OpenGLES客户端指定网格数据的操作方式,以便做其他绘制操作,
// 如果没有其他绘制操作,可以一直启用这种操作方式
GLES11.glDisableClientState(GL10.GL_VERTEX_ARRAY); // 关闭OpenGLES客户端指定顶点数组的操作方式
GLES11.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // 关闭OpenGLES客户端指定贴图坐标数组的操作方式
}
}
/**
* 获取OpenGLES中的上传纹理可能发生的错误
*/
private void printGLError(String location) {
int glError = GLES11.glGetError(); // 获取上一个OpenGLES API调用出现的错误码
if (glError != GLES11.GL_NO_ERROR) {
Log.e(TAG, String.format("[%s] GLError = %s",
location, GLUtils.getEGLErrorString(glError))); // 把错误码转换为可读字符串
} else {
Log.i(TAG, String.format("[%s] GLError = No error", location));
}
}
/**
* 把Bitmap显示到OpenGLES绘制的视窗中
* @param bitmap
*/
public void displayBitmap(final Bitmap bitmap, final int position) {
if (mHostView != null) {
// 因为这个Runnable中的代码是把Bitmap上传为OpenGLES可用的纹理,
// 需要使用到OpenGLES API,所以需要用queueEvent方法把这个
// Runnable抛到GLThread中去执行
mHostView.queueEvent(new Runnable() {
@Override
public void run() {
if (bitmap == null) {
Log.e(TAG, String.format("[uploadTexture] bitmap is null, position is %d", position));
return;
}
{// 读取图片的宽高比,以便能按比例显示纹理
float imageWidth = bitmap.getWidth();
float imageHeight = bitmap.getHeight();
float aspectRadian = (float) Math.atan2(imageHeight, imageWidth);
mTextureScalers[position][SCALE_WIDTH] = (float) Math.cos(aspectRadian) * EXPECTED_SCALE;
mTextureScalers[position][SCALE_HEIGHT] = (float) Math.sin(aspectRadian) * EXPECTED_SCALE;
Log.e(TAG, String.format("[uploadTexture] position = %d, bitmapSize = (%f, %f), aspectRadian = %f, scale = (%f, %f)",
position, imageWidth, imageHeight, aspectRadian, mTextureScalers[position][SCALE_WIDTH], mTextureScalers[position][SCALE_HEIGHT]));
}
{// 上传纹理
GLES11.glGenTextures(1, mTextureIDs, position);
GLES11.glBindTexture(GLES11.GL_TEXTURE_2D, mTextureIDs[position]);
GLES11.glTexParameterx(GLES11.GL_TEXTURE_2D, GLES11.GL_TEXTURE_WRAP_S, GLES11.GL_CLAMP_TO_EDGE);
GLES11.glTexParameterx(GLES11.GL_TEXTURE_2D, GLES11.GL_TEXTURE_WRAP_T, GLES11.GL_CLAMP_TO_EDGE);
GLES11.glTexParameterx(GLES11.GL_TEXTURE_2D, GLES11.GL_TEXTURE_MIN_FILTER, GLES11.GL_LINEAR);
GLES11.glTexParameterx(GLES11.GL_TEXTURE_2D, GLES11.GL_TEXTURE_MAG_FILTER, GLES11.GL_LINEAR);
GLUtils.texImage2D(GLES11.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
}
// 获取OpenGLES中的上传纹理可能发生的错误
printGLError("displayBitmap");
// 纹理上传完成后,通知OpenGLES去重绘一帧,以便绘制已上传的纹理
mHostView.requestRender();
}
});
}
}
/**
* Bitmap asynchronous Loader
* @author Cocoonshu@Plf.MediaCenter.Gallery
* @date 2016-06-29 19:46:29
*/
private class BitmapLoader extends AsyncTask<String, Integer, Void> {
private Resources mResource = null;
private ImageRenderer mRenderer = null;
public BitmapLoader(Resources resource, ImageRenderer renderer) {
mResource = resource;
mRenderer = renderer;
}
@Override
protected Void doInBackground(String... imageAssetPaths) {
if (imageAssetPaths != null) {
for (int i = 0; i < imageAssetPaths.length; i++) {
String assetPath = imageAssetPaths[i];
Bitmap bitmap = decodeImage(assetPath);
mRenderer.displayBitmap(bitmap, i);
}
}
return null;
}
public Bitmap decodeImage(String assetPath) {
try {
AssetManager assetManager = mResource.getAssets();
InputStream inputStream = assetManager.open(assetPath);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
}
网友评论