Bitmap之巨圖加載

前言

在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區域,無論雙擊哪個部分,縮放效果是一樣的,就是說沒有以雙擊的點爲焦點進行縮放,這個部分暫時還沒有解決,有待優化,如有知道如何處理的,還望不吝指教。

代碼鏈接:
https://github.com/mitufengyun/LoadBigBitmap

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章