当我们在需要加载大图,长图的时候。如果,我们把整张图片都加载进去的话,很可能会OOM。
因为,我们的手机屏幕是有限的。所以,我们可以只加载显示的部分就可以了,这样就需要用到局部加载。
需求分析:
其实除了上面的东西,我们也需要图片的宽高,view的宽高等等属性。
下面,我们就开始编写:
我们先来定义这个类的属性
//要加载的局部图片的矩形
private Rect mRect;
//设置内存复用。
private BitmapFactory.Options mOptions;
//手势识别
private GestureDetector mGestureDetector;
//处理图片的滑动
private Scroller mScroller;
//图片的实际宽高
private int mImageWidth;
private int mImageHeight;
//图片的局部加载类
private BitmapRegionDecoder mDecoder;
//View的宽高
private int mViewWidth;
private int mViewHeight;
//要复用的Bitmap
private Bitmap mBitmap;
//图片的缩放大小
private float mScale;
这里的属性,主要就是开始分析时,用到的那些属性。
下面,我们就要初始化这些属性了
public class LongImageView extends View implements GestureDetector.OnGestureListener,View.OnTouchListener {
public LongImageView(Context context) {
this(context, null);
}
public LongImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LongImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
//需要绘制的图片矩形
mRect = new Rect();
//手势识别
mGestureDetector = new GestureDetector(context, this);
mScroller = new Scroller(context);
mOptions = new BitmapFactory.Options();
setOnTouchListener(this);
}
这里,我们主要是初始化要绘制的图片矩形,及图片滑动时候的手势(GestureDetector)及滑动处理(Scroller)。
这个方法,主要是设置要查看的图片的流
public void setImage(InputStream inputStream) {
//只解码,不返回bitmap。可以获取图片的宽高等属性
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, mOptions);
//拿到图片的真实宽高
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//设置图片内存复用
mOptions.inMutable = true;
//图片格式
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
mOptions.inJustDecodeBounds = false;
try {
//初始化BitmapRegionDecoder。用于显示某一区域的图片
mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
} catch (IOException e) {
e.printStackTrace();
}
requestLayout();
}
这里,主要是通过流获取图片的真实宽高,设置图片可复用,及初始化BitmapRegionDecoder。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取View的大小
mViewWidth = getMeasuredWidth();
mViewHeight = getMeasuredHeight();
//确定加载图片的区域
mRect.left = 0;
mRect.top = 0;
mRect.right = mImageWidth;
//设置缩放的比例
mScale = mViewWidth / (float) mImageWidth;
mRect.bottom = (int) (mViewHeight / mScale);
}
这里,我们主要是
这里,就是要绘制的图片的局部区域。
private Matrix matrix=new Matrix();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDecoder == null) {
return;
}
//设置要复用的bitmap
mOptions.inBitmap = mBitmap;
//指定显示绘制的局部区域
mBitmap = mDecoder.decodeRegion(mRect, mOptions);
//把矩形的大小,通过缩放的大小,设置成控件的大小
matrix.reset();
matrix.setScale(mScale, mScale);
//绘制bitmap
canvas.drawBitmap(mBitmap, matrix, null);
}
这里,主要是:
这里,主要是图片在滑动过程中的处理。
@Override
public boolean onTouch(View v, MotionEvent event) {
//设置GestureDetector手势监听
return mGestureDetector.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent e) {
//只有接受down事件,才能接受后续事件
//如果,图片在滚动。强制停止滚动事件
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//更改绘制矩形的位置
mRect.offset(0, (int) distanceY);
//处理临界位置
// 到最下面的话
if (mRect.bottom > mImageHeight) {
mRect.bottom = mImageHeight;
mRect.top = (int) (mImageHeight - (mViewHeight / mScale));
}
//到最上面的话
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int) (mViewHeight / mScale);
}
invalidate();
return false;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.isFinished()) {
return;
}
//true,表示当前滑动没有结束。我们需要改变绘制矩形的区域
if (mScroller.computeScrollOffset()) {
mRect.top = mScroller.getCurrY();
mRect.bottom = mRect.top + (int) (mViewHeight / mScale);
invalidate();
}
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(0, mRect.top,
0, (int) -velocityY,
0, 0,
0, mImageHeight - (int) (mViewHeight / mScale));
return false;
}
这里,主要就是设置图片,在滑动过程中的位置,及绘制区域的计算。
到这里,我们的自定义控件,就处理完了。
通过上面的代码,我们的自定义控件就写完了。
但是,当我们第二次,通过setImage()设置图片的时候,就报错啦
java.lang.IllegalArgumentException: Problem decoding into existing bitmap
at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:626)
at com.liu.testproject.img.widget.LongImageView.setImage(LongImageView.java:84)
at com.liu.testproject.img.LongImageTestActivity$2.onClick(LongImageTestActivity.java:54)
at android.view.View.performClick(View.java:5637)
at android.view.View$PerformClick.run(View.java:22429)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6169)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:891)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:781)
我们查看下BitmapFactory#decodeStream()方法,具体报错的原因
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
@Nullable Options opts) {
......
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
......
return bm;
}
通过源码,我们知道,当我们第二次设置图片的时候,我们并没有清空BitmapFactory.Options这个类。所以,才引起的异常。
那么,我们就再写个reset()的方法,在每次设置图片的时候,来释放Option资源。
private void reset() {
//释放复用的Bitmap
if (mBitmap != null) {
mBitmap.recycle();
mBitmap = null;
}
//释放Decoder
if (mDecoder != null) {
mDecoder.recycle();
mDecoder = null;
}
//释放Options的资源
if (mOptions != null) {
if (mOptions.inBitmap != null) {
mOptions.inBitmap.recycle();
mOptions.inBitmap = null;
}
mOptions = null;
}
if (mScroller != null && !mScroller.isFinished()) {
mScroller.forceFinished(true);
}
}
把这个方法,添加在设置图片的方法上。
public void setImage(InputStream inputStream) {
reset();
mOptions = new BitmapFactory.Options();
//只解码,不返回bitmap。可以获取图片的宽高等属性
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, mOptions);
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//内存复用
mOptions.inMutable = true;
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
mOptions.inJustDecodeBounds = false;
try {
//这里放到上面的话,就拿不到图片宽高了
mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
} catch (IOException e) {
e.printStackTrace();
}
//刷新
requestLayout();
}
到这里,我们的自定义加载长图的控件就写完啦,下面通过调用看看下过吧
控件调用
InputStream is;
try {
is = getAssets().open("and_long_one.jpg");
mLongImageView.setImage(is);
} catch (IOException e) {
e.printStackTrace();
}
查看效果
public class LongImageView extends View implements GestureDetector.OnGestureListener,View.OnTouchListener {
//加载的矩形
private Rect mRect;
//设置内存复用
private BitmapFactory.Options mOptions;
//手势识别
private GestureDetector mGestureDetector;
private Scroller mScroller;
private int mImageWidth;
private int mImageHeight;
private BitmapRegionDecoder mDecoder;
private int mViewWidth;
private int mViewHeight;
private Bitmap mBitmap;
private float mScale;
public LongImageView(Context context) {
this(context, null);
}
public LongImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LongImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mRect = new Rect();
mGestureDetector = new GestureDetector(context, this);
mScroller = new Scroller(context);
setOnTouchListener(this);
}
public void setImage(InputStream inputStream) {
reset();
mOptions = new BitmapFactory.Options();
//只解码,不返回bitmap。可以获取图片的宽高等属性
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, mOptions);
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//内存复用
mOptions.inMutable = true;
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
mOptions.inJustDecodeBounds = false;
try {
mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
} catch (IOException e) {
e.printStackTrace();
}
//刷新
requestLayout();
}
private void reset() {
if (mBitmap != null) {
mBitmap.recycle();
mBitmap = null;
}
if (mDecoder != null) {
mDecoder.recycle();
mDecoder = null;
}
if (mOptions != null) {
if (mOptions.inBitmap != null) {
mOptions.inBitmap.recycle();
mOptions.inBitmap = null;
}
mOptions = null;
}
if (mScroller != null && !mScroller.isFinished()) {
mScroller.forceFinished(true);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mViewWidth = getMeasuredWidth();
mViewHeight = getMeasuredHeight();
//确定加载图片的区域
mRect.left = 0;
mRect.top = 0;
mRect.right = mImageWidth;
//设置缩放的比例
mScale = mViewWidth / (float) mImageWidth;
mRect.bottom = (int) (mViewHeight / mScale);
}
private Matrix matrix=new Matrix();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDecoder == null) {
return;
}
mOptions.inBitmap = mBitmap;
//指定解码区域
mBitmap = mDecoder.decodeRegion(mRect, mOptions);
matrix.reset();
matrix.setScale(mScale, mScale);
canvas.drawBitmap(mBitmap, matrix, null);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent e) {
//只有接受down事件,才能接受后续事件
//强制停止滚动事件
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//更改绘制矩形的位置
mRect.offset(0, (int) distanceY);
//处理临界位置
// 到最下面的话
if (mRect.bottom > mImageHeight) {
mRect.bottom = mImageHeight;
mRect.top = (int) (mImageHeight - (mViewHeight / mScale));
}
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int) (mViewHeight / mScale);
}
invalidate();
return false;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.isFinished()) {
return;
}
//true,表示当前滑动没有结束。我们需要改变绘制矩形的区域
if (mScroller.computeScrollOffset()) {
mRect.top = mScroller.getCurrY();
mRect.bottom = mRect.top + (int) (mViewHeight / mScale);
invalidate();
}
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(0, mRect.top,
0, (int) -velocityY,
0, 0,
0, mImageHeight - (int) (mViewHeight / mScale));
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
}