Skip to content

cshzhang/largeimageview

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

LargeImageView超大图的显示Demo

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages