前言
在Android开发中,有时候会有加载巨图(长图)的需求,可以上下左右滑动,双击放大或缩小,手指缩放。如何加载一个大图而不产生OOM呢?使用体统提供的BitmapRegionDecoder,区域解码器,可以用来解码一个矩形区域(Rect)的图像,有了这个类我们就可以自定义一块矩形的区域,然后根据手势来移动矩形区域的位置就能慢慢看到整张图片了。
效果演示:
1.初始化变量
public BigImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//第1步:设置bigImageView需要的成员变量
//加载显示的区域
mRect = new Rect();
mOptions = new BitmapFactory.Options();
//手勢识别
mGestureDetector = new GestureDetector(context, this);
//滚动类
mScroller = new Scroller(context);
//缩放手势识别
mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGesture() );
//设置触摸监听
setOnTouchListener(this);
}
BitmapFactory.Options我们很熟悉,用来配置Bitmap相关的参数,比如获取Bitmap的宽高,内存复用等参数。
GestureDetector用来识别双击事件,ScaleGestureDetector用来监听手指的缩放事件,都是系统提供的类,比较方便使用。
2.设置需要加载的图片
public void setImage(InputStream is) {
//获取图片的信息,不会将整张图片加载到内存
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, mOptions);
//获取图片的宽高信息
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//开启内存复用
mOptions.inMutable = true;
//设置像素格式
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
//真正意义加载图片
mOptions.inJustDecodeBounds = false;
try {
//创建一个区域解码器
mDecoder = BitmapRegionDecoder.newInstance(is, false);
} catch (IOException e) {
e.printStackTrace();
}
//刷新页面,与invalidate方法相反,只会触发onMeasure和onLayout方法,不会触发onDraw
requestLayout();
}
设置需要要加载的图片,无论图片放到哪里都可以拿到图片的一个输入流,所以参数使用输入流,通过BitmapFactory.Options拿到图片的真实宽高。
接下来是设置inMutable,开启内存复用。
关于像素格式设置,inPreferredConfig这个参数默认是Bitmap.Config.ARGB_8888,这里将它改成Bitmap.Config.RGB_565,去掉透明通道,可以减少一半的内存使用。
最后初始化区域解码器BitmapRegionDecoder。需要注意的是newInstance方法中参数false表示不共享输入流,可以理解为BitmapRegionDecoder拷贝了一份输入流,即获得的图片输入流close了,仍然可以进行解码操作。
requestLayout()刷新页面,需要注意与invalidate()的区别,后者请求重绘View树,即onDraw方法,不会触发onMeasure和onLayout方法,刚好相反。
3.测量控件View的宽高,计算缩放因子/比例
@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;
//得到图片的宽度,就能根据view的宽度计算缩放因子/比例
mScale = mViewWidth/(float)mImageWidth;
mRect.bottom = (int)(mViewHeight/mScale);*/
//加了缩放手势之后的逻辑
mRect.left = 0;
mRect.top = 0;
mRect.right = Math.min(mImageWidth, mViewWidth);
mRect.bottom = Math.min(mImageHeight, mViewHeight);
//再定义一个缩放因子
originalScale = mViewWidth / (float)mImageWidth;
mScale = originalScale;
}
mImageWidth为通过前面的mOptions解析的图片的宽度。
mImageHeight为通过前面的mOptions解析的图片的高度。
mViewWidth为要显示图片的控件的测量宽度
mViewHeight为要显示图片的控件的测量高度
mRect设置left、top、right、bottom等参数来决定要显示的区域。缩放比例就是mViewWidth/mImageWidth, 等比缩放,根据比例竖直方向显示对应的高度。
4.绘制/画出具体内容
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDecoder == null) {
return;
}
mBitmap = mDecoder.decodeRegion(mRect, mOptions);
//复用内存
mOptions.inBitmap = mBitmap;
//没有双击放大缩小的时候
//mMatrix.setScale(mScale,mScale);
//需要双击放大或缩小的时候
mMatrix.setScale(mViewWidth/(float)mRect.width(),mViewWidth/(float)mRect.width());
canvas.drawBitmap(mBitmap, mMatrix, null);
}
绘制也很简单,通过区域解码器解码一个矩形的区域,返回一个Bitmap对象,然后通过canvas绘制Bitmap。需要注意mOptions.inBitmap = mBitmap;这个配置可以复用内存,保证内存的使用一直只是矩形的这块区域。3.0~4.4版本是占用内存相等才能复用,4.4之后只要加载图片内存小于可复用内存就可以复用。此处就不过多介绍了。
到这里运行就能绘制出一部分图片了,想要看全部的图片,需要手指拖动来看,这就需要处理各种事件了。
5.处理点击事件
@Override
public boolean onTouch(View v, MotionEvent event) {
//直接将事件传递给手势事件
mGestureDetector.onTouchEvent(event);
//传递给双击事件
mScaleGestureDetector.onTouchEvent(event);
return true;
}
6.处理GestureDetector中的事件
@Override
public boolean onDown(MotionEvent e) {
//如果移动没有停止,就强行停止
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}
此处处理的是手按下事件,如果图片滑动没有停止,就强行停止
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//上下移动的时候,mRect需要改变现实区域
mRect.offset((int)distanceX, (int)distanceY);
//向上滑动边界处理
if (mRect.bottom > mImageHeight) {
mRect.bottom = mImageHeight;
mRect.top = mImageHeight - (int)(mViewHeight/mScale);
}
//向下滑动边界处理
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int)(mViewHeight/mScale);
}
//向左滑动边界处理
if (mRect.right > mImageWidth) {
mRect.right = mImageWidth;
mRect.left = mImageWidth - (int)(mViewWidth/mScale);
}
//向右滑动边界处理
if (mRect.left < 0) {
mRect.left = 0;
mRect.right = (int)(mViewWidth/mScale);
}
//请求重绘View树,即onDraw方法,不会触发onMeasure和onLayout方法
invalidate();
return false;
}
onScroll中处理滑动,根据手指移动的参数,来移动矩形绘制区域,这里需要处理各个边界点,比如左边最小就为0,右边最大为图片的宽度,上边最小为0,下边最大为图片的高度,不能超出边界否则就报错了。
/**
* 处理惯性问题
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(mRect.left, mRect.top, (int)-velocityX, (int)-velocityY, 0, mImageWidth- (int)(mViewWidth/mScale),
0, mImageHeight - (int)(mViewHeight/mScale));
return false;
}
//处理结果
@Override
public void computeScroll() {
if (mScroller.isFinished()) {
return;
}
if (mScroller.computeScrollOffset()) {
mRect.top = mScroller.getCurrY();
mRect.bottom = mRect.top + (int)(mViewHeight/mScale);
invalidate();
}
}
在onFling方法中调用滑动器Scroller的fling方法来处理手指离开之后惯性滑动。参数就是起始位置,滑动的惯性移动的距离在View的computeScroll()方法中计算,也需要注意边界问题,不要滑出边界。
7.双击事件处理
@Override
public boolean onDoubleTap(MotionEvent e) {
//双击事件
//双击是放大还是缩小,自己决定,此处定义为图片放大倍数小于1.5倍的时候,双击放大
if (mCurrentScale < mOriginalScale * 1.5) {
mCurrentScale = mOriginalScale * 2;
} else {
mCurrentScale = mOriginalScale;
}
mRect.right = mRect.left + (int)(mViewWidth/mCurrentScale);
mRect.bottom = mRect.top + (int)(mViewHeight/mCurrentScale);
//放大过程中,超出边界处理
//上边界处理
if (mRect.bottom > mImageHeight) {
mRect.bottom = mImageHeight;
mRect.top = mImageHeight - (int)(mViewHeight/mCurrentScale);
}
//下边界处理
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int) (mViewHeight / mCurrentScale);
}
//左滑界处理
if (mRect.right > mImageWidth) {
mRect.right = mImageWidth;
mRect.left = mImageWidth - (int) (mViewWidth / mCurrentScale);
}
//右边界处理
if (mRect.left < 0) {
mRect.left = 0;
mRect.right = (int) (mViewWidth / mCurrentScale);
}
//重绘
invalidate();
return false;
}
双击是放大还是缩小,自己决定,此处定义为图片放大倍数小于1.5倍的时候,双击放大,否则缩小即还原。倍数缩放倍数自己根据需求设定。在放大的过程中,也可能出现超出边界情形,和滑动的边界处理逻辑一样,可以将相同代码抽取为一个方法handleBorder();
8.手指缩放处理
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = mCurrentScale;
scale += detector.getScaleFactor() - 1;
if (scale <= mOriginalScale) {
scale = mOriginalScale;
} else if (scale > mOriginalScale * 2) {//设置最大的放大倍数,自行设定
scale = mOriginalScale * 2;
}
mRect.right = mRect.left + (int)(mViewWidth/scale);
mRect.bottom = mRect.top + (int)(mViewHeight/scale);
mCurrentScale = scale;
invalidate();
return true;
}
9.小结
到这里,巨图(长图)的加载就基本实现了,可以进行上下左右滑动,双击放大或缩小,手指缩放等操作。满足了巨图、长图加载的基本需求了。但是还有两个方向可以优化:
- 内存优化
尽管是截取图片的一部分进行显示,以及内存复用,在一定程度上减轻了内存压力,如果巨图占用的内存比较大,还有进行优化的空间,那就是通过设置 mOptions.inJustDecodeBounds = true; 计算采样率inSampleSize, 对加载的bitmap进行尺寸压缩,达到减小内存占用的目的,但是在缩放的时候,mMatrix.setScale(),参数在原来的基础上要乘以inSampleSize才能达到和原来缩放的视觉效果,减轻了内存压力,代价就是图片有一定程度的失真。这个可以根据加载的bitmap的内存占用情况,图片清晰程度进行合理的配置和权衡,是可以达到相对平衡的。 - 双击缩放问题
虽然实现了双击缩放,但是不够完美,在显示的Rect区域,无论双击哪个部分,缩放效果是一样的,就是说没有以双击的点为焦点进行缩放,这个部分暂时还没有解决,有待优化,如有知道如何处理的,还望不吝指教。