主頁 > 移動端開發 > 學習筆記-android大圖加載詳解

學習筆記-android大圖加載詳解

2021-09-15 10:01:49 移動端開發

關于android中大圖處理的采樣、縮放、平移、分塊、并行、漸進加載,

1. 圖片加載基礎

1.1. 引數意義

圖片加載程序涉及到

  • dpi:螢屏像素密度,每英寸內的像素點數,基準密度是160dpi
  • density:密度,比例值,等價于dpi/160
  • dp:密度無關像素單位,在所有螢屏上顯示效果一直,1dp相當于160dpi的螢屏上面的一個像素,
  • px:實際像素單位,相當于dp * density

像素點存盤的資料格式使用Bitmap.config 表示,用 ARGB來表示的,A 表示透明度;R 表示紅色;G 表示綠色;B 表示藍色,

  • ALPHA_8:A占8位,共占用1個位元組,只有透明度,沒有顏色,
  • RGB_565:R占5位,G占6位,B占5位,共占用2個位元組,
  • ARGB_4444:每個通道各占4位,共占用2個位元組,顯示效果較差,
  • ARGB_8888:每個通道各占8位,共占用4個位元組,
  • RGBA_F16:每個通道各占16位,共占用8個位元組,

1.2. 記憶體占用

圖片有多重存盤方式,webp,png,jpg等等,他們會根據各自的壓縮規則進行壓縮,

當需要把圖片加載到記憶體中時,會把每個像素點都加載到記憶體中,不會對相同像素進行壓碩訓者替換,

所以會有一個簡單的公式,Bitmap 的記憶體占用等于Bitmap寬度 * Bitmap高度 * 單位像素所占用位元組

眾所周知,Bitmap有繞不過去的問題,就是OOM,對Bitmap的優化重點就是記憶體問題,通過公式可以看出來,優化只能通過兩個方面來,一是寬高,二是單位像素所占用位元組,

單位像素所占用位元組只能通過更改Bitmap.config改變像素存盤格式來調整,比如如果沒有ALPHA通道的話,從ARGB_8888更改為RGB_565,每個像素占用的空間會直接小一倍,Bitmap占用的記憶體也會直接小一倍,貌似看起來是很喜人的結果,

雖然很多的地方都在說,ARGB_8888更改為RGB_565對顯示效果影響不大,但來算一下的話,ARGB_8888每個通道2^8 = 256 ,而RGB_565的只有2^5 = 32,這個數量級相差的巨大,也就意味著圖片的質量也是相差巨大的,大多數的場景下還是建議不要這樣,

既然單位所占用位元組不建議去做,那就只能在尺寸上下功夫,

這是后就需要使用Bitmap.Options圖片進行解碼時的配置引數,來控制加載的尺寸,比如:

  • inJustDecodeBounds:如果設定true,只查詢Bitmap,但不加載對應像素資料,用于獲得圖片的資料,比如長、寬之類的,
  • inSampleSize:采樣比例,比如設定為4的話,就會將原始圖片4 * 4的像素塊讀取為1 * 1的像素塊,需要設定為2的指數,否則向下取整,

1.3. 使用采樣比例優化

當要把一張圖片顯示出來的時候,顯示的區域是有限的,dpi是有限的,所能展示的也是有限的,而圖片的像素塊又是無限的,這就是優化的第一步,加載螢屏顯示上限的解析度與加載原始圖片的解析度,顯示效果并不會有變化,換一個更直接的說法,一個物理像素塊最多只能展示一個影像像素塊的資料,所以加載圖片時,只需要讓尺寸滿足大于物理像素塊即可,

具體的可以設定一個minimumDPi,表示圖片顯示的最小dpi,用來控制最小的加載加載密度,

有了dpi的加入之后,像素點px的計算就轉換成了dp的計算,又px = dp * dpi,所以目標像素數量reqWidth / minimumDPi = vWidth * averageDpi,原始大小和目標像素的比值就是采樣比例,

val vWidth: Int = 0  // view寬高,px
val vHeight: Int = 0
val sWidth: Int = 0 // 圖片寬高
val sHeight: Int = 0
val minimumDPi = 320 //可以設定一個的最低Dpi

private fun calculateInSampleSize(): Int {
    val metrics: DisplayMetrics = getResources().getDisplayMetrics()
    val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 //螢屏dpi
    //Dpi比例
    val scaleDpi = 1f * minimumDPi / averageDpi

    //顯示的目標像素數量
    val reqWidth = (vWidth * scaleDpi).toInt()
    val reqHeight = (vHeight * scaleDpi).toInt()

    var inSampleSize = 1

    if (sHeight > reqHeight || sWidth > reqWidth) {
        //原影像素量與目標像素量的比值就是采樣比例
        val heightRatio = (1f * sHeight / reqHeight).roundToInt()
        val widthRatio = (1f * sWidth / reqWidth).roundToInt()
        //取較小值,對應的是FIT_CENTER的顯示方式,
        inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio
    }
    //保證2的指數
    var power = 1
    while (power * 2 < inSampleSize) {
        power *= 2
    }
    return if (power > 1) power / 2 else power
}

2. 圖片手勢操作

前面介紹了使用采樣比例優化的方法,處理的場景是圖片只在控制元件中進行展示,為了可以繼續分析圖片在控制元件中可以放大縮小和平移的情況,先介紹一下手勢操作的實作,

2.1. 坐標系

在視圖坐標系中,以左上角為原點,橫向為X軸,縱向為Y軸,單位長度是像素點,

以同樣的道理設立一個圖片坐標系,以左上角為原點,橫向為X軸,縱向為Y軸,單位長度是原始圖片像素點,原始圖片像素點,重要的說兩遍,只有使用原始圖片像素點坐標系才是固定的,采樣后不一定固定,

2.2. 坐標系偏移

圖片的放大縮放和平移的程序中,實際上就是兩個坐標系的相互關系發生了改變,

定義兩個引數來表示縮放比例和偏移值,將兩個坐標系聯系起來,

  • scale:縮放比例,相同面積內,視影像素點數與原始圖片像素點數的比值,
  • translate:偏移值,圖片原點在視圖坐標系中的位置,

使用定義的scaletranslate,可以很簡單完成兩個坐標系的相互轉換,

比如有一個點,在視圖坐標系坐標為vPoint,在圖片坐標系坐標為sPoint,那么兩者的關系應該是:

vPoint = sPoint * scale + translate

這樣就可以完全的確認一個點在視圖以及圖片中的位置,

2.3. 引數初始化

默認圖片的顯示方式是FIT_CENTER,即把圖片按比例擴大/縮小到View的寬度,居中顯示,那么在初始化的時候:

  • 把圖片按比例擴大/縮小到View的寬度,長或者寬中較小的一個與view寬高相同,那么這時候的縮放比例也就是是控制元件大小和原始圖片大小比值中的較小值,
  • 圖片居中顯示并且有一邊對齊,那么偏移值是控制元件大小和圖片顯示大小差值的一半,
fun initScaleAndTranslate(){
    scale = Math.min(vWidth / sWidth, vHeight / sHeight)
    translate.x = (vWidth - scale * sWidth) / 2
    translate.y = (vHeight - scale * sHeight) / 2
}

2.4. 手勢處理

接著是在手勢操作與縮放比例以及偏移量的關系,這里只介紹雙指的處理,

為了讓手勢操作跟手,處理的原則只有一點,兩個手指的距離和中心點在圖片坐標系中是固定的,

在視圖坐標系,兩個手指的距離d和兩個手指的中心點PointCenter,對應scaletanslation的變化,

手指在圖片坐標系中距離s固定,又s * scale = dscale的值應該是:

s1 = s2 
d1 / scale1 = d2 / scale2 
scale2 = scale1 * (d2 / d1)

手指在圖片坐標系中心點坐標sPointCenter固定,又vPoint = sPoint * scale + translationtranslation應該是:

sPointCenter1 = sPointCenter2
(vPointCenter1 - translation1) / scale1 = (vPointCenter2 - translation2) / scale2
vtranslation2 = vPointCenter2 - (vPointCenter1 - translation1) * (scale2 / scale1)
vtranslation2 = vPointCenter2 - (vPointCenter1 - translation1) * (d2 / d1)

下面是具體的代碼實作:

var scaleStart: Float = 0f
var vDistStart = 0f

var leftStart = 0f
var topStart = 0f

fun onTouchEvent(event: MotionEvent) {
    if (event.pointerCount < 2) return
    when (event.action) {
        MotionEvent.ACTION_POINTER_2_DOWN -> {

            //兩個手指起始觸碰點的絕對距離
            vDistStart = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1))

            scaleStart = scale

            //兩個手指起始觸碰點的中心點坐標
            val centerStartX = (event.getX(0) + event.getX(1)) / 2
            val centerStartY = (event.getY(0) + event.getY(1)) / 2

            //公式計算出的中間值
            leftStart = centerStartX - translate.x
            topStart = centerStartY - translate.y
        }
        MotionEvent.ACTION_MOVE -> {

            //兩個手指觸碰點的絕對距離
            val vDistEnd: Float = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1))

            //兩個手指觸碰點的中心點坐標
            val vCenterEndX = (event.getX(0) + event.getX(1)) / 2
            val vCenterEndY = (event.getY(0) + event.getY(1)) / 2

            //縮放比例調整
            scale = scaleStart * (vDistEnd / vDistStart)

            //偏移值調整
            val leftNow = leftStart * (vDistEnd / vDistStart)
            val topNow = topStart * (vDistEnd / vDistStart)
            translate.x = vCenterEndX - leftNow
            translate.y = vCenterEndY - topNow
        }
        ...
    }
}
private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
    val x = x0 - x1
    val y = y0 - y1
    return sqrt(x * x + y * y)
}

2.5. 新的采樣比例計算方法

scale定義的是相同面積內視影像素點數與原始圖片像素點數的比值,是px的比值,

而采樣比例前面推導的的是兩者之間dp的比值,而這也就是scaleDpi的倒數,

所以前面計算采樣比例的方法在這里可以大大精簡,

val minimumDPi = 320 //可以設定不同的最低Dpi

private fun calculateInSampleSize(): Int {
    val metrics: DisplayMetrics = getResources().getDisplayMetrics()
    val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 //螢屏dpi
    //Dpi比例
    val scaleDpi = 1f * minimumDPi / averageDpi

    val inSampleSize = (1f / scale / scaleDpi).roundToInt()

    //保證2的指數
    var power = 1
    while (power * 2 < inSampleSize) {
        power *= 2
    }
    return if (power > 1) power / 2 else power
}

3. 圖片加載與顯示

在互動放大的程序中,scale一直在變化,也就所需要的采樣比例也可能發生變化,

在這個程序中,如果每一次都將圖片不同采樣比例完整的加載,就會導致一張圖片的多種解析度加載進記憶體,甚至超過將圖片完全加載的大小,

另一點采樣比例的變化肯定是伴隨著圖片的放大,而在這個時候,圖片中可能大部分的區域都不會顯示在螢屏中,也就是多了很多并沒有用到的記憶體占用,

3.1. BitmapRegionDecoder

官方給出了一個方案BitmapRegionDecoder,使用它可以只加載指定區域的的影像,

BitmapRegionDecoder使用比較簡單,通過newInstance()創建BitmapRegionDecoder物件,構造時的布林值引數表示創建的流是否強參考(false表示強參考),

val filePath = ""
val decoder = BitmapRegionDecoder.newInstance("imgPath", false)

創建好物件之后就可以呼叫方法decodeRegion()加載對應區域的bitmap,引數分別代表加載的區域和配置,

val rect = Rect()
val options = BitmapFactory.Options().also {
    it.inSampleSize = calculateInSampleSize()
}
val bitmap = decoder.decodeRegion(rect, options)

3.2. 分塊

有了BitmapRegionDecoder之后,并不能直接使用來加載,圖片的操作程序中,會非常頻繁的改變顯示區域,直接加載消耗不可估量,

比較好的解決方法是分塊加載,

分塊加載的基本原理就是將圖片切割成一小塊一小塊的區域,等到這部分進入到了顯示區域去加載它,

將最高的采樣比例完全加載,作為背景放置,圖片放大達到新的采樣比例之后,將當前需要顯示的影像快再加載進來,進行繪制,就可以很好的平衡解析度和記憶體之間的問題,

為了方便管理分塊,建立一個分塊物件,

每個物件中維護自身的位置資訊,以及加載所需資料和加載狀態,

分塊之后,每一塊獨自進行加載,并且加載bitmap也是個耗時操作,將加載bitmap異步實作,這樣單獨的塊加載完成后觸發重繪即可,

class Tile(
    sampleSize: Int,
    val sRect: Rect, //加載區域
    val decoder: BitmapRegionDecoder, //加載器
    val decoderLock: ReadWriteLock //鎖
) {
    var bitmap: Bitmap? = null
    var loading = false
    var visible = false
    private val options = BitmapFactory.Options()

    init {
        options.inSampleSize = sampleSize
    }

    //加載bitmap
    suspend fun startLoadBitmap() {
        if (loading || !visible) return
        loading = true
        bitmap = withContext(Dispatchers.IO) {
            tryLoadBitmap()
        }
        loading = false
    }

    private fun tryLoadBitmap(): Bitmap? {
        decoderLock.readLock().lock()
        try {
            return decoder.decodeRegion(sRect, options)
        } catch (e: Exception) {
        }
        finally {
            decoderLock.readLock().unlock()
        }
        return null
    }
}

View中觸發加載,使用lifecycleScope啟動協程,忽略掉生命周期的處理,

因為在Tile.startLoadBitmap()中做了阻塞,所以在bitmap加載完成后才會呼叫invalidate()觸發重繪,

fun loadBitmap(tile: Tile) {
    findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
        tile.startLoadBitmap()
        invalidate()
    }
}

3.3. 分塊邏輯

分塊的觸發應該在初始化完成之后,拿到了最高的采樣比例,將所有可能出現的采樣比例全部分好塊,

對于最高的采樣比例不進行分塊,需要全部加載進來,作為整體的背景進行展示,

對于其他的采樣比例都需要進行分塊,分塊的規則不需要過于嚴格,只需要分割形成的像素塊的大小和控制元件的大小比較接近即可,

這里采用的分割后每塊的大小不大于控制元件大小1.25倍的最小分割數,

    val tileMap = LinkedHashMap<Int, List<Tile>>() //所有的切片物件,key值是SampleSize,維持插入順序

    val decoder = BitmapRegionDecoder.newInstance("filePath", false) //加載器
    val decoderLock: ReadWriteLock = ReentrantReadWriteLock(true) //鎖
    var maxSampleSize: Int = 32 //最小縮放比例下的采樣率,也就是最高的采樣率

    fun initTiles() {
        tileMap.clear()
        maxSampleSize = calculateInSampleSize() //初始化時這里是最高采樣率

        //首先創建最高采樣率的切片并觸發加載,這個切片直接是圖片的大小,作為背景
        val tile = Tile(
            sampleSize = maxSampleSize,
            sRect = Rect(0, 0, sWidth, sHeight),
            decoder = decoder,
            decoderLock = decoderLock
        )
        tile.visible = true
        loadBitmap(tile) //觸發加載
        tileMap[maxSampleSize] = listOf(tile)

        //從最高采樣率/2,一直遞回創建所有切片(采樣率只能是2的指數)
        var sampleSize = maxSampleSize / 2

        //兩個方向的切片數
        var xTiles = 1
        var yTiles = 1
        while (sampleSize > 1) {
            //切片的原始大小
            var sTileWidth: Int = sWidth / xTiles
            var sTileHeight: Int = sHeight / yTiles
            //切片的加載大小
            var subTileWidth = sTileWidth / sampleSize
            var subTileHeight = sTileHeight / sampleSize

            //一直增加切片,直到滿足分割后每塊的大小不大于控制元件大小*1.25
            while (subTileWidth > vWidth * 1.25) {
                xTiles += 1
                sTileWidth = sWidth / xTiles
                subTileWidth = sTileWidth / sampleSize
            }
            while (subTileHeight > vHeight * 1.25) {
                yTiles += 1
                sTileHeight = sHeight / yTiles
                subTileHeight = sTileHeight / sampleSize
            }

            //創建tile
            val tileGrid = mutableListOf<Tile>()
            for (x in 0 until xTiles) {
                for (y in 0 until yTiles) {
                    tileGrid.add(
                        Tile(
                            sampleSize = sampleSize,
                            sRect = Rect(
                                x * sTileWidth, //根據切片大小和位置計算出在圖片坐標系中的位置
                                y * sTileHeight,
                                (x + 1) * sTileWidth,
                                (y + 1) * sTileHeight
                            ),
                            decoder = decoder,
                            decoderLock = decoderLock
                        )
                    )
                }
            }
            tileMap[sampleSize] = tileGrid
            sampleSize /= 2
        }
    }

3.4. 狀態重繪

分好塊之后還需要有一個觸發加載和回收的邏輯,原則就是現在它在顯示范圍內就觸發加載,離開顯示范圍就觸發回收,

實將控制元件的區域映射到圖片的坐標系上,然后和每塊維護的位置資訊做判斷,重疊就加載顯示,不重疊就觸發回收,

fun refreshTiles() {
    val sampleSize = calculateInSampleSize() //當前采樣率

    //將控制元件映射到圖片的坐標系
    val vRect = RectF()
    vRect.left = (0 - translate.x) / scale  
    vRect.right = (vWidth - translate.x) / scale
    vRect.top = (0 - translate.y) / scale
    vRect.bottom = (vHeight - translate.y) / scale

    tileMap.values.flatten().forEach { tile ->
        if (tile.sampleSize == maxSampleSize) { 
            // 確保作為背景的bitmap一直存在
            tile.visible = true
            if (!tile.loading && tile.bitmap == null) {
                loadBitmap(tile)
            }
        } else {
            // 采樣比例相等并且可見觸發加載,否則觸發回收
            if (tile.sampleSize == sampleSize && isTileVisible(tile.sRect, vRect)) {
                tile.visible = true
                if (!tile.loading && tile.bitmap == null) {
                    loadBitmap(tile)
                }
            } else {
                tile.visible = false
                tile.bitmap?.recycle()
                tile.bitmap = null
            }
        }
    }
}

//判斷兩個區域是否有重疊
fun isTileVisible(sRect: Rect, vRect: RectF): Boolean {
    return !(vRect.left > sRect.right
            || vRect.right < sRect.left
            || vRect.top > sRect.bottom
            || vRect.bottom < sRect.top)
}

3.5. 繪制

前面已經準備好了所有東西,現在只差一部,把這些切片繪制到螢屏上,

繪制前首先檢測所需要顯示的tile是否全部加載完成,如果加載完成了就只需要繪制這部分即可,如果加載沒完成就需要將背景也繪制一下,

tileMapLinkedHashMap,回圈時保證了按照插入的順序,從下向上渲染,越下面解析度越低,也就是低解析度的作為背景繪制,

//繪制
override fun onDraw(canvas: Canvas?) {
    if (tileMap.isEmpty()) {
        return
    }
    val sampleSize = min(maxSampleSize, calculateInSampleSize())

    //檢查需要顯示的tile是否全部加載完成
    var hasMissingTiles = false
    tileMap[sampleSize]?.forEach { tile ->
        if (tile.visible && (tile.loading || tile.bitmap == null)) {
            hasMissingTiles = true
        }
    }

    //如果顯示的tile加載完成,只繪制這部分即可
    //如果沒有加載完成,會嘗試對tile進行繪制
    if (hasMissingTiles) {
        tileMap.values.flatten()
    } else {
        tileMap[sampleSize]
    }?.filter {
        !it.loading && it.bitmap != null
    }?.forEach { tile ->
        resetMatrix(tile)
        canvas?.drawBitmap(tile.bitmap ?: return, mMatrix, bitmapPaint)
    }
}

val vRect = Rect()
val mMatrix = Matrix()
val bitmapPaint = Paint()
val srcArray = FloatArray(8)
val matrixArray = FloatArray(8)

//計算matrix,將圖片映射到顯示區域
fun resetMatrix(tile: Tile) {
    mMatrix.reset()
    val bitmap = tile.bitmap ?: return

    srcArray[0] = 0f
    srcArray[1] = 0f
    srcArray[2] = bitmap.width.toFloat()
    srcArray[3] = 0f
    srcArray[4] = bitmap.width.toFloat()
    srcArray[5] = bitmap.height.toFloat()
    srcArray[6] = 0f
    srcArray[7] = bitmap.height.toFloat()

    sRectToVRect(tile.sRect, vRect)
    matrixArray[0] = vRect.left.toFloat()
    matrixArray[1] = vRect.top.toFloat()
    matrixArray[2] = vRect.right.toFloat()
    matrixArray[3] = vRect.top.toFloat()
    matrixArray[4] = vRect.right.toFloat()
    matrixArray[5] = vRect.bottom.toFloat()
    matrixArray[6] = vRect.left.toFloat()
    matrixArray[7] = vRect.bottom.toFloat()

    mMatrix.setPolyToPoly(srcArray, 0, matrixArray, 0, 4)
}

//圖片坐標映射到控制元件坐標
fun sRectToVRect(sRect: Rect, vRect: Rect) {
    vRect.set(
        ((sRect.left * scale) + translate.x).toInt(),
        ((sRect.top * scale) + translate.y).toInt(),
        ((sRect.right * scale) + translate.x).toInt(),
        ((sRect.bottom * scale) + translate.y).toInt()
    )
}

推薦

90分鐘教你從零開始打造自定義圖片加載框架

即學即用的Android高級技能大長圖加載原理及手寫實作

Android App開發——如何從打造一款可商用的圖片加載框架?

Android進階之如何打造一個圖片加載框架

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/300273.html

標籤:其他

上一篇:Framework 廣播基礎知識和常見問題

下一篇:Android studio創建hello world

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more