Android性能優(yōu)化大圖治理示例詳解
引言
在實(shí)際的Android項(xiàng)目開(kāi)發(fā)中,圖片是必不可少的元素,幾乎所有的界面都是由圖片構(gòu)成的;像列表頁(yè)、查看大圖頁(yè)等,都是需要展示圖片,而且這兩者是有共同點(diǎn)的,列表展示的Item數(shù)量多,如果全部加載進(jìn)來(lái)勢(shì)必會(huì)造成OOM,因此列表頁(yè)通常采用分頁(yè)加載,加上RecyclerView的復(fù)用機(jī)制,一般很少會(huì)發(fā)生OOM。
但是對(duì)于大圖查看,通常在外界展示的是一張縮略圖,點(diǎn)開(kāi)之后放大就是原圖,如果圖片很大,OOM發(fā)生也是正常的,因此在加載大圖的時(shí)候,可以看下面這張圖

一張圖片如果很大,在手機(jī)屏幕中并不能完全展示,那么其實(shí)就沒(méi)有必要講圖片完全加載進(jìn)來(lái),而是可以采用分塊加載的方式,只展示顯示的那一部分,當(dāng)圖片向上滑動(dòng)的時(shí)候,之前展示的區(qū)域內(nèi)存能夠復(fù)用,不需要開(kāi)辟新的內(nèi)存空間來(lái)承接新的模塊,從而達(dá)到了大圖的治理的目的。
1 自定義大圖View
像在微信中點(diǎn)擊查看大圖,查看大圖的組件就是一個(gè)自定義View,能夠支持滑動(dòng)、拖拽、放大等功能,因此我們也可以自定義一個(gè)類似于微信的大圖查看器,從中了解圖片加載優(yōu)化的魅力
1.1 準(zhǔn)備工作
class BigView : View{
constructor(context: Context):super(context){
initBigView(context)
}
constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet){
initBigView(context)
}
private fun initBigView(context: Context) {
}
}
本節(jié)使用的語(yǔ)言為kotlin,需要java代碼的伙伴們可以找我私聊哦。

這個(gè)是我從網(wǎng)站上找的一張長(zhǎng)圖,大概700K左右,需要的可以自行下載,其實(shí)想要了解其中的原理和實(shí)現(xiàn),不一定要找一張?zhí)貏e大的圖片,所有的問(wèn)題都是舉一反三的。
class BigView : View, GestureDetector.OnGestureListener, View.OnTouchListener {
//分塊加載
private lateinit var mRect: Rect
//內(nèi)存復(fù)用
private lateinit var mOptions: BitmapFactory.Options
//手勢(shì)
private lateinit var mGestureDetector: GestureDetector
//滑動(dòng)
private lateinit var mScroller: Scroller
constructor(context: Context) : super(context) {
initBigView(context)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initBigView(context)
}
private fun initBigView(context: Context) {
mRect = Rect()
mOptions = BitmapFactory.Options()
mGestureDetector = GestureDetector(context, this)
mScroller = Scroller(context)
setOnTouchListener(this)
}
override fun onDown(e: MotionEvent?): Boolean {
return false
}
override fun onShowPress(e: MotionEvent?) {
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
return false
}
override fun onLongPress(e: MotionEvent?) {
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return false
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return false
}
}
前面我們提到的分塊加載、內(nèi)存復(fù)用、手勢(shì)等操作,直接在view初始化時(shí)完成,這樣我們前期的準(zhǔn)備工作就完成了。
1.2 圖片寬高適配
當(dāng)我們加載一張圖片的時(shí)候,要讓這張圖片完全展示在手機(jī)屏幕上不被裁剪,就需要做寬高的適配;如果這張圖片大小是80M,那么為了獲取寬高而將圖片加載到內(nèi)存中肯定會(huì)OOM,那么在圖片加載到內(nèi)存之前就像獲取圖片的寬高該怎么辦呢?BitmapFactory.Options就提供了這個(gè)手段
fun setImageUrl(inputStream: InputStream) {
//獲取圖片寬高
mOptions.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream,null,mOptions)
imageWidth = mOptions.outWidth
imageHeight = mOptions.outHeight
mOptions.inJustDecodeBounds = false
//開(kāi)啟復(fù)用
mOptions.inMutable = true
mOptions.inPreferredConfig = Bitmap.Config.RGB_565
//創(chuàng)建區(qū)域解碼器
try {
BitmapRegionDecoder.newInstance(inputStream,false)
}catch (e:Exception){
}
requestLayout()
}
當(dāng)設(shè)置inJustDecodeBounds為true(記住要成對(duì)出現(xiàn),使用完成之后需要設(shè)置為false),意味著我調(diào)用decodeStream方法的時(shí)候,不會(huì)將圖片的內(nèi)存加載而是僅僅為了獲取寬高。
然后拿到了圖片的寬高之后呢,調(diào)用requestLayout方法,會(huì)回調(diào)onMeasure方法,這個(gè)方法大家就非常熟悉了,能夠拿到view的寬高,從而完成圖片的適配
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//適配
viewWidth = measuredWidth
viewHeight = measuredHeight
originScale = viewWidth / imageWidth.toFloat()
mScale = originScale
//分塊加載首次進(jìn)入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = (viewHeight / mScale).toInt()
}
這里設(shè)置Rect的right就是圖片的寬度,因?yàn)樵紙D片的寬度可能比控件的寬度要寬,因此是將控件的寬度與圖片的寬度對(duì)比獲取了縮放比,那么Rect的bottom就需要等比縮放
這里的mRect可以看做是這張圖片上的一個(gè)滑動(dòng)窗口,無(wú)論是放大還是縮小,只要在屏幕上看到的區(qū)域,都可以看做是mRect在這張圖片上來(lái)回移動(dòng)截取的目標(biāo)區(qū)域
1.3 BitmapRegionDecoder
在onMeasure中,我們定義了需要加載的圖片的Rect,這是一塊區(qū)域,那么我們通過(guò)什么樣的方式能夠?qū)⑦@塊區(qū)域的圖片加載出來(lái),就是通過(guò)BitmapRegionDecoder區(qū)域解碼器。
區(qū)域解碼器,顧名思義,能夠在某個(gè)區(qū)域進(jìn)行圖片解碼展示
//創(chuàng)建區(qū)域解碼器
try {
BitmapRegionDecoder.newInstance(inputStream,false)
}catch (e:Exception){
}
在傳入圖片流的時(shí)候,我們就已經(jīng)創(chuàng)建了BitmapRegionDecoder,同時(shí)將圖片流作為參數(shù)構(gòu)建了解碼器,那么這個(gè)解碼器其實(shí)已經(jīng)拿到了整張圖片的資源,因此任意一塊區(qū)域,通過(guò)BitmapRegionDecoder都能夠解碼展示出來(lái)
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
mRegionDecoder ?: return
//復(fù)用bitmap
mOptions.inBitmap = mutableBitmap
mutableBitmap = mRegionDecoder?.decodeRegion(mRect, mOptions)
//畫(huà)出bitmap
val mMatrix = Matrix()
mMatrix.setScale(mScale, mScale)
mutableBitmap?.let {
canvas?.drawBitmap(it, mMatrix, null)
}
}
首先我們想要進(jìn)行內(nèi)存復(fù)用,需要調(diào)用BitmapFactory.Options的inBitmap,這個(gè)參數(shù)的含義就是,當(dāng)我們?cè)谀硥K區(qū)域加載圖片之后,如果圖片上滑那么就需要重新加載,那么這個(gè)時(shí)候就不會(huì)重新開(kāi)辟一塊內(nèi)存空間,而是復(fù)用之前的這塊區(qū)域,所以調(diào)用BitmapRegionDecoder的decodeRegion方法,傳入需要展示圖片的區(qū)域,就能夠給mutableBitmap賦值,這樣就達(dá)成了一塊內(nèi)存空間,多次復(fù)用的效果。

這樣通過(guò)壓縮之后,在屏幕中展示了這個(gè)長(zhǎng)圖的最上邊部分,那么剩下就需要做的是手勢(shì)事件的處理。
2 大圖View的手勢(shì)事件處理
通過(guò)前期的準(zhǔn)備工作,我們已經(jīng)實(shí)現(xiàn)了圖片的區(qū)域展示,那么接下來(lái)關(guān)鍵在于,我們通過(guò)手勢(shì)來(lái)查看完整的圖片,對(duì)于手勢(shì)事件的響應(yīng),在onTouch方法中處理。
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return mGestureDetector.onTouchEvent(event)
}
2.1 GestureDetector
通常來(lái)說(shuō),手勢(shì)事件的處理都是通過(guò)GestureDetector來(lái)完成,因此當(dāng)onTouch方法監(jiān)聽(tīng)到手勢(shì)事件之后,直接傳給GestureDetector,讓GestureDetector來(lái)處理這個(gè)事件。
override fun onDown(e: MotionEvent?): Boolean {
return false
}
override fun onShowPress(e: MotionEvent?) {
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
return false
}
override fun onLongPress(e: MotionEvent?) {
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return false
}
首先,我們先看下之前注冊(cè)的GestureDetector.OnGestureListener監(jiān)聽(tīng)器中實(shí)現(xiàn)的方法:
(1)onDown
override fun onDown(e: MotionEvent?): Boolean {
if(!mScroller.isFinished){
mScroller.forceFinished(true)
}
return true
}
當(dāng)手指按下時(shí),因?yàn)榛瑒?dòng)的慣性,所以down事件的處理就是如果圖片還在滑動(dòng)時(shí),按下就停止滑動(dòng);
(2)onScroll
那么當(dāng)你的手指按下之后,可能還會(huì)繼續(xù)滑動(dòng),那么就是會(huì)回調(diào)到onScroll方法,在這個(gè)方法中,主要做滑動(dòng)的處理
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
mRect.offset(0, distanceY.toInt())
//邊界case處理
if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}
if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}
postInvalidate()
return false
}
在onScroll方法中,其實(shí)已經(jīng)對(duì)滑動(dòng)的距離做了計(jì)算(這個(gè)真的太nice了,不需要我們自己手動(dòng)計(jì)算),因此只需要對(duì)mRect展示區(qū)域進(jìn)行變換即可;
但是這里會(huì)有兩個(gè)邊界case,例如滑動(dòng)到底部時(shí)就不能再滑了,這個(gè)時(shí)候,mRect的底部很可能都已經(jīng)超過(guò)了圖片的高度,因此需要做邊界的處理,那么滑動(dòng)到頂部的時(shí)候同樣也是需要做判斷。

(3)onFling
慣性滑動(dòng)。我們?cè)谑褂昧斜淼臅r(shí)候,我們?cè)诨瑒?dòng)的時(shí)候,雖然手指的滑動(dòng)距離很小,但是列表劃出去的距離卻很大,就是因?yàn)閼T性,所以GestureDetector中對(duì)慣性也做了處理。
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
mScroller.fling(0, mRect.top, 0, -velocityY.toInt(), 0, 0, 0, imageHeight - viewHeight)
return false
}
//計(jì)算慣性
override fun computeScroll() {
super.computeScroll()
if (mScroller.isFinished) {
return
}
if (mScroller.computeScrollOffset()) {
//正在滑動(dòng)
mRect.top = mScroller.currY
mRect.bottom = mScroller.currY + (viewHeight / mScale).toInt()
postInvalidate()
}
}
這個(gè)還是比較好理解的,就是設(shè)置最大的一個(gè)慣性滑動(dòng)距離,無(wú)論怎么滑動(dòng),邊界值就是從頂部一劃到底,這個(gè)最大的距離就是 imageHeight - viewHeight
設(shè)置了慣性滑動(dòng)的距離,那么在慣性滑動(dòng)時(shí),也需要實(shí)時(shí)改變mRect的解碼范圍,需要重寫(xiě)computeScroll方法,判斷如果是正在滑動(dòng)(通過(guò) mScroller.computeScrollOffset() 判斷),那么需要改變mRect的位置。
2.2 雙擊放大效果處理
我們?cè)谑褂胊pp時(shí),雙擊某張圖片或者雙指拉動(dòng)某張圖片的時(shí)候,都會(huì)講圖片放大,這也是業(yè)內(nèi)主流的兩種圖片放大的方式。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//適配
viewWidth = measuredWidth
viewHeight = measuredHeight
//縮放比
val radio = viewWidth / imageWidth.toFloat()
//分塊加載首次進(jìn)入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = viewHeight
}
我們先看一下不能縮放時(shí),mRect的賦值;那么當(dāng)我們雙擊放大時(shí),left和top的位置不會(huì)變,因?yàn)閳D片放大了,但是控件的大小不會(huì)變,因此left的最大值就是控件的寬度,bottom的最大值就是控件的高度。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//適配
viewWidth = measuredWidth
viewHeight = measuredHeight
originScale = viewWidth / imageWidth.toFloat()
mScale = originScale
//分塊加載首次進(jìn)入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = Math.min(imageWidth, viewWidth)
mRect.bottom = Math.min((viewHeight / mScale).toInt(), viewHeight)
}
這里就將onMeasure進(jìn)行改造;那么對(duì)于雙擊事件的處理,可以使用GestureDetector.OnDoubleTapListener來(lái)處理,在onDoubleTap事件中回調(diào)。
override fun onDoubleTap(e: MotionEvent?): Boolean {
if (mScale < originScale * 2) {
mScale = originScale * 2
} else {
mScale = originScale
}
postInvalidate()
return false
}
這里做了縮放就是判斷mScale的值,因?yàn)橐婚_(kāi)始進(jìn)來(lái)不是縮放的場(chǎng)景,因此 mScale = originScale,當(dāng)雙擊之后,需要將mScale擴(kuò)大2倍,當(dāng)重新繪制的時(shí)候,Bitmap就放大了2倍。
那么當(dāng)圖片放大之后,之前橫向不能滑動(dòng)現(xiàn)在也可以滑動(dòng)查看圖片,所以需要處理,同時(shí)也需要考慮邊界case
override fun onDoubleTap(e: MotionEvent?): Boolean {
if (mScale < originScale * 2) {
mScale = originScale * 2
} else {
mScale = originScale
}
//
mRect.right = mRect.left + (viewWidth / mScale).toInt()
mRect.bottom = mRect.top + (viewHeight / mScale).toInt()
if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}
if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}
if(mRect.right > imageWidth){
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
}
if(mRect.left < 0){
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
}
postInvalidate()
return false
}
當(dāng)雙擊圖片之后,mRect解碼的區(qū)域也隨之改變,因此需要對(duì)right和bottom做相應(yīng)的改變,圖片放大或者縮小,都是在控件寬高的基礎(chǔ)之上
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
mRect.offset(distanceX.toInt(), distanceY.toInt())
//邊界case處理
if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}
if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}
if(mRect.left < 0){
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
}
if(mRect.right > imageWidth){
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
}
postInvalidate()
return false
}
因?yàn)樾枰笥一瑒?dòng),那么onScroll方法也需要做相應(yīng)的改動(dòng),mRect的offset需要加上x(chóng)軸的偏移量。
2.3 手指放大效果處理
上一小節(jié)介紹了雙擊事件的效果處理,那么這一節(jié)就介紹另一個(gè)主流的放大效果實(shí)現(xiàn) - 手指縮放,是依賴 ScaleGestureDetector,其實(shí)跟GestureDetector的使用方式一致,這里就不做過(guò)多的贅述。
mScaleGestureDetector = ScaleGestureDetector(context, ScaleGesture())
在初始化ScaleGestureDetector的時(shí)候,需要傳入一個(gè)ScaleGesture內(nèi)部類,集成ScaleGestureDetector.SimpleOnScaleGestureListener,在onScale方法中獲取縮放因子來(lái)繪制
inner class ScaleGesture : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector?): Boolean {
var scale = detector?.scaleFactor ?: mScale//可以代替mScale
if (scale < originScale) {
scale = originScale
} else if (scale > originScale * 2) {
scale = originScale * 2
}
//在原先基礎(chǔ)上縮放
mRect.right = mRect.left + (viewWidth / scale).toInt()
mRect.bottom = mRect.top + (viewHeight / scale).toInt()
mScale = scale
postInvalidate()
return super.onScale(detector)
}
}
這里別忘記了別事件傳遞出來(lái),對(duì)于邊界case可自行處理
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
mGestureDetector.onTouchEvent(event)
mScaleGestureDetector.onTouchEvent(event)
return true
}
下面附上大圖治理的流程圖

黃顏色模塊: BitmapFactory.Options配置,避免整張大圖直接加載在內(nèi)存當(dāng)中,通過(guò)開(kāi)啟內(nèi)存復(fù)用(inMutable),使用區(qū)域解碼器,繪制一塊可見(jiàn)區(qū)域‘
淺黃色模塊: View的繪制流程
以上就是Android性能優(yōu)化大圖治理示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Android性能優(yōu)化大圖治理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android開(kāi)發(fā)通過(guò)Scroller實(shí)現(xiàn)過(guò)渡滑動(dòng)效果操作示例
這篇文章主要介紹了android開(kāi)發(fā)通過(guò)Scroller實(shí)現(xiàn)過(guò)渡滑動(dòng)效果,結(jié)合實(shí)例形式分析了Android Scroller類實(shí)現(xiàn)過(guò)渡滑動(dòng)效果的基本原理與實(shí)現(xiàn)技巧,需要的朋友可以參考下2020-01-01
android 觸屏的震動(dòng)響應(yīng)接口調(diào)用方法
android 相關(guān)開(kāi)發(fā)過(guò)程中,經(jīng)常會(huì)使用到觸屏的震動(dòng)響應(yīng)接口,為此本文列出以下方法,想要了解的朋友可以參考下2012-11-11
Android上傳文件到Web服務(wù)器 PHP接收文件
這篇文章主要為大家詳細(xì)介紹了Android上傳文件到Web服務(wù)器,PHP接收文件的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Flutter實(shí)現(xiàn)倒計(jì)時(shí)功能
這篇文章主要為大家詳細(xì)介紹了Flutter實(shí)現(xiàn)倒計(jì)時(shí)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Android不規(guī)則封閉區(qū)域填充色彩的實(shí)例代碼
這篇文章主要介紹了Android不規(guī)則封閉區(qū)域填充色彩的實(shí)例代碼, 具有很好的參考價(jià)值,希望對(duì)大家有所幫助,一起跟隨小編過(guò)來(lái)看看吧2018-05-05
ANDROID BottomNavigationBar底部導(dǎo)航欄的實(shí)現(xiàn)示例
本篇文章主要介紹了ANDROID BottomNavigationBar底部導(dǎo)航欄的實(shí)現(xiàn)示例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-10-10
Android側(cè)滑效果簡(jiǎn)單實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android側(cè)滑效果簡(jiǎn)單實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11

