前言
BitmapRegionDecoder类是Android系统提供的用来操作超大图片的工具类,它能够根据用户指定的区域大小部分加载图片数据。我们知道Android里的图片加载是有要求的,必须要小于某个特定阈值,如果图片尺寸特别大解析到内存中就会超出这个阈值导致图片无法加载,BitmapRegionDecoder类能够很好的辅助开发者在手机小屏幕上展示超大尺寸图
实现过程
先从网络上下载一份超大的《千里江山图》,直接作为图片资源设置到ImageView对象上,测试手机N5展示空白,并且在日志区提示如下日志,也就是说图片文件尺寸太大无法直接加载到内存中。
Bitmap too large to be uploaded into a texture (25245x1000, max=4096x4096)
现在使用自定义的ImageView控件来分块加载大图的部分,每次只加载不大于控件大小的部分来展示,通过监听用户在屏幕上的拖动来更新当前展示窗口里的图片部分,这样就好像把整幅图片都加在到内存中了。
在自定以的大图展示控件初始化时先定义BitmapRegionDecoder工具对象,这里把大图片资源放入到assets目录中,就可以通过InputStream输入流来读取图片数据。接着通过设置BitmapFactory.Options来只读取图片的长宽而不读取数据,记录下当前图片的实际尺寸。
public LargeImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
try {
InputStream inputStream = context.getAssets().open("long_picture.jpg");
// 初始化BitmapRegionDecoder
mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
// 设置只读取边界范围获取图片大小
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
// 记录图片大小
mPictureWidth = options.outWidth;
mPictureHeight = options.outHeight;
} catch (IOException e) {
e.printStackTrace();
}
mRect = new Rect();
mOptions = new BitmapFactory.Options();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
在View.onSizeChanged方法里会得到当前展示大图片的ImageView在屏幕上的宽高,通过图片宽高和控件宽高确定实际展示的部分图片宽高,在确定完展示部分图片的大小后先从大图中间切割一块图片出来展示在屏幕上。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
// 实际展示部分的宽高
mShowWidth = mPictureWidth > mViewWidth ? mViewWidth : mPictureWidth;
mShowHeight = mPictureHeight > mViewHeight ? mViewHeight : mPictureHeight;
// 展示图片部分左上角在大图里的坐标
mPortX = (mPictureWidth - mShowWidth) / 2;
mPortY = (mPictureHeight - mShowHeight) / 2;
mBitmap = Bitmap.createBitmap(mShowWidth, mShowHeight, Bitmap.Config.ARGB_8888);
loadPartial();
}
// 加载展示部分图片数据
private void loadPartial() {
mRect.set(mPortX, mPortY, mPortX + mShowWidth, mPortY + mShowHeight);
mBitmap = mDecoder.decodeRegion(mRect, mOptions);
}
// 在绘制函数里将加载的部分图片展示在控件里
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
}
接下来需需要监听用户的触摸操作,如果用户的手指移动超出点击范围就实在拖动整个界面,这时候需要根据用户移动的距离调整展示部分左上角在大图的位置,得到新位置后在调用BitmapRegionDecoder从本地获取新位置的内容并刷新当前控件。
// 记录用户上一次手指所在位置
private int mLastX;
private int mLastY;
// 记录用户手指按下的位置
private int mMotionX;
private int mMotionY;
// 用户是否在拖动
private boolean mIsDragging = false;
private int mTouchSlop;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX(), y = (int) event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 记录按下位置
mLastX = mMotionX = x;
mLastY = mMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
// 如果用户刚按下并且移动距离超出了点击范围,确认用户在拖动
if (!mIsDragging && isDragging(x - mMotionX, y - mMotionY)) {
mIsDragging = true;
}
if (mIsDragging) {
int dx = x - mLastX;
int dy = y - mLastY;
boolean changed = false;
// 图片能够横向拖动,就做横向拖动
if (canHorizontalMove() && mPortX - dx >= 0
&& mPortX - dx <= mPictureWidth - mShowWidth) {
mPortX -= dx;
changed = true;
}
// 图片能够做竖向拖动,就做竖向拖动
if (canVerticalMove() && mPortY - dy >= 0 &&
mPortY - dy <= mPictureHeight - mShowHeight) {
mPortY -= dy;
changed = true;
}
// 如果确定拖动有效,就更新展示图片数据并刷新控件
if (changed) {
loadPartial();
postInvalidate();
}
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsDragging = false;
break;
}
return true;
}
private boolean isDragging(int dx, int dy) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
}
private boolean canVerticalMove() {
return mPictureHeight > mViewHeight;
}
private boolean canHorizontalMove() {
return mPictureWidth > mViewWidth;
}