LargeImageView超大图的显示Demo
jopen
9年前
largeimageview
LargeImageView超大图的显示Demo
概述
对于加载图片,一般为了尽可能避免OOM都会按照如下做法:
- 对于图片显示:根据显示图片的控件大小对图片进行压缩;
- 对于图片数量非常多:使用LruCache等缓存机制,将一些图片维持在内存中;
其实对于图片还有一种加载情况,就是单个图片非常巨大且不允许压缩。比如显示:世界地图,清明上河图... 那么对于这种需求该如何实现?首先不压缩,按照原图尺寸加载,那么屏幕肯定不够大,所以肯定是局部加载,那么肯定用到一个类:
BitmapRegionRecoder
其次,既然屏幕显示不完全,就需要添加Move手势检查,让用户可以拖动查看。
效果图
BitmapRegionRecoder简单使用
BitmapRegionRecoder主要用于显示图片的某一块矩形区域。 BitmapRegionDecoder提供一系列构造方法来初始化该对象,支持传入文件路径,文件描述符,文件的inputstream等。例如:
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
接下来就是显示指定区域的方法:
bitmapRegionDecoder.decodeRegion(rect, options);
参数一是一个rect,参数二是BitmapFactory.Options,可以控制inSampleSize,inPreferredConfig等。
自定义View显示大图
思路:
- 提供一个设置图片的入口
- 重写onTouchEvent()方法,根据用户移动的手势,去更新显示区域的Rect
- 每次更新Rect之后,调用invalidate方法,重写onDraw(),在里面去regionDecoder.decodeRegion(rect, options)实现绘制
上代码:
public class LargeImageView extends View { /** * BitmapRegionDecoder */ private BitmapRegionDecoder mDecoder; private static final BitmapFactory.Options mDecodeOptions = new BitmapFactory.Options(); static { mDecodeOptions.inPreferredConfig = Bitmap.Config.RGB_565; } private Rect mRect = new Rect(); //图片的宽高 private int mImageWidth; private int mImageHeight; //检测Move private MoveGestureDetector mMoveGestureDetector; public LargeImageView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** * 对外公布的方法,设置图片 * * @param is */ public void getImageInputStream(InputStream is) { try { //初始化mDecoder mDecoder = BitmapRegionDecoder.newInstance(is, false); //得到图片的宽高 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, options); mImageWidth = options.outWidth; mImageHeight = options.outHeight; requestLayout(); invalidate(); } catch (IOException e) { e.printStackTrace(); }finally { try { if(is != null) { is.close(); } }catch (Exception e) {} } } private void init() { mMoveGestureDetector = new MoveGestureDetector(getContext(), new MoveGestureDetector.SimpleMoveGestureDetector() { @Override public boolean onMove(MoveGestureDetector detector) { //移动rect int movX = (int) detector.getMoveX(); int movY = (int) detector.getMoveY(); if(mImageWidth > getWidth()) { mRect.offset(-movX, 0); checkWidth(); invalidate(); } if(mImageHeight > getHeight()) { mRect.offset(0, -movY); checkHeight(); invalidate(); } return true; } }); } private void checkHeight() { if(mRect.bottom > mImageHeight) { mRect.bottom = mImageHeight; mRect.top = mRect.bottom - getHeight(); } if(mRect.top < 0) { mRect.top = 0; mRect.bottom = mRect.top + getHeight(); } } private void checkWidth() { if(mRect.right > mImageWidth) { mRect.right = mImageWidth; mRect.left = mImageWidth - getWidth(); } if(mRect.left < 0) { mRect.left = 0; mRect.right = mRect.left + getWidth(); } } @Override public boolean onTouchEvent(MotionEvent event) { mMoveGestureDetector.onTouchEvent(event); return true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getMeasuredWidth(); int height = getMeasuredHeight(); //初始化mRect,显示图片中间区域 mRect.left = mImageWidth/2 - width/2; mRect.top = mImageHeight/2 - height/2; mRect.right = mRect.left + width; mRect.bottom = mRect.top + height; } @Override protected void onDraw(Canvas canvas) { //拿到最新rect对应的bitmap,进行绘制; Bitmap bitmap = mDecoder.decodeRegion(mRect, mDecodeOptions); canvas.drawBitmap(bitmap, 0, 0, null); } }
根据上述代码
- getImageInputStream里面获取图片的真实高度,初始化mDecoder
- onMeasure里面初始化mRect,大小为view的尺寸,并且显示图片中间区域
- onTouchEvent里面监听Move手势,在监听的回调里面改变Rect参数,以及边界检查,最后invalidate
- onDraw里面拿到最新rect对应的bitmap,进行绘制
OK,上面并不复杂;但是监听Move的方法有点奇怪:
mMoveGestureDetector.onTouchEvent(event);
嗯,这里模仿了系统的ScaleGestureDetector编写了MoveGestureDetector
MoveGestureDetector代码如下:
public class MoveGestureDetector { private Context mContext; private PointF mPrePointer; private PointF mCurPointer; private boolean isGestureMoving; private MotionEvent mPreMotionEvent; private MotionEvent mCurrentMotionEvent; public OnMoveGestureListener mListener; //记录最终结果返回 private PointF mDeltaPointer = new PointF(); public MoveGestureDetector(Context context, OnMoveGestureListener listener) { this.mContext = context; this.mListener = listener; } public float getMoveX() { return mDeltaPointer.x; } public float getMoveY() { return mDeltaPointer.y; } public boolean onTouchEvent(MotionEvent event) { if(!isGestureMoving) { handleStartEvent(event); }else { handleProgressEvent(event); } return true; } private void handleProgressEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mListener.onMoveEnd(this); resetState(); break; case MotionEvent.ACTION_MOVE: updateStateByEvent(event); if(mListener.onMove(this)) { mPreMotionEvent.recycle(); mPreMotionEvent = MotionEvent.obtain(event); } break; } } private void handleStartEvent(MotionEvent event) { switch(event.getAction()) { case MotionEvent.ACTION_DOWN: resetState(); mPreMotionEvent = MotionEvent.obtain(event); updateStateByEvent(event); break; case MotionEvent.ACTION_MOVE: isGestureMoving = mListener.onMoveBegin(this); break; } } private void updateStateByEvent(MotionEvent event) { MotionEvent preEvent = mPreMotionEvent; mPrePointer = calculateFocalPointer(preEvent); mCurPointer = calculateFocalPointer(event); boolean skipThisMoveEvent = preEvent.getPointerCount() != event.getPointerCount(); //更新deltaX和deltaY mDeltaPointer.x = skipThisMoveEvent ? 0 : mCurPointer.x - mPrePointer.x; mDeltaPointer.y = skipThisMoveEvent ? 0 : mCurPointer.y - mPrePointer.y; } private PointF calculateFocalPointer(MotionEvent event) { 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); } private void resetState() { if(mPreMotionEvent != null) { mPreMotionEvent.recycle(); mPreMotionEvent = null; } if(mCurrentMotionEvent != null) { mCurrentMotionEvent.recycle(); mCurrentMotionEvent = null; } isGestureMoving = false; } 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) { } } }
简单分析一下:
- OnMoveGestureListener内部接口以及SimpleMoveGestureDetector内部类都是模仿系统ScaleGestureDetector设计
- 构造方法MoveGestureDetector(Context context, OnMoveGestureListener listener)要求用户初始化OnMoveGestureListener并传递进来
- 对外公布onTouchEvent(),外部必须调用该方法,并把最新的event传递进来;
- 对外公布getMoveX(),getMoveY(),外部可以通过这两个方法,拿到Move时候最新的deltaX和deltaY
- updateStateByEvent(event)根据最新的event,更新mDeltaPointer.x和mDeltaPointer.y
- 剩余的方法:handleStartEvent(MotionEvent);handleProgressEvent(MotionEvent) 主要就是记录mPreMotionEvent,调用updateStateByEvent(MotionEvent)等来实现逻辑功能