LargeImageView超大圖的顯示Demo

jopen 9年前發布 | 9K 次閱讀 Android開發 移動開發 LargeImageView

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.baiduhome.net/lib/view/home/1445774381023

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!