LargeImageView超大图的显示Demo

jopen 5年前

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)等来实现逻辑功能

项目主页:http://www.open-open.com/lib/view/home/1445774381023