(Android)如何加载100M的图片却不撑爆内存

面试题

  1. 图片的三级缓存中,图片加载到内存中,如果内存快爆了,会发生什么?怎么处理?
  2. 内存中如果加载一张 500*500 的 png 高清图片,应该是占用多少的内存?
  3. Bitmap 如何处理大图,如一张 30M 的大图,如何预防 OOM?

Android开发中,有时候会有加载巨图的需求,如何加载一个大图而不产生OOM呢,使用系统提供的BitmapRegionDecoder这个类可以很轻松的完成。

BitmapRegionDecoder:区域解码器,可以用来解码一个矩形区域的图像,有了这个我们就可以自定义一块矩形的区域,然后根据手势来移动矩形区域的位置就能慢慢看到整张图片了。

核心原理就是这么简单,不过做起来还是有一些细节处理,下面就一步一步的完成一个加载大图,支持拖动查看,双击放大,手势缩放的的自定义View。

大图加载实现

初始化变量

1
2
3
4
5
6
7
8
9
10
private void init() {
mOptions = new BitmapFactory.Options();
//滑动器
mScroller = new Scroller(getContext());
//所放器
mMatrix = new Matrix();
//手势识别
mGestureDetector = new GestureDetector(getContext(), this);
mScaleGestureDetector = new ScaleGestureDetector(getContext(), this);
}

BitmapFactory.Options我们很熟悉,用来配置Bitmap相关的参数,比如获取Bitmap的宽高,内存复用等参数。

GestureDetector用来识别双击事件,ScaleGestureDetector用来监听手指的缩放事件,都是系统提供的类,比较方便使用。

设置需要加载的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setImage(InputStream is) {
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, mOptions);
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
mOptions.inJustDecodeBounds = false;
try {
//区域解码器
mRegionDecoder = BitmapRegionDecoder.newInstance(is, false);
} catch (IOException e) {
e.printStackTrace();
}
requestLayout();
}

设置需要要加载的图片,无论图片放到哪里都可以拿到图片的一个输入流,所以参数使用输入流,通过BitmapFactory.Options拿到图片的真实宽高。

inPreferredConfig这个参数默认是Bitmap.Config.ARGB_8888,这里将它改成Bitmap.Config.RGB_565,去掉透明通道,可以减少一半的内存使用。最后初始化区域解码器BitmapRegionDecoder

ARGB_8888就是由4个8位组成即32位,RGB_565就是R为5位,G为6位,B为5位共16位

获取View的宽高,计算缩放值

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
mRect.top = 0;
mRect.left = 0;
mRect.right = (int) mViewWidth;
mRect.bottom = (int) mViewHeight;
mScale = mViewWidth/mImageWidth;
mCurrentScale = mScale;
}

onSizeChanged方法在布局期间,当此视图的大小发生更改时,将调用此方法,第一次在onMeasure之后调用,可以方便的拿到View的宽高。

然后给我们自定义的矩形mRect的上下左右的边界赋值。一般情况下我们使用这个自定义的View显示大图,都是占满这个View,所以这里矩形初始大小就让它跟View一样大。

mScale用来记录原始的缩放比,mCurrentScale用来记录当前的所方比,因为有双击放大和手势缩放,mCurrentScale随着手势变化。

绘制

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mRegionDecoder == null) {
return;
}
//复用内存
mOptions.inBitmap = mBitmap;
mBitmap = mRegionDecoder.decodeRegion(mRect, mOptions);
mMatrix.setScale(mCurrentScale, mCurrentScale);
canvas.drawBitmap(mBitmap, mMatrix, null);
}

绘制也很简单,通过区域解码器解码一个矩形的区域,返回一个Bitmap对象,然后通过canvas绘制Bitmap。需要注意mOptions.inBitmap = mBitmap;这个配置可以复用内存,保证内存的使用一直只是矩形的这块区域。

到这里运行就能绘制出一部分图片了,想要看全部的图片,需要手指拖动来看,这就需要处理各种事件了。

分发事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);

mScaleGestureDetector.onTouchEvent(event);
return true;
}

`onTouchEvent`中很简单,事件都交给两个手势检测器自己去处理。

### 处理GestureDetector中的事件

当手指按下的时候,如果图片正在飞速滑动,那么停止

```java
@Override
public boolean onDown(MotionEvent e) {
//如果正在滑动,先停止
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}

onScroll中处理滑,根据手指移动的参数,来移动矩形绘制区域,这里需要处理各个边界点,比如左边最小就为0,右边最大为图片的宽度,不能超出边界否则就报错了。

onFling方法中调用滑动器Scrollerfling方法来处理手指离开之后惯性滑动。惯性移动的距离在View的computeScroll()方法中计算,也需要注意边界问题,不要滑出边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//滑动的时候,改变mRect显示区域的位置
mRect.offset((int)distanceX,(int)distanceY);
//处理上下左右的边界
if (mRect.left < 0) {
mRect.left = 0;
mRect.right = (int) (mViewWidth/mCurrentScale);
}
if (mRect.right > mImageWidth) {
mRect.right = (int) mImageWidth;
mRect.left = (int) (mImageWidth-mViewWidth/mCurrentScale);
}
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int) (mViewHeight/mCurrentScale);
}
if (mRect.bottom > mImageHeight) {
mRect.bottom = (int) mImageHeight;
mRect.top = (int) (mImageHeight-mViewHeight/mCurrentScale);
}
invalidate();
return false;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(mRect.left, mRect.top, -(int)velocityX, -(int)velocityY, 0, (int)mImageWidth
, 0, (int)mImageHeight);
return false;
}

@Override
public void computeScroll() {
super.computeScroll();
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
if (mRect.top+mViewHeight/mCurrentScale < mImageHeight) {
mRect.top = mScroller.getCurrY();
mRect.bottom = (int) (mRect.top + mViewHeight/mCurrentScale);
}
if (mRect.bottom > mImageHeight) {
mRect.top = (int) (mImageHeight - mViewHeight/mCurrentScale);
mRect.bottom = (int) mImageHeight;
}
invalidate();
}
}

处理双击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public boolean onDoubleTap(MotionEvent e) {
//处理双击事件
if (mCurrentScale > mScale) {
mCurrentScale = mScale;
} else {
mCurrentScale = mScale*mMultiple;
}
mRect.right = mRect.left + (int)(mViewWidth/mCurrentScale);
mRect.bottom = mRect.top + (int)(mViewHeight/mCurrentScale);
//处理边界
if (mRect.left < 0) {
mRect.left = 0;
mRect.right = (int) (mViewWidth/mCurrentScale);
}
if (mRect.right > mImageWidth) {
mRect.right = (int) mImageWidth;
mRect.left = (int) (mImageWidth-mViewWidth/mCurrentScale);
}
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int) (mViewHeight/mCurrentScale);
}
if (mRect.bottom > mImageHeight) {
mRect.bottom = (int) mImageHeight;
mRect.top = (int) (mImageHeight-mViewHeight/mCurrentScale);
}

invalidate();
return true;
}

mMultiple为双击之后放大几倍,这里设置3倍。第一次双击放大3倍,第二次双击返回原状。缩放完成之后,需要根据当前的缩放比重新设置绘制区域的边界。最后也需要重新定位一下边界,因为如果使用两个手指放大之后,这时候双击返回原状,如果不处理边界,位置会出错。处理边界的代码可以抽取出来。

处理手指缩放事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public boolean onScale(ScaleGestureDetector detector) {
//处理手指缩放事件
//获取与上次事件相比,得到的比例因子
float scaleFactor = detector.getScaleFactor();
//mCurrentScale += scaleFactor-1;
mCurrentScale *= scaleFactor;
if (mCurrentScale > mScale*mMultiple) {
mCurrentScale = mScale*mMultiple;
} else if (mCurrentScale <= mScale) {
mCurrentScale = mScale;
}
mRect.right = mRect.left + (int)(mViewWidth/mCurrentScale);
mRect.bottom = mRect.top + (int)(mViewHeight/mCurrentScale);
invalidate();
return true;
}

@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
//当 >= 2 个手指碰触屏幕时调用,若返回 false 则忽略改事件调用
return true;
}

onScaleBegin方法需要返回true,否则无法检测到手势缩放。onScale方法中获取缩放因子,这个缩放因子是跟上次事件相比的出来的。所以这里使用*=,完成之后也需要重新设置绘制区域mRect的边界。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.Scroller;

import androidx.annotation.Nullable;

import java.io.IOException;
import java.io.InputStream;

/**
* 加载大图
*/
public class BigImageView extends View implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener,
ScaleGestureDetector.OnScaleGestureListener {

/**
* 图片的宽和高
*/
private float mImageWidth, mImageHeight;
/**
* 当前View的宽和高
*/
private float mViewWidth, mViewHeight;
/**
* 图片的缩放比
*/
private float mScale = 1;
private float mCurrentScale = 1;
/**
* 放大几倍
*/
private int mMultiple = 3;
/**
* 绘制区域
*/
private final Rect mRect = new Rect();
/**
* 分区域加载器
*/
private BitmapRegionDecoder mRegionDecoder;
private BitmapFactory.Options mOptions;
private Bitmap mBitmap;
/**
* 滑动器
*/
private Scroller mScroller;
/**
* 缩放矩阵
*/
private Matrix mMatrix;
/**
* 手势识别器
*/
private GestureDetector mGestureDetector;
private ScaleGestureDetector mScaleGestureDetector;

public BigImageView(Context context) {
super(context);
init();
}

public BigImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public BigImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
mOptions = new BitmapFactory.Options();
//滑动器
mScroller = new Scroller(getContext());
//所放器
mMatrix = new Matrix();
//手势识别
mGestureDetector = new GestureDetector(getContext(), this);
mScaleGestureDetector = new ScaleGestureDetector(getContext(), this);
}

public void setImage(InputStream is) {
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, mOptions);
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
mOptions.inJustDecodeBounds = false;
try {
//区域解码器
mRegionDecoder = BitmapRegionDecoder.newInstance(is, false);
} catch (IOException e) {
e.printStackTrace();
}
requestLayout();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
mRect.top = 0;
mRect.left = 0;
mRect.right = (int) mViewWidth;
mRect.bottom = (int) mViewHeight;
mScale = mViewWidth / mImageWidth;
mCurrentScale = mScale;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mRegionDecoder == null) {
return;
}
//复用内存
mOptions.inBitmap = mBitmap;
mBitmap = mRegionDecoder.decodeRegion(mRect, mOptions);
mMatrix.setScale(mCurrentScale, mCurrentScale);
canvas.drawBitmap(mBitmap, mMatrix, null);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);

mScaleGestureDetector.onTouchEvent(event);
return true;
}

@Override
public boolean onDown(MotionEvent e) {
//如果正在滑动,先停止
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}

@Override
public void onShowPress(MotionEvent e) {

}

@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//滑动的时候,改变mRect显示区域的位置
mRect.offset((int) distanceX, (int) distanceY);
//处理上下左右的边界
handleBorder();
invalidate();
return false;
}

private void handleBorder() {
if (mRect.left < 0) {
mRect.left = 0;
mRect.right = (int) (mViewWidth / mCurrentScale);
}
if (mRect.right > mImageWidth) {
mRect.right = (int) mImageWidth;
mRect.left = (int) (mImageWidth - mViewWidth / mCurrentScale);
}
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int) (mViewHeight / mCurrentScale);
}
if (mRect.bottom > mImageHeight) {
mRect.bottom = (int) mImageHeight;
mRect.top = (int) (mImageHeight - mViewHeight / mCurrentScale);
}
}

@Override
public void onLongPress(MotionEvent e) {

}

@Override
public void computeScroll() {
super.computeScroll();
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
if (mRect.top + mViewHeight / mCurrentScale < mImageHeight) {
mRect.top = mScroller.getCurrY();
mRect.bottom = (int) (mRect.top + mViewHeight / mCurrentScale);
}
if (mRect.bottom > mImageHeight) {
mRect.top = (int) (mImageHeight - mViewHeight / mCurrentScale);
mRect.bottom = (int) mImageHeight;
}
invalidate();
}
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(mRect.left, mRect.top, -(int) velocityX, -(int) velocityY, 0, (int) mImageWidth
, 0, (int) mImageHeight);
return false;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}

@Override
public boolean onDoubleTap(MotionEvent e) {
//处理双击事件
if (mCurrentScale > mScale) {
mCurrentScale = mScale;
} else {
mCurrentScale = mScale * mMultiple;
}
mRect.right = mRect.left + (int) (mViewWidth / mCurrentScale);
mRect.bottom = mRect.top + (int) (mViewHeight / mCurrentScale);
//处理上下左右的边界
handleBorder();
invalidate();
return true;
}

@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}

@Override
public boolean onScale(ScaleGestureDetector detector) {
//处理手指缩放事件
//获取与上次事件相比,得到的比例因子
float scaleFactor = detector.getScaleFactor();
// mCurrentScale+=scaleFactor-1;
mCurrentScale *= scaleFactor;
if (mCurrentScale > mScale * mMultiple) {
mCurrentScale = mScale * mMultiple;
} else if (mCurrentScale <= mScale) {
mCurrentScale = mScale;
}
mRect.right = mRect.left + (int) (mViewWidth / mCurrentScale);
mRect.bottom = mRect.top + (int) (mViewHeight / mCurrentScale);
invalidate();
return true;
}

@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
//当 >= 2 个手指碰触屏幕时调用,若返回 false 则忽略改事件调用
return true;
}

@Override
public void onScaleEnd(ScaleGestureDetector detector) {

}
}

本文转载至:https://www.jianshu.com/p/ca4e086a8b1d

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :