Android_高清加载巨图方案 拒绝压缩图片
原文出處:http://blog.csdn.net/lmj623565791/article/details/49300989?
一、概述
距離上一篇博客有段時間沒更新了,主要是最近有些私事導致的,那么就先來一篇簡單一點的博客脈動回來。
對于加載圖片,大家都不陌生,一般為了盡可能避免OOM都會按照如下做法:
對于圖片顯示:根據(jù)需要顯示圖片控件的大小對圖片進行壓縮顯示。
如果圖片數(shù)量非常多:則會使用LruCache等緩存機制,將所有圖片占據(jù)的內容維持在一個范圍內。
其實對于圖片加載還有種情況,就是單個圖片非常巨大,并且還不允許壓縮。比如顯示:世界地圖、清明上河圖、微博長圖等。
那么對于這種需求,該如何做呢?
首先不壓縮,按照原圖尺寸加載,那么屏幕肯定是不夠大的,并且考慮到內存的情況,不可能一次性整圖加載到內存中,所以肯定是局部加載,那么就需要用到一個類:
-
BitmapRegionDecoder
其次,既然屏幕顯示不完,那么最起碼要添加一個上下左右拖動的手勢,讓用戶可以拖動查看。
那么綜上,本篇博文的目的就是去自定義一個顯示巨圖的View,支持用戶去拖動查看,大概的效果圖如下:
好吧,這清明上河圖太長了,想要觀看全圖,文末下載,圖片在assets目錄。當然如果你的圖,高度也很大,肯定也是可以上下拖動的。
二、初識BitmapRegionDecoder
BitmapRegionDecoder主要用于顯示圖片的某一塊矩形區(qū)域,如果你需要顯示某個圖片的指定區(qū)域,那么這個類非常合適。
對于該類的用法,非常簡單,既然是顯示圖片的某一塊區(qū)域,那么至少只需要一個方法去設置圖片;一個方法傳入顯示的區(qū)域即可;詳見:
-
BitmapRegionDecoder提供了一系列的newInstance方法來構造對象,支持傳入文件路徑,文件描述符,文件的inputstrem等。
例如:
- ?BitmapRegionDecoder?bitmapRegionDecoder?=BitmapRegionDecoder.newInstance(inputStream,?false);
-
上述解決了傳入我們需要處理的圖片,那么接下來就是顯示指定的區(qū)域。
- bitmapRegionDecoder.decodeRegion(rect,?options)
-
參數(shù)一很明顯是一個rect,參數(shù)二是BitmapFactory.Options,你可以控制圖片的inSampleSize,inPreferredConfig等。
那么下面看一個超級簡單的例子:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | package?com.zhy.blogcodes.largeImage; import?android.graphics.Bitmap; import?android.graphics.BitmapFactory; import?android.graphics.BitmapRegionDecoder; import?android.graphics.Rect; import?android.os.Bundle; import?android.support.v7.app.AppCompatActivity; import?android.widget.ImageView; import?com.zhy.blogcodes.R; import?java.io.IOException; import?java.io.InputStream; public?class?LargeImageViewActivity?extends?AppCompatActivity { ????private?ImageView?mImageView; ????@Override ????protected?void?onCreate(Bundle?savedInstanceState) ????{ ????????super.onCreate(savedInstanceState); ????????setContentView(R.layout.activity_large_image_view); ????????mImageView?=?(ImageView)?findViewById(R.id.id_imageview); ????????try ????????{ ????????????InputStream?inputStream?=?getAssets().open("tangyan.jpg"); ????????????//獲得圖片的寬、高 ????????????BitmapFactory.Options?tmpOptions?=?new?BitmapFactory.Options(); ????????????tmpOptions.inJustDecodeBounds?=?true; ????????????BitmapFactory.decodeStream(inputStream,?null,?tmpOptions); ????????????int?width?=?tmpOptions.outWidth; ????????????int?height?=?tmpOptions.outHeight; ????????????//設置顯示圖片的中心區(qū)域 ????????????BitmapRegionDecoder?bitmapRegionDecoder?=?BitmapRegionDecoder.newInstance(inputStream,?false); ????????????BitmapFactory.Options?options?=?new?BitmapFactory.Options(); ????????????options.inPreferredConfig?=?Bitmap.Config.RGB_565; ????????????Bitmap?bitmap?=?bitmapRegionDecoder.decodeRegion(new?Rect(width?/?2?-?100,?height?/?2?-?100,?width?/?2?+?100,?height?/?2?+?100),?options); ????????????mImageView.setImageBitmap(bitmap); ????????}?catch?(IOException?e) ????????{ ????????????e.printStackTrace(); ????????} ????} } |
上面的小圖顯示的即為下面的大圖的中間區(qū)域。
ok,那么目前我們已經了解了BitmapRegionDecoder的基本用戶,那么往外擴散,我們需要自定義一個控件去顯示巨圖就很簡單了,首先Rect的范圍就是我們View的大小,然后根據(jù)用戶的移動手勢,不斷去更新我們的Rect的參數(shù)即可。
三、自定義顯示大圖控件
根據(jù)上面的分析呢,我們這個自定義控件思路就非常清晰了:
-
提供一個設置圖片的入口
-
重寫onTouchEvent,在里面根據(jù)用戶移動的手勢,去更新顯示區(qū)域的參數(shù)
-
每次更新區(qū)域參數(shù)后,調用invalidate,onDraw里面去regionDecoder.decodeRegion拿到bitmap,去draw
理清了,發(fā)現(xiàn)so easy,下面上代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | package?com.zhy.blogcodes.largeImage.view; import?android.content.Context; import?android.graphics.Bitmap; import?android.graphics.BitmapFactory; import?android.graphics.BitmapRegionDecoder; import?android.graphics.Canvas; import?android.graphics.Rect; import?android.util.AttributeSet; import?android.view.MotionEvent; import?android.view.View; import?java.io.IOException; import?java.io.InputStream; /** ?*?Created?by?zhy?on?15/5/16. ?*/ public?class?LargeImageView?extends?View { ????private?BitmapRegionDecoder?mDecoder; ????/** ?????*?圖片的寬度和高度 ?????*/ ????private?int?mImageWidth,?mImageHeight; ????/** ?????*?繪制的區(qū)域 ?????*/ ????private?volatile?Rect?mRect?=?new?Rect(); ????private?MoveGestureDetector?mDetector; ????private?static?final?BitmapFactory.Options?options?=?new?BitmapFactory.Options(); ????static ????{ ????????options.inPreferredConfig?=?Bitmap.Config.RGB_565; ????} ????public?void?setInputStream(InputStream?is) ????{ ????????try ????????{ ????????????mDecoder?=?BitmapRegionDecoder.newInstance(is,?false); ????????????BitmapFactory.Options?tmpOptions?=?new?BitmapFactory.Options(); ????????????//?Grab?the?bounds?for?the?scene?dimensions ????????????tmpOptions.inJustDecodeBounds?=?true; ????????????BitmapFactory.decodeStream(is,?null,?tmpOptions); ????????????mImageWidth?=?tmpOptions.outWidth; ????????????mImageHeight?=?tmpOptions.outHeight; ????????????requestLayout(); ????????????invalidate(); ????????}?catch?(IOException?e) ????????{ ????????????e.printStackTrace(); ????????}?finally ????????{ ????????????try ????????????{ ????????????????if?(is?!=?null)?is.close(); ????????????}?catch?(Exception?e) ????????????{ ????????????} ????????} ????} ????public?void?init() ????{ ????????mDetector?=?new?MoveGestureDetector(getContext(),?new?MoveGestureDetector.SimpleMoveGestureDetector() ????????{ ????????????@Override ????????????public?boolean?onMove(MoveGestureDetector?detector) ????????????{ ????????????????int?moveX?=?(int)?detector.getMoveX(); ????????????????int?moveY?=?(int)?detector.getMoveY(); ????????????????if?(mImageWidth?>?getWidth()) ????????????????{ ????????????????????mRect.offset(-moveX,?0); ????????????????????checkWidth(); ????????????????????invalidate(); ????????????????} ????????????????if?(mImageHeight?>?getHeight()) ????????????????{ ????????????????????mRect.offset(0,?-moveY); ????????????????????checkHeight(); ????????????????????invalidate(); ????????????????} ????????????????return?true; ????????????} ????????}); ????} ????private?void?checkWidth() ????{ ????????Rect?rect?=?mRect; ????????int?imageWidth?=?mImageWidth; ????????int?imageHeight?=?mImageHeight; ????????if?(rect.right?>?imageWidth) ????????{ ????????????rect.right?=?imageWidth; ????????????rect.left?=?imageWidth?-?getWidth(); ????????} ????????if?(rect.left?<?0) ????????{ ????????????rect.left?=?0; ????????????rect.right?=?getWidth(); ????????} ????} ????private?void?checkHeight() ????{ ????????Rect?rect?=?mRect; ????????int?imageWidth?=?mImageWidth; ????????int?imageHeight?=?mImageHeight; ????????if?(rect.bottom?>?imageHeight) ????????{ ????????????rect.bottom?=?imageHeight; ????????????rect.top?=?imageHeight?-?getHeight(); ????????} ????????if?(rect.top?<?0) ????????{ ????????????rect.top?=?0; ????????????rect.bottom?=?getHeight(); ????????} ????} ????public?LargeImageView(Context?context,?AttributeSet?attrs) ????{ ????????super(context,?attrs); ????????init(); ????} ????@Override ????public?boolean?onTouchEvent(MotionEvent?event) ????{ ????????mDetector.onToucEvent(event); ????????return?true; ????} ????@Override ????protected?void?onDraw(Canvas?canvas) ????{ ????????Bitmap?bm?=?mDecoder.decodeRegion(mRect,?options); ????????canvas.drawBitmap(bm,?0,?0,?null); ????} ????@Override ????protected?void?onMeasure(int?widthMeasureSpec,?int?heightMeasureSpec) ????{ ????????super.onMeasure(widthMeasureSpec,?heightMeasureSpec); ????????int?width?=?getMeasuredWidth(); ????????int?height?=?getMeasuredHeight(); ????????int?imageWidth?=?mImageWidth; ????????int?imageHeight?=?mImageHeight; ?????????//默認直接顯示圖片的中心區(qū)域,可以自己去調節(jié) ????????mRect.left?=?imageWidth?/?2?-?width?/?2; ????????mRect.top?=?imageHeight?/?2?-?height?/?2; ????????mRect.right?=?mRect.left?+?width; ????????mRect.bottom?=?mRect.top?+?height; ????} } |
根據(jù)上述源碼:
setInputStream里面去獲得圖片的真實的寬度和高度,以及初始化我們的mDecoder
onMeasure里面為我們的顯示區(qū)域的rect賦值,大小為view的尺寸
onTouchEvent里面我們監(jiān)聽move的手勢,在監(jiān)聽的回調里面去改變rect的參數(shù),以及做邊界檢查,最后invalidate
在onDraw里面就是根據(jù)rect拿到bitmap,然后draw了
ok,上面并不復雜,不過大家有沒有注意到,這個監(jiān)聽用戶move手勢的代碼寫的有點奇怪,恩,這里模仿了系統(tǒng)的ScaleGestureDetector,編寫了MoveGestureDetector,代碼如下:
-
MoveGestureDetector
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | package?com.zhy.blogcodes.largeImage.view; import?android.content.Context; import?android.graphics.PointF; import?android.view.MotionEvent; public?class?MoveGestureDetector?extends?BaseGestureDetector { ????private?PointF?mCurrentPointer; ????private?PointF?mPrePointer; ????//僅僅為了減少創(chuàng)建內存 ????private?PointF?mDeltaPointer?=?new?PointF(); ????//用于記錄最終結果,并返回 ????private?PointF?mExtenalPointer?=?new?PointF(); ????private?OnMoveGestureListener?mListenter; ????public?MoveGestureDetector(Context?context,?OnMoveGestureListener?listener) ????{ ????????super(context); ????????mListenter?=?listener; ????} ????@Override ????protected?void?handleInProgressEvent(MotionEvent?event) ????{ ????????int?actionCode?=?event.getAction()?&?MotionEvent.ACTION_MASK; ????????switch?(actionCode) ????????{ ????????????case?MotionEvent.ACTION_CANCEL: ????????????case?MotionEvent.ACTION_UP: ????????????????mListenter.onMoveEnd(this); ????????????????resetState(); ????????????????break; ????????????case?MotionEvent.ACTION_MOVE: ????????????????updateStateByEvent(event); ????????????????boolean?update?=?mListenter.onMove(this); ????????????????if?(update) ????????????????{ ????????????????????mPreMotionEvent.recycle(); ????????????????????mPreMotionEvent?=?MotionEvent.obtain(event); ????????????????} ????????????????break; ????????} ????} ????@Override ????protected?void?handleStartProgressEvent(MotionEvent?event) ????{ ????????int?actionCode?=?event.getAction()?&?MotionEvent.ACTION_MASK; ????????switch?(actionCode) ????????{ ????????????case?MotionEvent.ACTION_DOWN: ????????????????resetState();//防止沒有接收到CANCEL?or?UP?,保險起見 ????????????????mPreMotionEvent?=?MotionEvent.obtain(event); ????????????????updateStateByEvent(event); ????????????????break; ????????????case?MotionEvent.ACTION_MOVE: ????????????????mGestureInProgress?=?mListenter.onMoveBegin(this); ????????????????break; ????????} ????} ????protected?void?updateStateByEvent(MotionEvent?event) ????{ ????????final?MotionEvent?prev?=?mPreMotionEvent; ????????mPrePointer?=?caculateFocalPointer(prev); ????????mCurrentPointer?=?caculateFocalPointer(event); ????????//Log.e("TAG",?mPrePointer.toString()?+?"?,??"?+?mCurrentPointer); ????????boolean?mSkipThisMoveEvent?=?prev.getPointerCount()?!=?event.getPointerCount(); ????????//Log.e("TAG",?"mSkipThisMoveEvent?=?"?+?mSkipThisMoveEvent); ????????mExtenalPointer.x?=?mSkipThisMoveEvent???0?:?mCurrentPointer.x?-?mPrePointer.x; ????????mExtenalPointer.y?=?mSkipThisMoveEvent???0?:?mCurrentPointer.y?-?mPrePointer.y; ????} ????/** ?????*?根據(jù)event計算多指中心點 ?????* ?????*?@param?event ?????*?@return ?????*/ ????private?PointF?caculateFocalPointer(MotionEvent?event) ????{ ????????final?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); ????} ????public?float?getMoveX() ????{ ????????return?mExtenalPointer.x; ????} ????public?float?getMoveY() ????{ ????????return?mExtenalPointer.y; ????} ????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) ????????{ ????????} ????} } |
BaseGestureDetector
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | package?com.zhy.blogcodes.largeImage.view; import?android.content.Context; import?android.view.MotionEvent; public?abstract?class?BaseGestureDetector { ????protected?boolean?mGestureInProgress; ????protected?MotionEvent?mPreMotionEvent; ????protected?MotionEvent?mCurrentMotionEvent; ????protected?Context?mContext; ????public?BaseGestureDetector(Context?context) ????{ ????????mContext?=?context; ????} ????public?boolean?onToucEvent(MotionEvent?event) ????{ ????????if?(!mGestureInProgress) ????????{ ????????????handleStartProgressEvent(event); ????????}?else ????????{ ????????????handleInProgressEvent(event); ????????} ????????return?true; ????} ????protected?abstract?void?handleInProgressEvent(MotionEvent?event); ????protected?abstract?void?handleStartProgressEvent(MotionEvent?event); ????protected?abstract?void?updateStateByEvent(MotionEvent?event); ????protected?void?resetState() ????{ ????????if?(mPreMotionEvent?!=?null) ????????{ ????????????mPreMotionEvent.recycle(); ????????????mPreMotionEvent?=?null; ????????} ????????if?(mCurrentMotionEvent?!=?null) ????????{ ????????????mCurrentMotionEvent.recycle(); ????????????mCurrentMotionEvent?=?null; ????????} ????????mGestureInProgress?=?false; ????} } |
-
你可能會說,一個move手勢搞這么多代碼,太麻煩了。的確是的,move手勢的檢測非常簡單,那么之所以這么寫呢,主要是為了可以復用,比如現(xiàn)在有一堆的XXXGestureDetector,當我們需要監(jiān)聽什么手勢,就直接拿個detector來檢測多方便。我相信大家肯定也郁悶過Google,為什么只有ScaleGestureDetector而沒有RotateGestureDetector呢。
根據(jù)上述,大家應該理解了為什么要這么做,當時不強制,每個人都有個性。
不過值得一提的是:上面這個手勢檢測的寫法,不是我想的,而是一個開源的項目https://github.com/rharter/android-gesture-detectors,里面包含很多的手勢檢測。對應的博文是:http://code.almeros.com/android-multitouch-gesture-detectors#.VibzzhArJXg那面上面兩個類就是我偷學了的~ 哈
四、測試
測試其實沒撒好說的了,就是把我們的LargeImageView放入布局文件,然后Activity里面去設置inputstream了。
| 1 2 3 4 5 6 7 8 9 10 11 12 | <RelativeLayout?xmlns:android="http://schemas.android.com/apk/res/android" ????????????????xmlns:tools="http://schemas.android.com/tools" ????????????????android:layout_width="match_parent" ????????????????android:layout_height="match_parent"> ????<com.zhy.blogcodes.largeImage.view.LargeImageView ????????android:id="@+id/id_largetImageview" ????????android:layout_width="match_parent" ????????android:layout_height="match_parent"/> </RelativeLayout> |
然后在Activity里面去設置圖片:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package?com.zhy.blogcodes.largeImage; import?android.os.Bundle; import?android.support.v7.app.AppCompatActivity; import?com.zhy.blogcodes.R; import?com.zhy.blogcodes.largeImage.view.LargeImageView; import?java.io.IOException; import?java.io.InputStream; public?class?LargeImageViewActivity?extends?AppCompatActivity { ????private?LargeImageView?mLargeImageView; ????@Override ????protected?void?onCreate(Bundle?savedInstanceState) ????{ ????????super.onCreate(savedInstanceState); ????????setContentView(R.layout.activity_large_image_view); ????????mLargeImageView?=?(LargeImageView)?findViewById(R.id.id_largetImageview); ????????try ????????{ ????????????InputStream?inputStream?=?getAssets().open("world.jpg"); ????????????mLargeImageView.setInputStream(inputStream); ????????}?catch?(IOException?e) ????????{ ????????????e.printStackTrace(); ????????} ????} } |
效果圖:
ok,那么到此,顯示巨圖的方案以及詳細的代碼就描述完成了,總體還是非常簡單的。?
但是,在實際的項目中,可能會有更多的需求,比如增加放大、縮小;增加快滑手勢等等,那么大家可以去參考這個庫:https://github.com/johnnylambada/WorldMap,該庫基本實現(xiàn)了絕大多數(shù)的需求,大家根據(jù)本文這個思路再去看這個庫,也會簡單很多,定制起來也容易。我這個地圖的圖就是該庫里面提供的。
哈,掌握了這個,以后面試過程中也可以悄悄的裝一把了,當你優(yōu)雅的答完android加載圖片的方案以后,然后接一句,其實還有一種情況,就是高清顯示巨圖,那么我們應該…相信面試官對你的印象會好很多~ have a nice day ~
源碼火速上傳中,give me 5 mins
參考鏈接
-
http://code.almeros.com/android-multitouch-gesture-detectors#.VibzzhArJXg
-
https://github.com/rharter/android-gesture-detectors
-
https://github.com/johnnylambada/WorldMap
總結
以上是生活随笔為你收集整理的Android_高清加载巨图方案 拒绝压缩图片的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android按比例布局之layout_
- 下一篇: Android 编程规范与常用技巧