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