大尺寸图片加载

通常情况下,图片的原始尺寸要比 ImageView 尺寸大,这时候把整张图片加载进 ImageView 会造成内存内存浪费,甚至造成 OOM (Out Of Memory Killer)。

在 Android App 中,为什么加载位图(Bitmap)是很棘手,是有很多原因的:

  • Bitmap 可以很轻松的消耗 App 的内存预算(Android 分配给每个 APP 的最大内存)。例如,Pixel 手机上的相机可拍摄高达 4048x3036 像素(1200万像素)如果 bitmap 配置使用 ARGB_8888(每个像素占 4 字节)—— Android 2.3(API 9 版本)及以上版本的默认值,将这张照片加载到内存中需要大约48MB 的内存(4048 * 3036 * 4 字节)。如此大的内存能够立即耗尽 App 可用的所有的内存。
  • 在 UI 线程中加载 bitmap 会降低你的 App 的性能,引发响应缓慢甚至发生 ANR (Application Not Responding) 的信息。因此,在使用 bitmap 时适当的管理线程是非常重要的。
  • 如果你的 App 正在加载多张 bitmap 到内存中,你需要熟练的管理内存和磁盘缓存。否则,你的 App 可能要忍受响应性和滑动性差的体验

多数情况下,我们推荐你在 App 中,使用 Glide 框架获得,解析展示 bitmap。在 Android中,Glide 抽象出来很多复杂的 bitmap 和其他图像和其他任务的相关工作。

然而,在使用开源框架的同时,我们仍然要明白一个像 Glide 这样优秀的框架要做哪些事情。

图片采样率放缩

正如文章开头展示的情形,我们可以使用BitmapFactory.Options按照一定的采样率加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就能降低内存占用,在一定程度上避免 OOM,提高 bitmap 加载时候的性能。

BitmapFactory有一个参数:inSampleSize(采样率),这个值为 1 时,那么采样图片大小等于原始图片大小。为 2 时,那么采样后图片宽高均为原始图片的1/2,像素为原图的1/4,占有的内存大小为原图的1/4。

如前边的例子,放缩后,图片大约需要 12MB 的内存。

12MB 依然很大,我们还可以继续压缩,放缩规则是这样的:inSampleSize 是必须大于 1 的整数才有效果,小与 1 就相当于 1,并且同时作用于宽高,所以缩放后的图片大小以采样率的 2 次方形式递减。根据最新的官方文档,inSampleSize 的取值应该总是为 2 的指数,若给系统的 inSampleSize 不为 2 的指数,那么系统会向下取整并且选择一个最接近 2 的指数来代替,不过经过验证,这个结论并不是在所有的Android 版本上都成立。

如何使用采样率

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
InputStream is = getAssets().open("big_picture.jpg");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
int width = options.outWidth;
int height = options.outHeight;
Log.i(TAG, "origin picture: width: " + width + " height: " + height);
is = getAssets().open("big_picture.jpg");
options = new BitmapFactory.Options();
options.inSampleSize = 2;
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is,null,options);
int scaleWidth = options.outWidth;
int scaleHeight = options.outHeight;
Log.i(TAG, "origin picture: scaleWidth: " + scaleWidth + " scaleHeight: " + scaleHeight);

打印结果如下:

1
2
origin picture: width: 690 height: 12287
origin picture: scaleWidth: 345 scaleHeight: 6144

由结果可知,放缩采样率后,图片的宽高都缩小了 2 倍。

在实际使用中,需要计算出合适的 inSampleSize 参数,以保证节省内存,这部分内容在 Android 开发艺术探索中 描述的很轻出,我就不班门弄斧了。

大图片展示

由上述结果我们可以发现,这幅图片的高度实在是太大,这样的图片加载到应用是这个样子的

加载大图片

这样的图片是根本看不清的,所以加载大图片的方式略有不同,需要借助一个类 BitmapRegionDecoder

初识 BitmapRegionDecoder

BitmapRegionDecoder 主要用于显示图片的某一块矩形区域,如果你需要显示某个图片的指定区域,那么这个类非常合适。

  • BitmapRegionDecoder 提供了一系列的 newInstance 方法来构造对象,支持传入文件路径,文件描述符,文件的 inputstrem 等。
1
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
  • 上述解决了传入我们需要处理的图片,那么接下来就是显示指定的区域。
1
bitmapRegionDecoder.decodeRegion(rect, options);

参数一很明显是一个rect,参数二是BitmapFactory.Options,你可以控制图片的inSampleSize,inPreferredConfig等。

下面是一个简单例子的核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
InputStream is = getAssets().open("big_picture.jpg");
//获得图片的宽、高
BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
tmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, tmpOptions);
int width = tmpOptions.outWidth;
int height = tmpOptions.outHeight;
//设置显示图片的中心区域
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 200, height / 2 - 200, width / 2 + 200, height / 2 + 200), options);
mImageView.setImageBitmap(bitmap);

以上代码是显示 big_picture 中间 400 * 400 的区域。

自定义显示大图控件

根据上面的分析,我们的自定义控件思路就非常清晰了:

  • 提供一个设置图片的入口
  • 重写 onTouchEvent,在里面根据用户移动的手势,去更新显示区域的参数
  • 每次更新区域参数后,调用 invalidate,onDraw 里面去regionDecoder.decodeRegion 拿到 bitmap,去 draw

以下是自定义 View 的源码:

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
public class LargeImageView extends View {
private static final String TAG = "LargeImageView";
private BitmapRegionDecoder mDecoder;
/**
* 图片的宽度和高度
*/
private int mImageWidth, mImageHeight;
/**
* 绘制的区域
*/
private volatile Rect mRect = new Rect();
private MoveGestureDetector mDetector;
private static final BitmapFactory.Options options = new BitmapFactory.Options();
static {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
public void setInputStream(InputStream is) {
try {
BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
tmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, tmpOptions);
mImageWidth = tmpOptions.outWidth;
mImageHeight = tmpOptions.outHeight;
mDecoder = BitmapRegionDecoder.newInstance(is, false);
requestLayout();
invalidate();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void init() {
mDetector = new MoveGestureDetector(getContext(), new MoveGestureDetector
.SimpleMoveGestureDetector() {
@Override
public boolean onMove(MoveGestureDetector detector) {
int moveX = (int) detector.getMoveX();
int moveY = (int) detector.getMoveY();
if (mImageWidth > getWidth()) {
mRect.offset(-moveX, 0);
checkWidth();
invalidate();
}
if (mImageHeight > getHeight()) {
mRect.offset(0, -moveY);
checkHeight();
invalidate();
}
return true;
}
});
}
private void checkWidth() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.right > imageWidth) {
rect.right = imageWidth;
rect.left = imageWidth - getWidth();
}
if (rect.left < 0) {
rect.left = 0;
rect.right = getWidth();
}
}
private void checkHeight() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.bottom > imageHeight) {
rect.bottom = imageHeight;
rect.top = imageHeight - getHeight();
}
if (rect.top < 0) {
rect.top = 0;
rect.bottom = getHeight();
}
}
public LargeImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDetector.onTouchEvent(event);
return true;
}
@Override
protected void onDraw(Canvas canvas) {
Bitmap bm = mDecoder.decodeRegion(mRect, options);
canvas.drawBitmap(bm, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
//默认直接显示图片的顶部区域
mRect.left = 0;
mRect.top = 0;
mRect.right = mRect.left + imageWidth;
mRect.bottom = mRect.top + height;
}
}

注意: 32 行的代码,如果它的位置放在了获取宽高之前,会导致宽高无法获取,结果会为 -1,将此行加粗希望读者不要在掉坑里了。

根据上述源码:

  1. setInputStream 里面去获得图片的真实的宽度和高度,以及初始化我们的 mDecoder
  2. onMeasure 里面为我们的显示区域的 rect 赋值,大小为 view 的尺寸
  3. onTouchEvent 里面我们监听 move 的手势,在监听的回调里面去改变 rect 的参数,以及做边界检查,最后 invalidate
  4. 在 onDraw 里面就是根据 rect 拿到 bitmap,然后 draw 了

ok,上面并不复杂,不过大家有没有注意到,这个监听用户move手势的代码写的有点奇怪,恩,这里模仿了系统的 ScaleGestureDetector,编写了MoveGestureDetector,代码如下:

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
public class MoveGestureDetector extends BaseGestureDetector
{
private PointF mCurrentPointer;
private PointF mPrePointer;
//仅仅为了减少创建内存
private PointF mDeltaPointer = new PointF();
//用于记录最终结果,并返回
private PointF mExtenalPointer = new PointF();
private OnMoveGestureListener mListenter;
public MoveGestureDetector(Context context, OnMoveGestureListener listener)
{
super(context);
mListenter = listener;
}
@Override
protected void handleInProgressEvent(MotionEvent event)
{
int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
switch (actionCode)
{
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mListenter.onMoveEnd(this);
resetState();
break;
case MotionEvent.ACTION_MOVE:
updateStateByEvent(event);
boolean update = mListenter.onMove(this);
if (update)
{
mPreMotionEvent.recycle();
mPreMotionEvent = MotionEvent.obtain(event);
}
break;
}
}
@Override
protected void handleStartProgressEvent(MotionEvent event)
{
int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
switch (actionCode)
{
case MotionEvent.ACTION_DOWN:
resetState();//防止没有接收到CANCEL or UP ,保险起见
mPreMotionEvent = MotionEvent.obtain(event);
updateStateByEvent(event);
break;
case MotionEvent.ACTION_MOVE:
mGestureInProgress = mListenter.onMoveBegin(this);
break;
}
}
protected void updateStateByEvent(MotionEvent event)
{
final MotionEvent prev = mPreMotionEvent;
mPrePointer = caculateFocalPointer(prev);
mCurrentPointer = caculateFocalPointer(event);
//Log.e("TAG", mPrePointer.toString() + " , " + mCurrentPointer);
boolean mSkipThisMoveEvent = prev.getPointerCount() != event.getPointerCount();
//Log.e("TAG", "mSkipThisMoveEvent = " + mSkipThisMoveEvent);
mExtenalPointer.x = mSkipThisMoveEvent ? 0 : mCurrentPointer.x - mPrePointer.x;
mExtenalPointer.y = mSkipThisMoveEvent ? 0 : mCurrentPointer.y - mPrePointer.y;
}
/**
* 根据event计算多指中心点
*
* @param event
* @return
*/
private PointF caculateFocalPointer(MotionEvent event)
{
final int count = event.getPointerCount();
float x = 0, y = 0;
for (int i = 0; i < count; i++)
{
x += event.getX(i);
y += event.getY(i);
}
x /= count;
y /= count;
return new PointF(x, y);
}
public float getMoveX()
{
return mExtenalPointer.x;
}
public float getMoveY()
{
return mExtenalPointer.y;
}
public interface OnMoveGestureListener
{
public boolean onMoveBegin(MoveGestureDetector detector);
public boolean onMove(MoveGestureDetector detector);
public void onMoveEnd(MoveGestureDetector detector);
}
public static class SimpleMoveGestureDetector implements OnMoveGestureListener
{
@Override
public boolean onMoveBegin(MoveGestureDetector detector)
{
return true;
}
@Override
public boolean onMove(MoveGestureDetector detector)
{
return false;
}
@Override
public void onMoveEnd(MoveGestureDetector detector)
{
}
}
}
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
public abstract class BaseGestureDetector {
protected boolean mGestureInProgress;
protected MotionEvent mPreMotionEvent;
protected MotionEvent mCurrentMotionEvent;
protected Context mContext;
public BaseGestureDetector(Context context) {
mContext = context;
}
public boolean onTouchEvent(MotionEvent event) {
if (!mGestureInProgress) {
handleStartProgressEvent(event);
} else {
handleInProgressEvent(event);
}
return true;
}
protected abstract void handleInProgressEvent(MotionEvent event);
protected abstract void handleStartProgressEvent(MotionEvent event);
protected abstract void updateStateByEvent(MotionEvent event);
protected void resetState() {
if (mPreMotionEvent != null) {
mPreMotionEvent.recycle();
mPreMotionEvent = null;
}
if (mCurrentMotionEvent != null) {
mCurrentMotionEvent.recycle();
mCurrentMotionEvent = null;
}
mGestureInProgress = false;
}
}

不过值得一提的是:上面这个手势检测的写法,不是我想的,而是一个开源的项目https://github.com/rharter/android-gesture-detectors,里面包含很多的手势检测。对应的博文是:http://code.almeros.com/android-multitouch-gesture-detectors#.VibzzhArJXg 我是偷学鸿洋大神的。

测试

1
2
3
4
5
6
7
8
9
10
11
12
<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">
<com.zhy.blogcodes.largeImage.view.LargeImageView
android:id="@+id/id_largetImageview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LargeImageViewActivity extends AppCompatActivity
{
private LargeImageView mLargeImageView;
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_large_image_view);
mLargeImageView = (LargeImageView) findViewById(R.id.id_largetImageview);
try{
InputStream inputStream = getAssets().open("big_picture.jpg");
mLargeImageView.setInputStream(inputStream);
} catch (IOException e){
e.printStackTrace();
}
}
}

最后如上一张效果图:

加载大图效果图

结束,感谢您的阅读。

坚持原创技术分享,您的支持将鼓励我继续创作!