<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在實際的Android專案開發中,圖片是必不可少的元素,幾乎所有的介面都是由圖片構成的;像列表頁、檢視大圖頁等,都是需要展示圖片,而且這兩者是有共同點的,列表展示的Item數量多,如果全部載入進來勢必會造成OOM,因此列表頁通常採用分頁載入,加上RecyclerView的複用機制,一般很少會發生OOM。
但是對於大圖檢視,通常在外界展示的是一張縮圖,點開之後放大就是原圖,如果圖片很大,OOM發生也是正常的,因此在載入大圖的時候,可以看下面這張圖
一張圖片如果很大,在手機螢幕中並不能完全展示,那麼其實就沒有必要講圖片完全載入進來,而是可以採用分塊載入的方式,只展示顯示的那一部分,當圖片向上滑動的時候,之前展示的區域記憶體能夠複用,不需要開闢新的記憶體空間來承接新的模組,從而達到了大圖的治理的目的。
像在微信中點選檢視大圖,檢視大圖的元件就是一個自定義View,能夠支援滑動、拖拽、放大等功能,因此我們也可以自定義一個類似於微信的大圖檢視器,從中瞭解圖片載入優化的魅力
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) { } }
本節使用的語言為kotlin,需要java程式碼的夥伴們可以找我私聊哦。
這個是我從網站上找的一張長圖,大概700K左右,需要的可以自行下載,其實想要了解其中的原理和實現,不一定要找一張特別大的圖片,所有的問題都是舉一反三的。
class BigView : View, GestureDetector.OnGestureListener, View.OnTouchListener { //分塊載入 private lateinit var mRect: Rect //記憶體複用 private lateinit var mOptions: BitmapFactory.Options //手勢 private lateinit var mGestureDetector: GestureDetector //滑動 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 } }
前面我們提到的分塊載入、記憶體複用、手勢等操作,直接在view初始化時完成,這樣我們前期的準備工作就完成了。
當我們載入一張圖片的時候,要讓這張圖片完全展示在手機螢幕上不被裁剪,就需要做寬高的適配;如果這張圖片大小是80M,那麼為了獲取寬高而將圖片載入到記憶體中肯定會OOM,那麼在圖片載入到記憶體之前就像獲取圖片的寬高該怎麼辦呢?BitmapFactory.Options就提供了這個手段
fun setImageUrl(inputStream: InputStream) { //獲取圖片寬高 mOptions.inJustDecodeBounds = true BitmapFactory.decodeStream(inputStream,null,mOptions) imageWidth = mOptions.outWidth imageHeight = mOptions.outHeight mOptions.inJustDecodeBounds = false //開啟複用 mOptions.inMutable = true mOptions.inPreferredConfig = Bitmap.Config.RGB_565 //建立區域解碼器 try { BitmapRegionDecoder.newInstance(inputStream,false) }catch (e:Exception){ } requestLayout() }
當設定inJustDecodeBounds為true(記住要成對出現,使用完成之後需要設定為false),意味著我呼叫decodeStream方法的時候,不會將圖片的記憶體載入而是僅僅為了獲取寬高。
然後拿到了圖片的寬高之後呢,呼叫requestLayout方法,會回撥onMeasure方法,這個方法大家就非常熟悉了,能夠拿到view的寬高,從而完成圖片的適配
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) //適配 viewWidth = measuredWidth viewHeight = measuredHeight originScale = viewWidth / imageWidth.toFloat() mScale = originScale //分塊載入首次進入展示的rect mRect.left = 0 mRect.top = 0 mRect.right = imageWidth mRect.bottom = (viewHeight / mScale).toInt() }
這裡設定Rect的right就是圖片的寬度,因為原始圖片的寬度可能比控制元件的寬度要寬,因此是將控制元件的寬度與圖片的寬度對比獲取了縮放比,那麼Rect的bottom就需要等比縮放
這裡的mRect可以看做是這張圖片上的一個滑動視窗,無論是放大還是縮小,只要在螢幕上看到的區域,都可以看做是mRect在這張圖片上來回移動擷取的目標區域
在onMeasure中,我們定義了需要載入的圖片的Rect,這是一塊區域,那麼我們通過什麼樣的方式能夠將這塊區域的圖片載入出來,就是通過BitmapRegionDecoder區域解碼器。
區域解碼器,顧名思義,能夠在某個區域進行圖片解碼展示
//建立區域解碼器 try { BitmapRegionDecoder.newInstance(inputStream,false) }catch (e:Exception){ }
在傳入圖片流的時候,我們就已經建立了BitmapRegionDecoder,同時將圖片流作為引數構建瞭解碼器,那麼這個解碼器其實已經拿到了整張圖片的資源,因此任意一塊區域,通過BitmapRegionDecoder都能夠解碼展示出來
override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) mRegionDecoder ?: return //複用bitmap mOptions.inBitmap = mutableBitmap mutableBitmap = mRegionDecoder?.decodeRegion(mRect, mOptions) //畫出bitmap val mMatrix = Matrix() mMatrix.setScale(mScale, mScale) mutableBitmap?.let { canvas?.drawBitmap(it, mMatrix, null) } }
首先我們想要進行記憶體複用,需要呼叫BitmapFactory.Options的inBitmap,這個引數的含義就是,當我們在某塊區域載入圖片之後,如果圖片上滑那麼就需要重新載入,那麼這個時候就不會重新開闢一塊記憶體空間,而是複用之前的這塊區域,所以呼叫BitmapRegionDecoder的decodeRegion方法,傳入需要展示圖片的區域,就能夠給mutableBitmap賦值,這樣就達成了一塊記憶體空間,多次複用的效果。
這樣通過壓縮之後,在螢幕中展示了這個長圖的最上邊部分,那麼剩下就需要做的是手勢事件的處理。
通過前期的準備工作,我們已經實現了圖片的區域展示,那麼接下來關鍵在於,我們通過手勢來檢視完整的圖片,對於手勢事件的響應,在onTouch方法中處理。
override fun onTouch(v: View?, event: MotionEvent?): Boolean { return mGestureDetector.onTouchEvent(event) }
通常來說,手勢事件的處理都是通過GestureDetector來完成,因此當onTouch方法監聽到手勢事件之後,直接傳給GestureDetector,讓GestureDetector來處理這個事件。
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 }
首先,我們先看下之前註冊的GestureDetector.OnGestureListener監聽器中實現的方法:
(1)onDown
override fun onDown(e: MotionEvent?): Boolean { if(!mScroller.isFinished){ mScroller.forceFinished(true) } return true }
當手指按下時,因為滑動的慣性,所以down事件的處理就是如果圖片還在滑動時,按下就停止滑動;
(2)onScroll
那麼當你的手指按下之後,可能還會繼續滑動,那麼就是會回撥到onScroll方法,在這個方法中,主要做滑動的處理
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方法中,其實已經對滑動的距離做了計算(這個真的太nice了,不需要我們自己手動計算),因此只需要對mRect展示區域進行變換即可;
但是這裡會有兩個邊界case,例如滑動到底部時就不能再滑了,這個時候,mRect的底部很可能都已經超過了圖片的高度,因此需要做邊界的處理,那麼滑動到頂部的時候同樣也是需要做判斷。
(3)onFling
慣性滑動。我們在使用列表的時候,我們在滑動的時候,雖然手指的滑動距離很小,但是列表劃出去的距離卻很大,就是因為慣性,所以GestureDetector中對慣性也做了處理。
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 } //計算慣性 override fun computeScroll() { super.computeScroll() if (mScroller.isFinished) { return } if (mScroller.computeScrollOffset()) { //正在滑動 mRect.top = mScroller.currY mRect.bottom = mScroller.currY + (viewHeight / mScale).toInt() postInvalidate() } }
這個還是比較好理解的,就是設定最大的一個慣性滑動距離,無論怎麼滑動,邊界值就是從頂部一劃到底,這個最大的距離就是 imageHeight - viewHeight
設定了慣性滑動的距離,那麼在慣性滑動時,也需要實時改變mRect的解碼範圍,需要重寫computeScroll方法,判斷如果是正在滑動(通過 mScroller.computeScrollOffset() 判斷),那麼需要改變mRect的位置。
我們在使用app時,雙擊某張圖片或者雙指拉動某張圖片的時候,都會講圖片放大,這也是業內主流的兩種圖片放大的方式。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) //適配 viewWidth = measuredWidth viewHeight = measuredHeight //縮放比 val radio = viewWidth / imageWidth.toFloat() //分塊載入首次進入展示的rect mRect.left = 0 mRect.top = 0 mRect.right = imageWidth mRect.bottom = viewHeight }
我們先看一下不能縮放時,mRect的賦值;那麼當我們雙擊放大時,left和top的位置不會變,因為圖片放大了,但是控制元件的大小不會變,因此left的最大值就是控制元件的寬度,bottom的最大值就是控制元件的高度。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) //適配 viewWidth = measuredWidth viewHeight = measuredHeight originScale = viewWidth / imageWidth.toFloat() mScale = originScale //分塊載入首次進入展示的rect mRect.left = 0 mRect.top = 0 mRect.right = Math.min(imageWidth, viewWidth) mRect.bottom = Math.min((viewHeight / mScale).toInt(), viewHeight) }
這裡就將onMeasure進行改造;那麼對於雙擊事件的處理,可以使用GestureDetector.OnDoubleTapListener來處理,在onDoubleTap事件中回撥。
override fun onDoubleTap(e: MotionEvent?): Boolean { if (mScale < originScale * 2) { mScale = originScale * 2 } else { mScale = originScale } postInvalidate() return false }
這裡做了縮放就是判斷mScale的值,因為一開始進來不是縮放的場景,因此 mScale = originScale,當雙擊之後,需要將mScale擴大2倍,當重新繪製的時候,Bitmap就放大了2倍。
那麼當圖片放大之後,之前橫向不能滑動現在也可以滑動檢檢視片,所以需要處理,同時也需要考慮邊界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 }
當雙擊圖片之後,mRect解碼的區域也隨之改變,因此需要對right和bottom做相應的改變,圖片放大或者縮小,都是在控制元件寬高的基礎之上
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 }
因為需要左右滑動,那麼onScroll方法也需要做相應的改動,mRect的offset需要加上x軸的偏移量。
上一小節介紹了雙擊事件的效果處理,那麼這一節就介紹另一個主流的放大效果實現 - 手指縮放,是依賴 ScaleGestureDetector,其實跟GestureDetector的使用方式一致,這裡就不做過多的贅述。
mScaleGestureDetector = ScaleGestureDetector(context, ScaleGesture())
在初始化ScaleGestureDetector的時候,需要傳入一個ScaleGesture內部類,整合ScaleGestureDetector.SimpleOnScaleGestureListener,在onScale方法中獲取縮放因子來繪製
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 } //在原先基礎上縮放 mRect.right = mRect.left + (viewWidth / scale).toInt() mRect.bottom = mRect.top + (viewHeight / scale).toInt() mScale = scale postInvalidate() return super.onScale(detector) } }
這裡別忘記了別事件傳遞出來,對於邊界case可自行處理
override fun onTouch(v: View?, event: MotionEvent?): Boolean { mGestureDetector.onTouchEvent(event) mScaleGestureDetector.onTouchEvent(event) return true }
下面附上大圖治理的流程圖
黃顏色模組: BitmapFactory.Options設定,避免整張大圖直接載入在記憶體當中,通過開啟記憶體複用(inMutable),使用區域解碼器,繪製一塊可見區域‘
淺黃色模組: View的繪製流程
以上就是Android效能優化大圖治理範例詳解的詳細內容,更多關於Android效能優化大圖治理的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45