Android OpenGL ES 2.0学习研究 (一)
基于对 Google 的 Gallery 代码的研究和修改,对 OpenGL ES 2.0 在 Android 中的使用进行总结;
这一篇主要集中于四点进行简要介绍:GLRootView(base) + GLView(UI) + GLES20Canvas(canvas) + Texture;
关于OpenGL ES的基础知识可以参考:OpenGL ES 简明教程
概述:
目前,关于GLES的教程也有不少,但是基本上都是基础知识介绍,很零散;
Google官方的Gallery则对代码进行了封装,结构更加清晰灵活,非常适合学习研究GLES;
GLRootView(base)+ GLES20Canvas(canvas) + GLView(ui) + Texture:
GLRootView是一个GLSurfaceView,通过GLSurfaceView.Renderer的三大方法将GLView绘制到GLCanvas上:
- GLSurfaceView:
- 起到连接 OpenGL ES与Android 的 View 层次结构之间的桥梁作用。
- 使得 Open GL ES 库适应于 Anndroid 系统的 Activity 生命周期。
- 使得选择合适的 Frame buffer 像素格式变得容易。
- 创建和管理单独绘图线程以达到平滑动画效果。
- 提供了方便使用的调试工具来跟踪 OpenGL ES 函数调用以帮助检查错误。
public void setRenderer(GLSurfaceView.Renderer renderer)
GLSurfaceView.Renderer:
public void onSurfaceCreated(GL10 gl, EGLConfig config)
public void onDrawFrame(GL10 gl)
public void onSurfaceChanged(GL10 gl, int width, int height)
GLES20Canvas相当于Canvas,设置好Vertex shader和Fragment shader等参数,将GLView绘制出来:
- GLES的使用都集中在这里处理,相当于从GLSurfaceView中抽离出来GLES的代码进行封装处理;
- GLSurfaceView:
GLView是要绘制的UI,可以同时有多个,它处理触摸事件,通过GLRootView将其绘制在GLCanvas上:
- GLView是整个UI的布局;
- 可以在GLView基础上进行继承,充分自定义;
Texture则是绘制的画面,在GLES20Canvas上绘制出来:
- 可以将其细分为ColorTexture、StringTexture、BitmapTexture分别绘制色块、字符串和图片;
接下来,看代码,具体说明看备注:
自定义一个GLRootView,继承GLSurfaceView并实现GLSurfaceView.Renderer接口(GLRoot为自定义接口)
//GLRoot为自定义接口
public class GLRootView extends GLSurfaceView
implements GLSurfaceView.Renderer, GLRoot {
public GLRootView(Context context) {
this(context, null);
}
public GLRootView(Context context, AttributeSet attrs) {
super(context, attrs);
//进行初始化设置
setBackgroundDrawable(null);
setEGLContextClientVersion(2);
setEGLConfigChooser(mEglConfigChooser);
setRenderer(this);
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
getHolder().setFormat(PixelFormat.RGB_888);
} else {
getHolder().setFormat(PixelFormat.RGB_565);
}
}
@Override
public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
GL11 gl = (GL11) gl1;
mRenderLock.lock();
try {
mGL = gl;
//创建GLES20Canvas,创建的时候会自动加载shader
mCanvas = new GLES20Canvas();
BasicTexture.invalidateAllTextures();
} finally {
mRenderLock.unlock();
}
//设置渲染模式为 RENDERMODE_WHEN_DIRTY 以节约性能
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
//setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
@Override
public void onSurfaceChanged(GL10 gl1, int width, int height) {
//设置线程
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
GalleryUtils.setRenderThread();
GL11 gl = (GL11) gl1;
Utils.assertTrue(mGL == gl);
//重新设置GLES20Canvas的尺寸
mCanvas.setSize(width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
AnimationTime.update();
mRenderLock.lock();
try {
//重点在这,具体的绘制操作
} finally {
mRenderLock.unlock();
}
//第一次绘制的时候放置一个黑色的背景,以防透明
if (mFirstDraw) {
mFirstDraw = false;
post(new Runnable() {
@Override
public void run() {
View root = getRootView();
View cover = root.findViewById(R.id.gl_root_cover);
cover.setVisibility(GONE);
}
});
}
}
//处理触摸事件
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (!isEnabled()) return false;
int action = event.getAction();
if (action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_UP) {
mInDownState = false;
} else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
return false;
}
mRenderLock.lock();
try {
// If this has been detached from root, we don't need to handle event
boolean handled = mContentView != null
&& mContentView.dispatchTouchEvent(event);
if (action == MotionEvent.ACTION_DOWN && handled) {
mInDownState = true;
}
return handled;
} finally {
mRenderLock.unlock();
}
}
}
自定义GLES20Canvas
@SuppressLint("NewApi")
public class GLES20Canvas implements GLCanvas {
private abstract static class ShaderParameter {
public int handle;
protected final String mName;
public ShaderParameter(String name) {
mName = name;
}
public abstract void loadHandle(int program);
}
//处理shader
private static class UniformShaderParameter extends ShaderParameter {
public UniformShaderParameter(String name) {
super(name);
}
@Override
public void loadHandle(int program) {
handle = GLES20.glGetUniformLocation(program, mName);
checkError();
}
}
private static class AttributeShaderParameter extends ShaderParameter {
public AttributeShaderParameter(String name) {
super(name);
}
@Override
public void loadHandle(int program) {
handle = GLES20.glGetAttribLocation(program, mName);
checkError();
}
}
//初始化
public GLES20Canvas() {
Matrix.setIdentityM(mTempTextureMatrix, 0);
Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES);
mBoxCoordinates = uploadBuffer(boxBuffer);
//创建的时候就加载shader
int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER);
int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER);
int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER);
int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER);
int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER);
int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
OES_TEXTURE_FRAGMENT_SHADER);
mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters);
mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader,
mTextureParameters);
mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader,
mOesTextureParameters);
mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters);
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
checkError();
}
@Override
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
GLES20.glViewport(0, 0, mWidth, mHeight);
checkError();
Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1);
if (getTargetTexture() == null) {
mScreenWidth = width;
mScreenHeight = height;
Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0);
Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1);
}
}
private void draw(int type, int offset, int count, float x, float y, float width, float height,
int color, float lineWidth) {
prepareDraw(offset, color, lineWidth);
draw(mDrawParameters, type, count, x, y, width, height);
}
//设置背景颜色
private void prepareDraw(int offset, int color, float lineWidth) {
GLES20.glUseProgram(mDrawProgram);
checkError();
if (lineWidth > 0) {
GLES20.glLineWidth(lineWidth);
checkError();
}
float[] colorArray = getColor(color);
boolean blendingEnabled = (colorArray[3] < 1f);
enableBlending(blendingEnabled);
if (blendingEnabled) {
GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]);
checkError();
}
GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0);
setPosition(mDrawParameters, offset);
checkError();
}
//绘制
private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width,
float height) {
setMatrix(params, x, y, width, height);
int positionHandle = params[INDEX_POSITION].handle;
GLES20.glEnableVertexAttribArray(positionHandle);
checkError();
GLES20.glDrawArrays(type, 0, count);
checkError();
GLES20.glDisableVertexAttribArray(positionHandle);
checkError();
}
public static void checkError() {
int error = GLES20.glGetError();
if (error != 0) {
Throwable t = new Throwable();
Log.e(TAG, "GL error: " + error, t);
}
}
//以下几个方法均为BitmapTexture调用
@Override
public void setTextureParameters(BasicTexture texture) {
int target = texture.getTarget();
GLES20.glBindTexture(target, texture.getId());
checkError();
GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
}
@Override
public void initializeTextureSize(BasicTexture texture, int format, int type) {
int target = texture.getTarget();
GLES20.glBindTexture(target, texture.getId());
checkError();
int width = texture.getTextureWidth();
int height = texture.getTextureHeight();
GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null);
}
@Override
public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
int target = texture.getTarget();
GLES20.glBindTexture(target, texture.getId());
checkError();
GLUtils.texImage2D(target, 0, bitmap, 0);
}
@Override
public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,
int format, int type) {
int target = texture.getTarget();
GLES20.glBindTexture(target, texture.getId());
checkError();
GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
}
}
自定义GLView
public class GLView {
public void startAnimation(CanvasAnimation animation) {
GLRoot root = getGLRoot();
if (root == null) throw new IllegalStateException();
mAnimation = animation;
if (mAnimation != null) {
mAnimation.start();
root.registerLaunchedAnimation(mAnimation);
}
invalidate();
}
// Sets the visiblity of this GLView (either GLView.VISIBLE or
// GLView.INVISIBLE).
public void setVisibility(int visibility) {
if (visibility == getVisibility()) return;
if (visibility == VISIBLE) {
mViewFlags &= ~FLAG_INVISIBLE;
} else {
mViewFlags |= FLAG_INVISIBLE;
}
onVisibilityChanged(visibility);
invalidate();
}
// Returns GLView.VISIBLE or GLView.INVISIBLE
public int getVisibility() {
return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
}
// This should only be called on the content pane (the topmost GLView).
public void attachToRoot(GLRoot root) {
Utils.assertTrue(mParent == null && mRoot == null);
onAttachToRoot(root);
}
// This should only be called on the content pane (the topmost GLView).
public void detachFromRoot() {
Utils.assertTrue(mParent == null && mRoot != null);
onDetachFromRoot();
}
// Returns the number of children of the GLView.
public int getComponentCount() {
return mComponents == null ? 0 : mComponents.size();
}
// Returns the children for the given index.
public GLView getComponent(int index) {
if (mComponents == null) {
throw new ArrayIndexOutOfBoundsException(index);
}
return mComponents.get(index);
}
// Adds a child to this GLView.
public void addComponent(GLView component) {
// Make sure the component doesn't have a parent currently.
if (component.mParent != null) throw new IllegalStateException();
// Build parent-child links
if (mComponents == null) {
mComponents = new ArrayList<GLView>();
}
mComponents.add(component);
component.mParent = this;
// If this is added after we have a root, tell the component.
if (mRoot != null) {
component.onAttachToRoot(mRoot);
}
}
// Removes a child from this GLView.
public boolean removeComponent(GLView component) {
if (mComponents == null) return false;
if (mComponents.remove(component)) {
removeOneComponent(component);
return true;
}
return false;
}
// Removes all children of this GLView.
public void removeAllComponents() {
for (int i = 0, n = mComponents.size(); i < n; ++i) {
removeOneComponent(mComponents.get(i));
}
mComponents.clear();
}
private void removeOneComponent(GLView component) {
if (mMotionTarget == component) {
long now = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(
now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
component.onDetachFromRoot();
component.mParent = null;
}
public Rect bounds() {
return mBounds;
}
public int getWidth() {
return mBounds.right - mBounds.left;
}
public int getHeight() {
return mBounds.bottom - mBounds.top;
}
public GLRoot getGLRoot() {
return mRoot;
}
// Request re-rendering of the view hierarchy.
// This is used for animation or when the contents changed.
public void invalidate() {
GLRoot root = getGLRoot();
if (root != null) root.requestRender();
}
// Request re-layout of the view hierarchy.
public void requestLayout() {
mViewFlags |= FLAG_LAYOUT_REQUESTED;
mLastHeightSpec = -1;
mLastWidthSpec = -1;
if (mParent != null) {
mParent.requestLayout();
} else {
// Is this a content pane ?
GLRoot root = getGLRoot();
if (root != null) root.requestLayoutContentPane();
}
}
protected void render(GLCanvas canvas) {
boolean transitionActive = false;
if (mTransition != null && mTransition.calculate(AnimationTime.get())) {
invalidate();
transitionActive = mTransition.isActive();
}
renderBackground(canvas);
canvas.save();
if (transitionActive) {
mTransition.applyContentTransform(this, canvas);
}
for (int i = 0, n = getComponentCount(); i < n; ++i) {
renderChild(canvas, getComponent(i));
}
canvas.restore();
if (transitionActive) {
mTransition.applyOverlay(this, canvas);
}
}
public void setIntroAnimation(StateTransitionAnimation intro) {
mTransition = intro;
if (mTransition != null) mTransition.start();
}
public float [] getBackgroundColor() {
return mBackgroundColor;
}
public void setBackgroundColor(float [] color) {
mBackgroundColor = color;
}
protected void renderBackground(GLCanvas view) {
if (mBackgroundColor != null) {
view.clearBuffer(mBackgroundColor);
}
if (mTransition != null && mTransition.isActive()) {
mTransition.applyBackground(this, view);
return;
}
}
protected void renderChild(GLCanvas canvas, GLView component) {
if (component.getVisibility() != GLView.VISIBLE
&& component.mAnimation == null) return;
int xoffset = component.mBounds.left - mScrollX;
int yoffset = component.mBounds.top - mScrollY;
canvas.translate(xoffset, yoffset);
CanvasAnimation anim = component.mAnimation;
if (anim != null) {
canvas.save(anim.getCanvasSaveFlags());
if (anim.calculate(AnimationTime.get())) {
invalidate();
} else {
component.mAnimation = null;
}
anim.apply(canvas);
}
component.render(canvas);
if (anim != null) canvas.restore();
canvas.translate(-xoffset, -yoffset);
}
protected boolean onTouch(MotionEvent event) {
return false;
}
protected boolean dispatchTouchEvent(MotionEvent event,
int x, int y, GLView component, boolean checkBounds) {
Rect rect = component.mBounds;
int left = rect.left;
int top = rect.top;
if (!checkBounds || rect.contains(x, y)) {
event.offsetLocation(-left, -top);
if (component.dispatchTouchEvent(event)) {
event.offsetLocation(left, top);
return true;
}
event.offsetLocation(left, top);
}
return false;
}
protected boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if (mMotionTarget != null) {
if (action == MotionEvent.ACTION_DOWN) {
MotionEvent cancel = MotionEvent.obtain(event);
cancel.setAction(MotionEvent.ACTION_CANCEL);
dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
mMotionTarget = null;
} else {
dispatchTouchEvent(event, x, y, mMotionTarget, false);
if (action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_UP) {
mMotionTarget = null;
}
return true;
}
}
if (action == MotionEvent.ACTION_DOWN) {
// in the reverse rendering order
for (int i = getComponentCount() - 1; i >= 0; --i) {
GLView component = getComponent(i);
if (component.getVisibility() != GLView.VISIBLE) continue;
if (dispatchTouchEvent(event, x, y, component, true)) {
mMotionTarget = component;
return true;
}
}
}
return onTouch(event);
}
public Rect getPaddings() {
return mPaddings;
}
public void layout(int left, int top, int right, int bottom) {
boolean sizeChanged = setBounds(left, top, right, bottom);
mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
// We call onLayout no matter sizeChanged is true or not because the
// orientation may change without changing the size of the View (for
// example, rotate the device by 180 degrees), and we want to handle
// orientation change in onLayout.
onLayout(sizeChanged, left, top, right, bottom);
}
private boolean setBounds(int left, int top, int right, int bottom) {
boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left)
|| (bottom - top) != (mBounds.bottom - mBounds.top);
mBounds.set(left, top, right, bottom);
return sizeChanged;
}
public void measure(int widthSpec, int heightSpec) {
if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec
&& (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) {
return;
}
mLastWidthSpec = widthSpec;
mLastHeightSpec = heightSpec;
mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
onMeasure(widthSpec, heightSpec);
if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
throw new IllegalStateException(getClass().getName()
+ " should call setMeasuredSize() in onMeasure()");
}
}
protected void onMeasure(int widthSpec, int heightSpec) {
}
protected void setMeasuredSize(int width, int height) {
mViewFlags |= FLAG_SET_MEASURED_SIZE;
mMeasuredWidth = width;
mMeasuredHeight = height;
}
public int getMeasuredWidth() {
return mMeasuredWidth;
}
public int getMeasuredHeight() {
return mMeasuredHeight;
}
protected void onLayout(
boolean changeSize, int left, int top, int right, int bottom) {
}
/**
* Gets the bounds of the given descendant that relative to this view.
*/
public boolean getBoundsOf(GLView descendant, Rect out) {
int xoffset = 0;
int yoffset = 0;
GLView view = descendant;
while (view != this) {
if (view == null) return false;
Rect bounds = view.mBounds;
xoffset += bounds.left;
yoffset += bounds.top;
view = view.mParent;
}
out.set(xoffset, yoffset, xoffset + descendant.getWidth(),
yoffset + descendant.getHeight());
return true;
}
protected void onVisibilityChanged(int visibility) {
for (int i = 0, n = getComponentCount(); i < n; ++i) {
GLView child = getComponent(i);
if (child.getVisibility() == GLView.VISIBLE) {
child.onVisibilityChanged(visibility);
}
}
}
protected void onAttachToRoot(GLRoot root) {
mRoot = root;
for (int i = 0, n = getComponentCount(); i < n; ++i) {
getComponent(i).onAttachToRoot(root);
}
}
protected void onDetachFromRoot() {
for (int i = 0, n = getComponentCount(); i < n; ++i) {
getComponent(i).onDetachFromRoot();
}
mRoot = null;
}
public void lockRendering() {
if (mRoot != null) {
mRoot.lockRenderThread();
}
}
public void unlockRendering() {
if (mRoot != null) {
mRoot.unlockRenderThread();
}
}
}
自定义Texture
ColorTexture
// ColorTexture 就是一个填充特定颜色的色块,代码很简单,包括颜色和尺寸,已经绘制的方法;
public class ColorTexture implements Texture {
private final int mColor;
private int mWidth;
private int mHeight;
public ColorTexture(int color) {
mColor = color;
mWidth = 1;
mHeight = 1;
}
@Override
public void draw(GLCanvas canvas, int x, int y) {
draw(canvas, x, y, mWidth, mHeight);
}
@Override
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
canvas.fillRect(x, y, w, h, mColor);
}
@Override
public boolean isOpaque() {
return Utils.isOpaque(mColor);
}
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
}
StringTexture
// StringTexture 提供了文本内容的绘制方法,可以设置文本内容、字体大小,颜色,也很简单;
public class StringTexture extends CanvasTexture {
private final String mText;
private final TextPaint mPaint;
private final FontMetricsInt mMetrics;
private StringTexture(String text, TextPaint paint,
FontMetricsInt metrics, int width, int height) {
super(width, height);
mText = text;
mPaint = paint;
mMetrics = metrics;
}
public static TextPaint getDefaultPaint(float textSize, int color) {
TextPaint paint = new TextPaint();
paint.setTextSize(textSize);
paint.setAntiAlias(true);
paint.setColor(color);
paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);
return paint;
}
public static StringTexture newInstance(
String text, float textSize, int color) {
return newInstance(text, getDefaultPaint(textSize, color));
}
public static StringTexture newInstance(
String text, float textSize, int color,
float lengthLimit, boolean isBold) {
TextPaint paint = getDefaultPaint(textSize, color);
if (isBold) {
paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
}
if (lengthLimit > 0) {
text = TextUtils.ellipsize(
text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
}
return newInstance(text, paint);
}
private static StringTexture newInstance(String text, TextPaint paint) {
FontMetricsInt metrics = paint.getFontMetricsInt();
int width = (int) Math.ceil(paint.measureText(text));
int height = metrics.bottom - metrics.top;
// The texture size needs to be at least 1x1.
if (width <= 0) width = 1;
if (height <= 0) height = 1;
return new StringTexture(text, paint, metrics, width, height);
}
@Override
protected void onDraw(Canvas canvas, Bitmap backing) {
canvas.translate(0, -mMetrics.ascent);
canvas.drawText(mText, 0, 0, mPaint);
}
}
BitmapTexture
// BitmapTexture 提供了图片的绘制方法,细节都在UploadedTexture里,这是处理起来最麻烦的Texture了
public class BitmapTexture extends UploadedTexture {
protected Bitmap mContentBitmap;
public BitmapTexture(Bitmap bitmap) {
this(bitmap, false);
}
public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
super(hasBorder);
Assert.assertTrue(bitmap != null && !bitmap.isRecycled());
mContentBitmap = bitmap;
}
@Override
protected void onFreeBitmap(Bitmap bitmap) {
if (!inFinalizer()) {
bitmap.recycle();
}
}
@Override
protected Bitmap onGetBitmap() {
return mContentBitmap;
}
public Bitmap getBitmap() {
return mContentBitmap;
}
}
说明
下一篇会细化介绍