Android 协程加载图片小练习

2022-03-17  本文已影响0人  雁过留声_泪落无痕

背景

看 kotlin 协程相关的文章时,看到了扔物线的一篇文章,Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了

文章末尾有个小练习题:

使用协程下载一张图,并行进行两次切割

一次切成大小相同的 4 份,取其中的第一份
一次切成大小相同的 9 份,取其中的最后一份
得到结果后,将它们展示在两个 ImageView 上。

这里进行一个简单的实现。

依赖

    def lifecycle_version = "2.4.1"
    // implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    implementation("io.coil-kt:coil:2.0.0-rc01")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")

这里用到了 coil,也是将协程用到炉火纯青的一个图片库

实现

class MainActivity : AppCompatActivity() {

    companion object {
        private const val IMAGE =
            "https://images.unsplash.com/photo-1550979068-47f8ec0c92d0?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjU4MjM5fQ"
    }

    private lateinit var mBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)

        go()
    }

    private fun go() {
        lifecycleScope.launch {
            val context = this@MainActivity
            val request = ImageRequest.Builder(context).data(IMAGE).build()
            val result = context.imageLoader.execute(request)
            if (result is SuccessResult) {
                showToast("fetch image success.")
                // split image to 4 parts
                val firstList = async {
                    splitImage(result.drawable as BitmapDrawable, 2, 2)
                }
                // split image to 9 parts
                val secondList = async {
                    splitImage(result.drawable as BitmapDrawable, 3, 3)
                }
                mBinding.first.setImageBitmap(firstList.await()[0])
                mBinding.second.setImageBitmap(secondList.await()[8])
            } else {
                showToast("fetch image error: ${(result as ErrorResult).throwable.message}")
            }
        }
    }

    private suspend fun splitImage(
        drawable: BitmapDrawable,
        horizontalParts: Int,
        verticalParts: Int
    ): List<Bitmap> {
        if (horizontalParts <= 0 || verticalParts <= 0) {
            return listOf()
        }
        val width = drawable.intrinsicWidth / horizontalParts
        val height = drawable.intrinsicHeight / verticalParts

        val list = mutableListOf<Bitmap>()
        for (i in 0 until horizontalParts) {
            for (j in 0 until verticalParts) {
                withContext(Dispatchers.Default) {
                    Bitmap.createBitmap(drawable.bitmap, i * width, j * height, width, height).let {
                        list.add(it)
                    }
                }
            }
        }

        return list
    }

    private fun showToast(msg: String) {
        Toast.makeText(this@MainActivity, msg, Toast.LENGTH_SHORT).show()
    }

}

效果

原图 效果图

改进1

上面的写法中,虽然也是并行处理的,但是 second 要显示出来,必须要 first 先显示完成,因为 await 是会挂起的;假设 first 操作很慢,而 second 操作很快,会导致即使 second 已经完成了操作但是还是显示不出来的问题。

而两个操作本质上没有相关性,于是考虑改为单独处理并单独显示,如下:

private fun goPro() {
    lifecycleScope.launch {
        val context = this@MainActivity
        val request = ImageRequest.Builder(context).data(IMAGE).build()
        val result = context.imageLoader.execute(request)
        if (result is SuccessResult) {
            showToast("fetch image success.")
            // split image to 4 parts
            launch {
                val list = splitImage(result.drawable as BitmapDrawable, 2, 2)
                mBinding.first.setImageBitmap(list[0])
            }
            // split image to 9 parts
            launch {
                val list = splitImage(result.drawable as BitmapDrawable, 3, 3)
                mBinding.second.setImageBitmap(list[8])
            }
        } else {
            showToast("fetch image error: ${(result as ErrorResult).throwable.message}")
        }
    }
}

改进2

为了看上去更直观,添加了模糊效果,题目中要求提取的部分图片不模糊,其余部分模糊化显示;整个图片用的 RecyclerView 装载,这里在 Adapter 中动态设置了图片的尺寸:

private fun goPro() {
    lifecycleScope.launch {
        val context = this@MainActivity
        val request = ImageRequest.Builder(context)
            .data(IMAGE)
            // 禁止使用 Hardware Bitmap,否则模糊处理会失败
            .allowHardware(false)
            .build()
        val result = context.imageLoader.execute(request)
        if (result is SuccessResult) {
            showToast("fetch image success.")
            // split image to 4 parts
            launch {
                val list = splitImage(result.drawable as BitmapDrawable, 2, 2)
                mFirstAdapter.setData(
                    blurList(this@MainActivity, list, 0),
                    result.drawable.intrinsicWidth / 6,
                    result.drawable.intrinsicHeight / 6
                )
            }
            // split image to 9 parts
            launch {
                val list = splitImage(result.drawable as BitmapDrawable, 3, 3)
                mSecondAdapter.setData(
                    blurList(this@MainActivity, list, 8), result.drawable.intrinsicWidth / 9,
                    result.drawable.intrinsicHeight / 9
                )
            }
        } else {
            showToast("fetch image error: ${(result as ErrorResult).throwable.message}")
        }
    }
}

private suspend fun blurList(context: Context, list: List<Bitmap>, exclude: Int): List<Bitmap> {
    if (exclude < 0 || exclude >= list.size) {
        return list
    }

    val blurList = mutableListOf<Bitmap>()
    withContext(Dispatchers.Default) {
        list.withIndex().forEach {
            if (it.index != exclude) {
                val blurBitmap = BlurUtil.myBlur(context, it.value, 1.0f / 8, 8)
                blurList.add(blurBitmap)
            } else {
                blurList.add(it.value)
            }
        }
    }
    return blurList
}
模糊显示
上一篇下一篇

猜你喜欢

热点阅读