如何在安卓系统中用Coil进行图像处理

413 阅读5分钟

在安卓中使用Coil进行图像处理

安卓开发者必须知道的基本技能之一是如何正确处理图像,特别是来自远程源的图像。这是因为许多应用程序以某种方式使用图像。

图像处理可以通过许多不同的方式进行,这取决于你想要实现的功能。在大多数情况下,你可能不仅需要进行加载,还需要进行内存和磁盘缓存,对内存中的图像进行下采样,重新使用位图,并自动暂停/取消请求。

在本教程中,我们将学习如何使用一个快速、轻量级、易于使用的库Coil ,来执行上述任务以及更多的任务。

Coil 它是用Kotlin制作的,并且有嵌入式的coroutine支持,这使得它适用于现代的Android开发。

前提条件

在开始学习本教程之前,请确保你已经熟悉了。

  • [在Android开发中]使用[Kotlin]。
  • 约束布局的Flow辅助部件
  • 视图绑定和/或[数据绑定]
  • [Kotlin Coroutines]的基础知识

开始吧

让我们从创建一个Android项目开始,我们将使用这个教程。

为了在我们的应用程序中包含Coil,我们需要以下已经发布在mavenCentral() 的依赖项。

注意,在写这篇文章的时候,使用的Coil 库是version 1.2.2

implementation("io.coil-kt:coil:1.2.2")

这是默认的依赖关系,与ImageView 扩展函数和Coil 单元一起。它高度依赖于io.coil-kt:coil-base 。这个依赖项支持静态图像的加载。

implementation("io.coil-kt:coil-base:1.2.2")

这是一个基本的工件,主要是由其他依赖关系所依赖的。与io.coil-kt:coil 不同,它不包括ImageView 的扩展函数和Coil 的单子。

implementation("io.coil-kt:coil-gif:1.2.2")

包括GIF 解码器,允许你在你的应用程序中显示GIF 图片。Animated WEBP 需要Android 9.0+ ,而animated HEIF 图片需要Android 11.0+

implementation("io.coil-kt:coil-svg:1.2.2")

上述依赖性支持SVG的解码。

implementation("io.coil-kt:coil-video:1.2.2")

io.coil-kt:coil-video:1.2.2 插件为Android中支持的视频编解码器提供视频框架。编解码器是对数字数据流进行解码和编码的软件。

选择正确的工件是基于你的应用需求。幸运的是,在同一个项目中可以使用一个以上的依赖关系。

说到这里,将以下内容添加到模块级的build.gradle 文件中,因为我们只对纯图像进行工作。

dependencies{
    implementation("io.coil-kt:coil:1.2.2")
}

注意,Coil 依赖于Java-8 。要启用这一点,在同一个build.gradle 文件中包括以下内容。

android {
    compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
}

最后,Coil需要互联网许可,因为我们将获取远程图像。在Manifest 文件中加入以下一行。

<uses-permission android:name="android.permission.INTERNET"/>

我们现在可以同步该项目了。

构建用户界面

在这个项目中,我们将使用一个单一的ImageView 和几个Buttons 。每个按钮将代表一个修改加载图像的功能。

在你的activity_main.xml 文件中,添加下面的代码。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_margin="16dp"
        android:src="@drawable/ic_launcher_foreground"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/stateText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="State: Default"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/flow"
        app:layout_constraintEnd_toEndOf="@+id/imageView"
        app:layout_constraintStart_toStartOf="@+id/imageView"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        app:constraint_referenced_ids="circle,rounded,grayScale,crossFade,blur,crop,placeholder,error,combined"
        app:flow_horizontalGap="8dp"
        app:flow_horizontalStyle="packed"
        app:flow_verticalStyle="spread"
        app:flow_wrapMode="chain"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/imageView"
        app:layout_constraintVertical_bias="1" />

    <Button
        android:id="@+id/circle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Circle Trans" />

    <Button
        android:id="@+id/rounded"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="rounded" />

    <Button
        android:id="@+id/grayScale"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Gray scale" />

    <Button
        android:id="@+id/crossFade"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="cross fade" />

    <Button
        android:id="@+id/blur"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="blur" />

    <Button
        android:id="@+id/crop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="crop" />

    <Button
        android:id="@+id/placeholder"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Placeholder" />

    <Button
        android:id="@+id/error"  
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Error Image" />

    <Button
        android:id="@+id/combined"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="combined trans" />
</androidx.constraintlayout.widget.ConstraintLayout>

Flow是一个辅助部件,它允许我们在其内部对齐视图,而不约束每个被引用的视图。

上面的代码在一个水平包装、垂直传播的链式流中添加了按钮。在imageviewflow 之间是一个TextView ,表示图片的当前状态。

请看下面的预览。

Design preview

图片来源

Coil支持三种主要的图像来源。一个load() 方法被用来显示图像。这是一个扩展函数,扩展了ImageView 类。它提供了一个请求生成器lambda,大部分的操作功能在这里被应用。

下面是主要的图片来源。

URL

这使用了一个指向你想加载到目标中的远程图像的链接。

// for instance
imageView.load("https://example.images/example.png")

可绘资源

它从项目文件中加载图像。

imageView.load(R.drawable.image)

文件资源

文件资源利用来自主机设备的给定图像。

imageView.load(File("/path/to/image"))

启用视图绑定

viewBinding 允许我们以一种更简化的方式访问UI视图。它利用了由 库生成的绑定类。viewBinding

为了启用viewBinding ,在模块级的gradle.build 文件中添加以下代码,并进行同步。

android{
    buildFeatures{
        viewBinding true
    }
}

现在,我们就可以开始了!

图像处理功能

在这里,我们监听button 的点击并执行相应的动作。首先,打开MainActivity.kt ,添加以下启动代码。

class MainActivity : AppCompatActivity() {
    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!
    override fun onCreate(savedInstanceState: Bundle?) {
        supportActionBar?.hide()
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // we'll handle clicks here
    }
}

在上面的代码中,我们已经使用viewBinding膨胀了用户界面。

向目标加载图片

在这一步中,我们将从一个URL加载一张图片到目标(ImageView)。由于这将包括几个操作,最好将样本链接与主代码分开。

因此,创建一个名为ImageLink 的Kotlin类,并在其中添加以下代码。

class ImageLinks {
    // array of links to free images on the internet
    private val links = arrayListOf<String>(
        "https://images.freeimages.com/images/large-previews/825/linked-hands-1308777.jpg",
        "https://images.unsplash.com/photo-1541443131876-44b03de101c5?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&dl=mathieu-renier-4WBvCqeMaDE-unsplash.jpg",
        "https://images.unsplash.com/photo-1549399542-7e3f8b79c341?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&dl=roberto-nickson-zu95jkyrGtw-unsplash.jpg",
        "https://www.cnet.com/a/img/XtH050ErlMIQxKn_HYUx2plJnDc=/940x528/2020/12/17/c9a829c8-69d6-4299-b2d0-cf9624aa7556/2021-acura-tlx-a-spec-65.jpg",
        "https://cdn.jdpower.com/JDPA_2021%20Acura%20TLX%20Advance%20Red%20Front%20View.jpg",
        "https://s3-us-east-2.amazonaws.com/matter-blog/2020/09/People_Person_Cover_Image.png",
        "https://images.fandango.com/ImageRenderer/0/0/redesign/static/img/default_poster.png/0/images/masterrepository/other/ant_man_ver5.jpg"
    )
    fun randomLink(): String {
        return links.random()
    }
}

上面的randomLink() 函数,每当我们调用它时,都会返回一个随机选择的链接。这确保了我们不会在每个进程中使用相同的图片。

让我们来管理我们应用程序中的状态。

打开MainActivity.kt 文件,在onCreate() 方法的下面添加以下代码。


private fun updateState(newState: String){
    binding.stateText.text = "State: $newState"
}

上面的函数设置了新的状态,以帮助我们识别对图像进行的最新操作。

过渡

过渡是切换状态时产生的持续时间。

交叉渐变

这是一个动画过渡,它将imageView上的变化可视化。它是通过在给定的时间内从0 t0 1 ,改变不透明度来实现的。

// triggered when crossfade button is clicked...(this applies to other snippets as well)

binding.crossFade.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.load(link) {
        crossfade(750) // 75th percentile of a second
        build()
    }
    // set current state
    updateState("cross fade")
}

交叉渐变的持续时间是由crossfade() 函数中传递的时间(微秒)决定的。

变换

图像变换是可以应用于ImageView 的操作技术。Coil支持以下变换。

圆角

这可以在图像上创建弯曲的角落。圆角的程度是由RoundedCornersTransformation() 函数中传递的浮动值决定的。

binding.rounded.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.load(link) {
        transformations(
            RoundedCornersTransformation(8F)
        )
        build()
    }
    updateState("Rounded")
}

模糊化

这涉及到对图像质量的操作。

binding.blur.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.load(link) {
        transformations(
            BlurTransformation(this@MainActivity,radius = 8f),
        )
        build()
    }
    updateState("Blur")
}

模糊半径越大,图像就越模糊。当我们不希望用户看到某些图像或内容时,模糊会有帮助。例如,当服务只要求高级访问时。

圆形裁剪

圆形裁剪是在可用的尺寸范围内形成一个圆形的图像。

binding.circle.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.load(link) {
        transformations(
            CircleCropTransformation()
        )
        build()
    }
    updateState("Circle Crop")
}

灰度(Grayscale

灰度是指一种脱色的图像格式。所有颜色都变成了黑色或白色。

binding.grayScale.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.load(link) {
        transformations(
            GrayscaleTransformation(),
        )
        build()
    }
    updateState("Grayscale")
}

占位符

顾名思义,placeholders ,是指在设置实际图像之前,预先设置在ImageView 的图像。

当一个请求完成后,占位符会立即被替换成预定的图像。由于这个原因,它可能不会被视觉检测到,特别是当一个图像请求完成得非常快的时候。

binding.placeholder.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.load(link){
        /* create a drawable resource to use here */
        placeholder(R.drawable.ic_placeholder)
        build()
    }
    updateState("Placeholder")
}

裁剪

它允许我们硬编码尺寸或图像大小。请注意,在使用Coil的size() 功能时,我们不指定测量单位,因为默认单位是像素。

binding.crop.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.apply {
        scaleType = ImageView.ScaleType.CENTER_CROP
        load(link) {
            size(300, 300)
        }
    }
}

组合变换

在某些情况下,你可能想在同一图像上同时应用一个以上的变换。这可以通过以下方式实现。

binding.combined.setOnClickListener {
    val link = ImageLinks().randomLink()
    binding.imageView.load(link) {
        crossfade(750)
        transformations(
            CircleCropTransformation(),
            GrayscaleTransformation() // e.t.c
        )
        build()
    }
    updateState("Combined State")
}

注意使用逗号来分隔变换。

取消请求

在某些情况下,一个网络请求需要被取消,特别是当它需要太长的时间来响应时。请求取消也被称为处置,如下面的片段所示。

imageView.load(link){
        ...
    }.dispose()

这通过取消任何正在进行的工作来释放与该请求相关的资源。这种方法是empotent的,意味着多个应用程序不会影响第一个应用程序的结果。

内存缓存

缓存使显示远程图像成为可能,即使在互联网连接中断时也是如此。要做到这一点,图像必须至少在一个实例中被取走。

Coil利用了OkHttp ,这是一个支持SPDY 协议的HTTP client

异常处理

跟踪一个请求是很重要的,因为它有助于处理过程中遇到的错误。

Coil使用一个接受两个lambdas的onSuccessonErrorlistener() 函数,如下图所示。

// handle error button click
binding.error.setOnClickListener {
    binding.imageView.load("https://a/bad/link") {
        listener(
            // pass two arguments
            onSuccess = { _, _ ->
                Toast.makeText(this@MainActivity, "Success", Toast.LENGTH_SHORT).show()
            },
            onError = { request: ImageRequest, throwable: Throwable ->
                request.error
                Toast.makeText(this@MainActivity, "$throwable", Toast.LENGTH_SHORT).show()
            })
        // setup error image
        error(R.drawable.ic_error_image)
        
    }
}

一个错误图像被用来表示在解决请求时出了问题。

总结

恭喜你完成了本教程。你现在可以自如地使用Coil 来处理Android中的图像。因此,你可以利用这些知识来构建更强大的应用程序。