Android Compose 框架图像与矢量图深入剖析(五十四)

上一期:Android Compose 框架图像与矢量图深入剖析(五十三)

下一期:Android Compose 框架尺寸与密度深入剖析(五十五)

本人掘金号,欢迎点击关注:https://juejin.cn/user/4406498335701950

一、引言

在 Android 开发中,图像和矢量图的处理是构建美观、交互性强的用户界面的重要组成部分。随着 Android Compose 框架的出现,为开发者提供了一种全新的声明式方式来处理图像和矢量图。Android Compose 以其简洁、高效和灵活的特点,极大地简化了图像和矢量图的使用和管理。

本博客将深入分析 Android Compose 框架中图像与矢量图的相关内容,从基础概念开始,逐步深入到源码级别,详细介绍图像和矢量图的加载、显示、绘制、优化等方面。通过对源码的分析,我们可以更好地理解 Android Compose 是如何实现这些功能的,以及如何在实际开发中更好地利用这些功能来构建出色的用户界面。

二、Android Compose 中图像与矢量图的基础概念

2.1 图像(Image)

在 Android Compose 中,图像是指各种格式的位图(Bitmap),如 JPEG、PNG 等。图像可以从不同的来源加载,包括本地资源、网络、文件系统等。Compose 提供了 Image 组件来显示图像,它可以根据需要进行缩放、裁剪、变换等操作。

2.2 矢量图(Vector Graphics)

矢量图是使用数学公式来描述图形的图像,与位图不同,矢量图可以无损缩放,不会出现锯齿或模糊的情况。在 Android Compose 中,矢量图通常以 XML 文件的形式存在,使用 VectorPainter 来绘制。矢量图适用于需要高分辨率显示和动态调整大小的场景,如图标、图形化界面等。

三、图像的加载与显示

3.1 使用 Image 组件显示本地资源图像

在 Android Compose 中,最简单的显示图像的方式是使用 Image 组件显示本地资源图像。以下是一个示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun LocalResourceImage() {
    // 使用 painterResource 函数从资源中获取图像的 Painter
    val painter = painterResource(id = R.drawable.sample_image)
    // 使用 Image 组件显示图像
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "本地资源图像", // 设置图像的描述,用于无障碍访问
        modifier = androidx.compose.ui.Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
    )
}

@Preview
@Composable
fun LocalResourceImagePreview() {
    LocalResourceImage()
}

在上述代码中,painterResource 函数用于从资源中获取图像的 Painter 对象,Painter 是一个用于绘制图像的抽象类。Image 组件接收 Painter 对象,并根据 modifier 进行布局和样式设置。

3.1.1 painterResource 源码分析

painterResource 函数的源码如下:

kotlin

@Composable
fun painterResource(
    id: Int
): Painter {
    // 获取当前的资源环境
    val resources = LocalContext.current.resources
    // 使用 ResourceLoader 加载资源
    return remember { ResourceLoader(resources).load(id) }
}

在这个函数中,首先通过 LocalContext.current.resources 获取当前的资源环境,然后使用 ResourceLoader 类的 load 方法加载指定 ID 的资源。ResourceLoader 类的 load 方法源码如下:

kotlin

class ResourceLoader(private val resources: Resources) {
    fun load(id: Int): Painter {
        // 根据资源 ID 获取资源类型
        val resourceTypeName = resources.getResourceTypeName(id)
        return when (resourceTypeName) {
            "drawable" -> {
                // 如果是 drawable 资源,使用 DrawablePainter 绘制
                val drawable = resources.getDrawable(id, null)
                DrawablePainter(drawable)
            }
            else -> throw IllegalArgumentException("Unsupported resource type: $resourceTypeName")
        }
    }
}

load 方法中,首先根据资源 ID 获取资源类型,然后根据资源类型进行不同的处理。如果是 drawable 资源,使用 DrawablePainter 来绘制图像。

3.1.2 Image 组件源码分析

Image 组件的源码如下:

kotlin

@Composable
fun Image(
    painter: Painter,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = 1f,
    colorFilter: ColorFilter? = null
) {
    // 创建一个 ImagePainter 对象,用于绘制图像
    val imagePainter = remember(painter, alignment, contentScale, alpha, colorFilter) {
        ImagePainter(painter, alignment, contentScale, alpha, colorFilter)
    }
    // 使用 Canvas 组件进行绘制
    Canvas(modifier) {
        imagePainter.draw(this)
    }
}

Image 组件中,首先创建一个 ImagePainter 对象,该对象封装了图像的绘制逻辑,包括对齐方式、内容缩放、透明度和颜色滤镜等。然后使用 Canvas 组件进行绘制,调用 imagePainterdraw 方法将图像绘制到画布上。

3.2 从网络加载图像

在实际开发中,经常需要从网络加载图像。Compose 本身没有提供直接的网络图像加载功能,但可以使用第三方库,如 Coil 或 Glide。以下是使用 Coil 加载网络图像的示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest

@Composable
fun NetworkImage() {
    // 使用 rememberAsyncImagePainter 函数从网络加载图像
    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(LocalContext.current)
           .data("https://example.com/image.jpg") // 设置图像的 URL
           .crossfade(true) // 启用淡入淡出效果
           .build()
    )
    // 使用 Image 组件显示图像
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "网络图像", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
        contentScale = ContentScale.Crop // 设置内容缩放方式
    )
}

@Preview
@Composable
fun NetworkImagePreview() {
    NetworkImage()
}

在上述代码中,rememberAsyncImagePainter 函数用于从网络加载图像,它接收一个 ImageRequest 对象,该对象可以设置图像的 URL、加载选项等。Image 组件用于显示加载的图像。

3.2.1 rememberAsyncImagePainter 源码分析

rememberAsyncImagePainter 函数的源码如下:

kotlin

@Composable
fun rememberAsyncImagePainter(
    model: Any?,
    imageLoader: ImageLoader = LocalImageLoader.current,
    placeholder: Painter? = null,
    error: Painter? = null,
    fallback: Painter? = null,
    onState: ((AsyncImagePainter.State) -> Unit)? = null,
    contentScale: ContentScale = ContentScale.Fit,
    alignment: Alignment = Alignment.Center,
    alpha: Float = 1f,
    colorFilter: ColorFilter? = null
): AsyncImagePainter {
    // 创建一个 AsyncImagePainter 对象
    return remember(model, imageLoader, placeholder, error, fallback, onState, contentScale, alignment, alpha, colorFilter) {
        AsyncImagePainter(
            model = model,
            imageLoader = imageLoader,
            placeholder = placeholder,
            error = error,
            fallback = fallback,
            onState = onState,
            contentScale = contentScale,
            alignment = alignment,
            alpha = alpha,
            colorFilter = colorFilter
        )
    }
}

在这个函数中,使用 remember 来记忆 AsyncImagePainter 对象,确保在重组时不会重新创建。AsyncImagePainter 类负责从网络加载图像,并处理加载状态的变化。

3.2.2 AsyncImagePainter 源码分析

AsyncImagePainter 类的部分源码如下:

kotlin

class AsyncImagePainter(
    private val model: Any?,
    private val imageLoader: ImageLoader,
    private val placeholder: Painter? = null,
    private val error: Painter? = null,
    private val fallback: Painter? = null,
    private val onState: ((State) -> Unit)? = null,
    private val contentScale: ContentScale = ContentScale.Fit,
    private val alignment: Alignment = Alignment.Center,
    private val alpha: Float = 1f,
    private val colorFilter: ColorFilter? = null
) : Painter() {

    private var state: State = State.Loading
    private var result: ImageResult? = null

    init {
        // 启动图像加载任务
        launchImageLoad()
    }

    private fun launchImageLoad() {
        val request = ImageRequest.Builder(imageLoader.context)
           .data(model)
           .target(
                onStart = {
                    // 开始加载时,更新状态为 Loading
                    state = State.Loading
                    onState?.invoke(state)
                },
                onSuccess = { drawable ->
                    // 加载成功时,更新状态为 Success
                    result = drawable
                    state = State.Success(drawable)
                    onState?.invoke(state)
                },
                onError = { throwable ->
                    // 加载失败时,更新状态为 Error
                    state = State.Error(throwable)
                    onState?.invoke(state)
                }
            )
           .build()
        // 使用 ImageLoader 加载图像
        imageLoader.enqueue(request)
    }

    override fun DrawScope.onDraw() {
        when (state) {
            is State.Loading -> {
                // 加载中,绘制占位符
                placeholder?.draw(this, alpha = alpha, colorFilter = colorFilter)
            }
            is State.Success -> {
                // 加载成功,绘制图像
                val drawable = (state as State.Success).drawable
                drawable.draw(this, contentScale, alignment, alpha, colorFilter)
            }
            is State.Error -> {
                // 加载失败,绘制错误图像
                error?.draw(this, alpha = alpha, colorFilter = colorFilter)
            }
        }
    }
}

AsyncImagePainter 类中,launchImageLoad 方法用于启动图像加载任务,使用 ImageLoader 来加载图像,并根据加载状态更新 state 变量。onDraw 方法根据不同的状态绘制不同的图像,如占位符、成功加载的图像或错误图像。

四、图像的裁剪与缩放

4.1 图像裁剪

在 Android Compose 中,可以使用 Modifier.clip 方法对图像进行裁剪。以下是一个示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun CroppedImage() {
    // 使用 painterResource 函数从资源中获取图像的 Painter
    val painter = painterResource(id = R.drawable.sample_image)
    // 使用 Image 组件显示图像,并进行裁剪
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "裁剪后的图像", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
           .clip(CircleShape) // 使用圆形形状进行裁剪
    )
}

@Preview
@Composable
fun CroppedImagePreview() {
    CroppedImage()
}

在上述代码中,使用 Modifier.clip 方法结合 CircleShape 对图像进行圆形裁剪。

4.1.1 Modifier.clip 源码分析

Modifier.clip 方法的源码如下:

kotlin

fun Modifier.clip(shape: Shape): Modifier = this.then(ClipShapeModifier(shape, ClipOp.Intersect))

在这个方法中,使用 then 方法将 ClipShapeModifier 修饰符添加到当前修饰符链中。ClipShapeModifier 类的源码如下:

kotlin

private class ClipShapeModifier(
    private val shape: Shape,
    private val clipOp: ClipOp
) : DrawModifier {
    override fun DrawScope.draw() {
        // 创建一个 Path 对象,用于定义裁剪路径
        val path = shape.createOutline(size, layoutDirection, this).asPath()
        // 保存当前画布状态
        val saveCount = drawContext.canvas.save()
        try {
            // 设置裁剪操作
            drawContext.canvas.clipPath(path, clipOp)
            // 绘制内容
            drawContent()
        } finally {
            // 恢复画布状态
            drawContext.canvas.restoreToCount(saveCount)
        }
    }
}

ClipShapeModifier 类的 draw 方法中,首先根据 Shape 创建一个 Path 对象,然后使用 canvas.clipPath 方法对画布进行裁剪,最后绘制内容。

4.2 图像缩放

在 Android Compose 中,可以使用 ContentScale 参数对图像进行缩放。ContentScale 是一个枚举类,定义了不同的缩放方式,如 FitCropFillBounds 等。以下是一个示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun ScaledImage() {
    // 使用 painterResource 函数从资源中获取图像的 Painter
    val painter = painterResource(id = R.drawable.sample_image)
    // 使用 Image 组件显示图像,并进行缩放
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "缩放后的图像", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
        contentScale = ContentScale.Crop // 设置内容缩放方式为裁剪
    )
}

@Preview
@Composable
fun ScaledImagePreview() {
    ScaledImage()
}

在上述代码中,使用 ContentScale.Crop 对图像进行裁剪缩放。

4.2.1 ContentScale 源码分析

ContentScale 枚举类的源码如下:

kotlin

enum class ContentScale {
    /**
     * 缩放图像以适应容器,保持图像的宽高比,可能会在容器内留下空白。
     */
    Fit,

    /**
     * 缩放图像以填充容器,保持图像的宽高比,可能会裁剪图像。
     */
    Crop,

    /**
     * 缩放图像以填充容器,不保持图像的宽高比。
     */
    FillBounds,

    /**
     * 不进行缩放,直接显示图像。
     */
    None;

    internal fun computeScaleFactor(
        sourceWidth: Float,
        sourceHeight: Float,
        destWidth: Float,
        destHeight: Float
    ): Float {
        return when (this) {
            Fit -> minOf(destWidth / sourceWidth, destHeight / sourceHeight)
            Crop -> maxOf(destWidth / sourceWidth, destHeight / sourceHeight)
            FillBounds -> 1f
            None -> 1f
        }
    }
}

ContentScale 枚举类中,定义了不同的缩放方式,并提供了 computeScaleFactor 方法用于计算缩放因子。在 Image 组件的 ImagePainter 类中,会根据 ContentScale 计算缩放因子,并对图像进行相应的缩放。

五、矢量图的使用与绘制

5.1 使用 ImageVector 显示矢量图

在 Android Compose 中,可以使用 ImageVector 来显示矢量图。ImageVector 是一个表示矢量图的抽象类,可以通过 imageVectorResource 函数从资源中加载矢量图。以下是一个示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.imageVectorResource
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun VectorImage() {
    // 使用 imageVectorResource 函数从资源中获取矢量图的 ImageVector
    val imageVector: ImageVector = imageVectorResource(id = R.drawable.sample_vector)
    // 使用 Image 组件显示矢量图
    Image(
        imageVector = imageVector, // 设置矢量图的 ImageVector
        contentDescription = "矢量图", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
    )
}

@Preview
@Composable
fun VectorImagePreview() {
    VectorImage()
}

在上述代码中,imageVectorResource 函数用于从资源中获取矢量图的 ImageVector 对象,Image 组件接收 ImageVector 对象并显示矢量图。

5.1.1 imageVectorResource 源码分析

imageVectorResource 函数的源码如下:

kotlin

@Composable
fun imageVectorResource(
    id: Int
): ImageVector {
    // 获取当前的资源环境
    val resources = LocalContext.current.resources
    // 使用 VectorResourceLoader 加载矢量图资源
    return remember { VectorResourceLoader(resources).load(id) }
}

在这个函数中,首先通过 LocalContext.current.resources 获取当前的资源环境,然后使用 VectorResourceLoader 类的 load 方法加载指定 ID 的矢量图资源。VectorResourceLoader 类的 load 方法源码如下:

kotlin

class VectorResourceLoader(private val resources: Resources) {
    fun load(id: Int): ImageVector {
        // 根据资源 ID 获取矢量图的 XML 文件
        val xml = resources.getXml(id)
        try {
            // 使用 VectorDrawableCompat 解析 XML 文件
            val vectorDrawable = VectorDrawableCompat.createFromXmlInner(resources, xml, null)
            if (vectorDrawable != null) {
                // 将 VectorDrawableCompat 转换为 ImageVector
                return vectorDrawable.toImageVector()
            }
        } catch (e: Exception) {
            throw IllegalArgumentException("Failed to load vector resource: $id", e)
        }
        throw IllegalArgumentException("Failed to load vector resource: $id")
    }
}

load 方法中,首先根据资源 ID 获取矢量图的 XML 文件,然后使用 VectorDrawableCompat 解析 XML 文件,最后将 VectorDrawableCompat 转换为 ImageVector

5.1.2 Image 组件显示 ImageVector 的源码分析

Image 组件接收 ImageVector 对象时,会使用 VectorPainter 来绘制矢量图。Image 组件的部分源码如下:

kotlin

@Composable
fun Image(
    imageVector: ImageVector,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = 1f,
    colorFilter: ColorFilter? = null
) {
    // 创建一个 VectorPainter 对象,用于绘制矢量图
    val painter = remember(imageVector, alignment, contentScale, alpha, colorFilter) {
        VectorPainter(imageVector, alignment, contentScale, alpha, colorFilter)
    }
    // 使用 Canvas 组件进行绘制
    Canvas(modifier) {
        painter.draw(this)
    }
}

在这个方法中,创建一个 VectorPainter 对象,该对象封装了矢量图的绘制逻辑,然后使用 Canvas 组件调用 painterdraw 方法将矢量图绘制到画布上。

5.2 自定义矢量图绘制

除了从资源中加载矢量图,还可以自定义矢量图的绘制。在 Android Compose 中,可以使用 PathDrawScope 来绘制自定义的矢量图形。以下是一个示例代码:

kotlin

import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun CustomVectorDrawing() {
    Canvas(
        modifier = Modifier
           .width(200.dp) // 设置画布的宽度
           .height(200.dp) // 设置画布的高度
    ) {
        // 创建一个 Path 对象,用于定义矢量图形的路径
        val path = Path()
        // 移动到起始点
        path.moveTo(100f, 20f)
        // 绘制线条到指定点
        path.lineTo(180f, 180f)
        path.lineTo(20f, 180f)
        // 闭合路径
        path.close()
        // 设置画笔颜色
        drawPath(
            path = path,
            color = Color.Blue
        )
    }
}

@Preview
@Composable
fun CustomVectorDrawingPreview() {
    CustomVectorDrawing()
}

在上述代码中,使用 Canvas 组件创建一个画布,然后使用 Path 对象定义矢量图形的路径,最后使用 drawPath 方法将路径绘制到画布上。

5.2.1 Path 类源码分析

Path 类是用于定义矢量图形路径的类,它提供了一系列方法来创建和操作路径,如 moveTolineToarcTo 等。以下是 Path 类的部分源码:

kotlin

class Path {
    private val nativePath = android.graphics.Path()

    /**
     * 将路径的当前点移动到指定的坐标。
     */
    fun moveTo(x: Float, y: Float) {
        nativePath.moveTo(x, y)
    }

    /**
     * 从当前点绘制一条直线到指定的坐标。
     */
    fun lineTo(x: Float, y: Float) {
        nativePath.lineTo(x, y)
    }

    /**
     * 闭合路径。
     */
    fun close() {
        nativePath.close()
    }

    // 其他方法...
}

Path 类中,内部使用 android.graphics.Path 来实现路径的创建和操作。

5.2.2 DrawScope 中绘制路径的源码分析

Canvas 组件的 DrawScope 中,drawPath 方法用于绘制路径。DrawScope 是一个接口,定义了一系列绘制方法。drawPath 方法的源码如下:

kotlin

fun DrawScope.drawPath(
    path: Path,
    color: Color,
    alpha: Float = 1f,
    style: PaintingStyle = PaintingStyle.Fill,
    strokeWidth: Float = 0f,
    strokeCap: StrokeCap = StrokeCap.Butt,
    strokeJoin: StrokeJoin = StrokeJoin.Miter,
    strokeMiter: Float = 4f,
    colorFilter: ColorFilter? = null
) {
    // 创建一个 Paint 对象,用于设置绘制样式
    val paint = Paint().apply {
        this.color = color
        this.alpha = alpha
        this.style = style
        this.strokeWidth = strokeWidth
        this.strokeCap = strokeCap
        this.strokeJoin = strokeJoin
        this.strokeMiter = strokeMiter
        this.colorFilter = colorFilter
    }
    // 使用 Canvas 绘制路径
    drawContext.canvas.drawPath(path.nativePath, paint.asFrameworkPaint())
}

drawPath 方法中,首先创建一个 Paint 对象,用于设置绘制样式,然后使用 CanvasdrawPath 方法将路径绘制到画布上。

六、图像与矢量图的动画效果

6.1 图像淡入淡出动画

在 Android Compose 中,可以使用 AnimatedVisibility 组件实现图像的淡入淡出动画。以下是一个示例代码:

kotlin

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun FadeInOutImage() {
    // 定义一个可变状态,用于控制图像的可见性
    var isVisible by mutableStateOf(true)
    // 使用 AnimatedVisibility 组件实现淡入淡出动画
    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn(), // 淡入动画
        exit = fadeOut()  // 淡出动画
    ) {
        // 使用 Image 组件显示图像
        Image(
            painter = painterResource(id = R.drawable.sample_image), // 设置图像的 Painter
            contentDescription = "淡入淡出图像", // 设置图像的描述,用于无障碍访问
            modifier = Modifier
               .width(200.dp) // 设置图像的宽度
               .height(200.dp) // 设置图像的高度
        )
    }
    // 切换图像的可见性
    if (isVisible) {
        isVisible = false
    } else {
        isVisible = true
    }
}

@Preview
@Composable
fun FadeInOutImagePreview() {
    FadeInOutImage()
}

在上述代码中,使用 AnimatedVisibility 组件包裹 Image 组件,并设置 enterexit 动画为 fadeIn()fadeOut(),实现图像的淡入淡出动画。

6.1.1 AnimatedVisibility 源码分析

AnimatedVisibility 组件的源码如下:

kotlin

@Composable
@ExperimentalAnimationApi
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandVertically(),
    exit: ExitTransition = shrinkVertically() + fadeOut(),
    content: @Composable () -> Unit
) {
    // 创建一个 Transition 对象,用于管理动画状态
    val transition = updateTransition(visible, label = "AnimatedVisibility")
    // 根据可见性状态执行相应的动画
    transition.AnimatedVisibilityScope(
        modifier = modifier,
        enter = enter,
        exit = exit,
        content = content
    )
}

AnimatedVisibility 组件中,使用 updateTransition 函数创建一个 Transition 对象,用于管理动画状态。然后调用 Transition 对象的 AnimatedVisibilityScope 方法,根据可见性状态执行相应的动画。

6.1.2 fadeInfadeOut 源码分析

fadeInfadeOut 函数是用于创建淡入和淡出动画的函数,它们的源码如下:

kotlin

@Composable
@ExperimentalAnimationApi
fun fadeIn(
    animationSpec: FiniteAnimationSpec<Float> = tween(durationMillis = 300),
    initialAlpha: Float = 0f
): EnterTransition {
    return FadeTransition(
        alpha = transition(
            to = { 1f },
            from = { initialAlpha },
            animationSpec = animationSpec
        )
    )
}

@Composable
@ExperimentalAnimationApi
fun fadeOut(
    animationSpec: FiniteAnimationSpec<Float> = tween(durationMillis = 300),
    targetAlpha: Float = 0f
): ExitTransition {
    return FadeTransition(
        alpha = transition(
            to = { targetAlpha },
            from = { 1f },
            animationSpec = animationSpec
        )
    )
}

fadeInfadeOut 函数中,使用 transition 函数创建一个动画过渡,控制透明度的变化,然后创建一个 FadeTransition 对象,用于实现淡入淡出效果。

6.2 矢量图的缩放动画

可以使用 animateFloatAsState 函数实现矢量图的缩放动画。以下是一个示例代码:

kotlin

import androidx.compose.animation.animateFloatAsState
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.imageVectorResource
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun VectorScaleAnimation() {
    // 使用 imageVectorResource 函数从资源中获取矢量图的 ImageVector
    val imageVector: ImageVector = imageVectorResource(id = R.drawable.sample_vector)
    // 定义一个可变状态,用于控制缩放比例
    var scale by androidx.compose.runtime.mutableStateOf(1f)
    // 使用 animateFloatAsState 函数实现缩放动画
    val animatedScale: Float by animateFloatAsState(
        targetValue = scale,
        animationSpec = androidx.compose.animation.tween(durationMillis = 1000)
    )
    // 使用 Image 组件显示矢量图,并应用缩放动画
    Image(
        imageVector = imageVector, // 设置矢量图的 ImageVector
        contentDescription = "矢量图缩放动画", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
           .scale(animatedScale) // 应用缩放动画
    )
    // 切换缩放比例
    if (scale == 1f) {
        scale = 2f
    } else {
        scale = 1f
    }
}

@Preview
@Composable
fun VectorScaleAnimationPreview() {
    VectorScaleAnimation()
}

在上述代码中,使用 animateFloatAsState 函数实现缩放动画,通过改变 scale 值来触发动画,然后使用 Modifier.scale 方法应用缩放效果。

6.2.1 animateFloatAsState 源码分析

animateFloatAsState 函数的源码如下:

kotlin

@Composable
fun animateFloatAsState(
    targetValue: Float,
    animationSpec: AnimationSpec<Float> = spring(),
    finishedListener: ((Float) -> Unit)? = null
): State<Float> {
    // 创建一个 AnimationState 对象,用于管理动画状态
    val animationState = remember {
        AnimationState(
            initialValue = targetValue,
            animationSpec = animationSpec
        )
    }
    // 更新动画目标值
    LaunchedEffect(targetValue) {
        animationState.animateTo(targetValue, finishedListener)
    }
    // 返回动画状态
    return animationState.asState()
}

animateFloatAsState 函数中,首先创建一个 AnimationState 对象,用于管理动画状态。然后使用 LaunchedEffect 函数在 targetValue 变化时更新动画目标值,最后返回动画状态。

6.2.2 Modifier.scale 源码分析

Modifier.scale 方法的源码如下:

kotlin

fun Modifier.scale(scale: Float): Modifier = this.then(ScaleModifier(scale))

在这个方法中,使用 then 方法将 ScaleModifier 修饰符添加到当前修饰符链中。ScaleModifier 类的源码如下:

kotlin

private class ScaleModifier(private val scale: Float) : DrawModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // 计算缩放后的尺寸
        val placeable = measurable.measure(constraints)
        val width = (placeable.width * scale).roundToInt()
        val height = (placeable.height * scale).roundToInt()
        return layout(width, height) {
            // 放置可测量对象
            placeable.placeRelative(0, 0, scaleX = scale, scaleY = scale)
        }
    }

    override fun DrawScope.draw() {
        // 绘制内容
        drawContent()
    }
}

ScaleModifier 类的 measure 方法中,计算缩放后的尺寸,并使用 placeRelative 方法放置可测量对象,应用缩放效果。

七、图像与矢量图的性能优化

7.1 图像缓存

在从网络加载图像时,使用缓存可以减少重复加载,提高性能。Coil 库提供了内置的缓存机制。以下是一个使用 Coil 缓存的示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.size.Size

@Composable
fun CachedNetworkImage() {
    // 创建一个 ImageLoader,设置缓存
    val imageLoader = coil.ImageLoader.Builder(LocalContext.current)
       .memoryCache {
            // 设置内存缓存大小
            coil.memory.MemoryCache.Builder(LocalContext.current)
               .maxSizePercent(0.25)
               .build()
        }
       .diskCache {
            // 设置磁盘缓存
            coil.disk.DiskCache.Builder()
               .directory(LocalContext.current.cacheDir.resolve("image_cache"))
               .maxSizeBytes(1024 * 1024 * 50) // 50MB
               .build()
        }
       .build()
    // 使用 rememberAsyncImagePainter 函数从网络加载图像,并使用 ImageLoader
    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(Local

kotlin

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.size.Size

@Composable
fun CachedNetworkImage() {
    // 创建一个 ImageLoader,设置缓存
    val imageLoader = coil.ImageLoader.Builder(LocalContext.current)
       .memoryCache {
            // 设置内存缓存大小
            coil.memory.MemoryCache.Builder(LocalContext.current)
               .maxSizePercent(0.25)
               .build()
        }
       .diskCache {
            // 设置磁盘缓存
            coil.disk.DiskCache.Builder()
               .directory(LocalContext.current.cacheDir.resolve("image_cache"))
               .maxSizeBytes(1024 * 1024 * 50) // 50MB
               .build()
        }
       .build()
    // 使用 rememberAsyncImagePainter 函数从网络加载图像,并使用 ImageLoader
    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(LocalContext.current)
           .data("https://example.com/image.jpg") // 设置图像的 URL
           .size(Size.ORIGINAL) // 使用原始尺寸
           .crossfade(true) // 启用淡入淡出效果
           .build(),
        imageLoader = imageLoader // 使用自定义的 ImageLoader
    )
    // 使用 Image 组件显示图像
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "缓存网络图像", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
        contentScale = ContentScale.Crop // 设置内容缩放方式
    )
}

@Preview
@Composable
fun CachedNetworkImagePreview() {
    CachedNetworkImage()
}
7.1.1 ImageLoader 缓存机制源码分析

ImageLoader 是 Coil 库中负责图像加载和缓存管理的核心类。以下是 ImageLoader.Builder 中设置缓存的相关源码分析:

kotlin

// ImageLoader.Builder 中设置内存缓存的部分
fun memoryCache(block: () -> MemoryCache): Builder {
    this.memoryCache = block()
    return this
}

// MemoryCache.Builder 类的部分源码
class MemoryCache.Builder(
    private val context: Context
) {
    private var weakValues = false
    private var maxSizePercent = 0.25f

    fun weakValues(weakValues: Boolean): Builder {
        this.weakValues = weakValues
        return this
    }

    fun maxSizePercent(maxSizePercent: Float): Builder {
        require(maxSizePercent in 0f..1f) { "maxSizePercent must be between 0 and 1." }
        this.maxSizePercent = maxSizePercent
        return this
    }

    fun build(): MemoryCache {
        val maxSize = (Runtime.getRuntime().maxMemory() * maxSizePercent).toInt()
        return RealMemoryCache(context, maxSize, weakValues)
    }
}

ImageLoader.Builder 中,memoryCache 方法接收一个返回 MemoryCache 对象的 lambda 表达式。MemoryCache.Builder 类用于构建内存缓存,maxSizePercent 方法设置内存缓存占总可用内存的百分比,build 方法根据设置的参数创建 RealMemoryCache 对象。

kotlin

// ImageLoader.Builder 中设置磁盘缓存的部分
fun diskCache(block: () -> DiskCache): Builder {
    this.diskCache = block()
    return this
}

// DiskCache.Builder 类的部分源码
class DiskCache.Builder {
    private var directory: File? = null
    private var maxSizeBytes = 250L * 1024 * 1024 // 250MB by default
    private var strictMode = false

    fun directory(directory: File): Builder {
        this.directory = directory
        return this
    }

    fun maxSizeBytes(maxSizeBytes: Long): Builder {
        require(maxSizeBytes > 0) { "maxSizeBytes must be > 0." }
        this.maxSizeBytes = maxSizeBytes
        return this
    }

    fun strictMode(strictMode: Boolean): Builder {
        this.strictMode = strictMode
        return this
    }

    fun build(): DiskCache {
        checkNotNull(directory) { "directory == null" }
        return RealDiskCache(directory!!, maxSizeBytes, strictMode)
    }
}

ImageLoader.Builder 中,diskCache 方法接收一个返回 DiskCache 对象的 lambda 表达式。DiskCache.Builder 类用于构建磁盘缓存,directory 方法设置磁盘缓存的目录,maxSizeBytes 方法设置磁盘缓存的最大大小,build 方法根据设置的参数创建 RealDiskCache 对象。

7.1.2 rememberAsyncImagePainter 与缓存的交互

rememberAsyncImagePainter 会使用传入的 ImageLoader 进行图像加载。当请求图像时,ImageLoader 会先检查内存缓存,如果内存缓存中有对应的图像,则直接返回;如果内存缓存中没有,则检查磁盘缓存;如果磁盘缓存中也没有,则从网络加载图像,并将图像存入内存缓存和磁盘缓存中。

kotlin

// AsyncImagePainter 中与 ImageLoader 交互的部分源码
private fun launchImageLoad() {
    val request = ImageRequest.Builder(imageLoader.context)
       .data(model)
       .target(
            onStart = {
                // 开始加载时,更新状态为 Loading
                state = State.Loading
                onState?.invoke(state)
            },
            onSuccess = { drawable ->
                // 加载成功时,更新状态为 Success
                result = drawable
                state = State.Success(drawable)
                onState?.invoke(state)
            },
            onError = { throwable ->
                // 加载失败时,更新状态为 Error
                state = State.Error(throwable)
                onState?.invoke(state)
            }
        )
       .build()
    // 使用 ImageLoader 加载图像
    imageLoader.enqueue(request)
}

launchImageLoad 方法中,ImageLoaderenqueue 方法会处理图像加载请求,在加载过程中会进行缓存检查和更新操作。

7.2 矢量图优化

7.2.1 减少矢量图复杂度

复杂的矢量图会增加绘制的时间和内存消耗。可以通过以下方式减少矢量图的复杂度:

  • 简化路径:避免使用过多的曲线和控制点,尽量使用简单的几何形状组合。例如,将复杂的图标分解为多个简单的形状。
  • 减少图层:避免使用过多的图层,将不必要的图层合并或删除。
7.2.2 矢量图压缩

在 Android 中,可以使用 Vector Asset Studio 对矢量图进行压缩。Vector Asset Studio 会自动优化矢量图的 XML 文件,去除不必要的属性和标签,减小文件大小。

7.2.3 按需加载矢量图

在应用中,不是所有的矢量图都需要在应用启动时加载。可以根据实际需求,在需要显示矢量图时再进行加载。例如,在 RecyclerView 中,只有当某个 item 可见时,才加载该 item 所需的矢量图。

kotlin

import androidx.compose.foundation.Image
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.imageVectorResource
import androidx.compose.ui.tooling.preview.Preview

data class VectorItem(val id: Int, val description: String)

@Composable
fun LazyVectorList() {
    val vectorItems = listOf(
        VectorItem(R.drawable.sample_vector_1, "矢量图 1"),
        VectorItem(R.drawable.sample_vector_2, "矢量图 2"),
        VectorItem(R.drawable.sample_vector_3, "矢量图 3")
    )
    LazyColumn {
        items(vectorItems) { item ->
            Image(
                imageVector = imageVectorResource(id = item.id), // 设置矢量图的 ImageVector
                contentDescription = item.description, // 设置图像的描述,用于无障碍访问
                modifier = Modifier
                   .width(200.dp) // 设置图像的宽度
                   .height(200.dp) // 设置图像的高度
            )
        }
    }
}

@Preview
@Composable
fun LazyVectorListPreview() {
    LazyVectorList()
}

在上述代码中,使用 LazyColumn 来显示矢量图列表,LazyColumn 会根据列表项的可见性进行按需加载,提高性能。

7.3 图像与矢量图的内存管理

7.3.1 及时释放资源

在 Android 中,图像和矢量图占用的内存需要及时释放,避免内存泄漏。对于图像,可以在 Image 组件不再使用时,调用 ImageLoadershutdown 方法来释放相关资源。对于矢量图,虽然 ImageVector 本身占用的内存相对较小,但在不再使用时,也应该避免持有引用。

kotlin

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import coil.ImageLoader

@Composable
fun ImageWithResourceRelease() {
    val imageLoader = ImageLoader(LocalContext.current)
    // 使用 DisposableEffect 在组件销毁时释放资源
    DisposableEffect(Unit) {
        onDispose {
            imageLoader.shutdown()
        }
    }
    // 其他图像加载和显示代码...
}

在上述代码中,使用 DisposableEffect 在组件销毁时调用 ImageLoadershutdown 方法来释放资源。

7.3.2 控制图像尺寸

在加载图像时,根据显示的需求控制图像的尺寸,避免加载过大的图像导致内存占用过高。可以使用 ImageRequestsize 方法来设置加载图像的尺寸。

kotlin

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.size.Size

@Composable
fun SizedNetworkImage() {
    // 使用 rememberAsyncImagePainter 函数从网络加载图像,并设置尺寸
    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(LocalContext.current)
           .data("https://example.com/image.jpg") // 设置图像的 URL
           .size(Size(200, 200)) // 设置加载图像的尺寸
           .crossfade(true) // 启用淡入淡出效果
           .build()
    )
    // 使用 Image 组件显示图像
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "指定尺寸的网络图像", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
        contentScale = ContentScale.Crop // 设置内容缩放方式
    )
}

@Preview
@Composable
fun SizedNetworkImagePreview() {
    SizedNetworkImage()
}

在上述代码中,使用 ImageRequestsize 方法设置加载图像的尺寸为 200x200 像素,减少内存占用。

八、图像与矢量图的交互处理

8.1 图像点击事件

在 Android Compose 中,可以为 Image 组件添加点击事件。以下是一个示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun ClickableImage() {
    // 使用 painterResource 函数从资源中获取图像的 Painter
    val painter = painterResource(id = R.drawable.sample_image)
    // 使用 Image 组件显示图像,并添加点击事件
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "可点击的图像", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
           .clickable {
                // 点击事件处理逻辑
                println("图像被点击了!")
            }
    )
}

@Preview
@Composable
fun ClickableImagePreview() {
    ClickableImage()
}
8.1.1 Modifier.clickable 源码分析

Modifier.clickable 方法的源码如下:

kotlin

fun Modifier.clickable(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onClick: () -> Unit
): Modifier = this.then(
    ClickableModifier(
        enabled = enabled,
        onClickLabel = onClickLabel,
        role = role,
        onClick = onClick
    )
)

在这个方法中,使用 then 方法将 ClickableModifier 修饰符添加到当前修饰符链中。ClickableModifier 类的源码如下:

kotlin

private class ClickableModifier(
    private val enabled: Boolean,
    private val onClickLabel: String?,
    private val role: Role?,
    private val onClick: () -> Unit
) : PointerInputModifier {
    override fun PointerInputScope.modifyPointerInput(
        other: PointerInputChangeScope.() -> Unit
    ): PointerInputChangeScope.() -> Unit = {
        other()
        detectTapGestures(
            enabled = enabled,
            onTap = onClick
        )
    }
}

ClickableModifier 类中,实现了 PointerInputModifier 接口,使用 detectTapGestures 函数来检测点击手势,当检测到点击事件时,调用传入的 onClick 方法。

8.2 矢量图的交互动画

可以为矢量图添加交互动画,例如点击矢量图时进行缩放或旋转。以下是一个点击矢量图进行缩放的示例代码:

kotlin

import androidx.compose.animation.animateFloatAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.imageVectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun InteractiveVectorImage() {
    // 使用 imageVectorResource 函数从资源中获取矢量图的 ImageVector
    val imageVector: ImageVector = imageVectorResource(id = R.drawable.sample_vector)
    // 定义一个可变状态,用于控制缩放比例
    var isClicked by mutableStateOf(false)
    // 使用 animateFloatAsState 函数实现缩放动画
    val scale: Float by animateFloatAsState(
        targetValue = if (isClicked) 2f else 1f,
        animationSpec = androidx.compose.animation.tween(durationMillis = 300)
    )
    // 使用 Image 组件显示矢量图,并添加点击事件和缩放动画
    Image(
        imageVector = imageVector, // 设置矢量图的 ImageVector
        contentDescription = "可交互的矢量图", // 设置图像的描述,用于无障碍访问
        modifier = Modifier
           .width(200.dp) // 设置图像的宽度
           .height(200.dp) // 设置图像的高度
           .scale(scale) // 应用缩放动画
           .clickable {
                // 点击事件处理逻辑,切换点击状态
                isClicked = !isClicked
            }
    )
}

@Preview
@Composable
fun InteractiveVectorImagePreview() {
    InteractiveVectorImage()
}
8.2.2 交互动画的实现原理

在上述代码中,使用 mutableStateOf 来管理点击状态 isClicked,当点击矢量图时,切换 isClicked 的值。animateFloatAsState 函数会根据 isClicked 的值来改变缩放比例 scale,并使用 tween 动画规范来实现动画效果。Modifier.scale 方法会应用这个缩放比例,从而实现矢量图的缩放动画。

8.3 图像与矢量图的手势交互

除了点击事件,还可以为图像和矢量图添加其他手势交互,如缩放、平移和旋转。可以使用 Modifier.pointerInput 来实现这些手势交互。以下是一个实现图像缩放和平移的示例代码:

kotlin

import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun GestureInteractiveImage() {
    // 使用 painterResource 函数从资源中获取图像的 Painter
    val painter = painterResource(id = R.drawable.sample_image)
    // 定义可变状态,用于控制缩放比例和偏移量
    var scale by mutableStateOf(1f)
    var offsetX by mutableStateOf(0f)
    var offsetY by mutableStateOf(0f)
    // 使用 Modifier.pointerInput 实现手势交互
    val modifier = Modifier
       .width(200.dp) // 设置图像的宽度
       .height(200.dp) // 设置图像的高度
       .graphicsLayer(
            scaleX = scale,
            scaleY = scale,
            translationX = offsetX,
            translationY = offsetY
        )
       .pointerInput(Unit) {
            detectTransformGestures(
                onGesture = { centroid, pan, zoom, rotation ->
                    // 更新缩放比例和偏移量
                    scale = (scale * zoom).coerceIn(0.1f, 5f)
                    offsetX += pan.x
                    offsetY += pan.y
                }
            )
        }
    // 使用 Image 组件显示图像,并应用手势交互
    Image(
        painter = painter, // 设置图像的 Painter
        contentDescription = "手势交互的图像", // 设置图像的描述,用于无障碍访问
        modifier = modifier
    )
}

@Preview
@Composable
fun GestureInteractiveImagePreview() {
    GestureInteractiveImage()
}
8.3.1 Modifier.pointerInput 源码分析

Modifier.pointerInput 方法的源码如下:

kotlin

fun Modifier.pointerInput(
    key1: Any?,
    block: suspend PointerInputScope.() -> Unit
): Modifier = this.then(
    PointerInputModifier(
        key1 = key1,
        block = block
    )
)

在这个方法中,使用 then 方法将 PointerInputModifier 修饰符添加到当前修饰符链中。PointerInputModifier 类的源码如下:

kotlin

private class PointerInputModifier(
    private val key1: Any?,
    private val block: suspend PointerInputScope.() -> Unit
) : PointerInputModifier {
    override fun PointerInputScope.modifyPointerInput(
        other: PointerInputChangeScope.() -> Unit
    ): PointerInputChangeScope.() -> Unit = {
        other()
        launch {
            block()
        }
    }
}

PointerInputModifier 类中,实现了 PointerInputModifier 接口,使用 launch 函数启动一个协程来执行传入的 block 函数,该函数用于处理手势事件。

8.3.2 detectTransformGestures 源码分析

detectTransformGestures 函数用于检测缩放、平移和旋转手势。其源码如下:

kotlin

suspend fun PointerInputScope.detectTransformGestures(
    panZoomLock: Boolean = false,
    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
) {
    forEachGesture {
        awaitPointerEventScope {
            var zoom = 1f
            var rotation = 0f
            var pan = Offset.Zero
            var pastTouchSlop = false
            val touchSlop = viewConfiguration.touchSlop
            awaitFirstDown(requireUnconsumed = false)
            do {
                val event = awaitPointerEvent()
                val canceled = event.changes.any { it.isConsumed }
                if (!canceled) {
                    val zoomChange = event.calculateZoom()
                    val rotationChange = event.calculateRotation()
                    val panChange = event.calculatePan()
                    if (!pastTouchSlop) {
                        zoom *= zoomChange
                        rotation += rotationChange
                        pan += panChange
                        val centroidSize = event.calculateCentroidSize(useCurrent = false)
                        val zoomMotion = abs(1 - zoom) * centroidSize
                        val rotationMotion = abs(rotation * PI * centroidSize / 180f)
                        val panMotion = pan.getDistance()
                        if (zoomMotion > touchSlop ||
                            rotationMotion > touchSlop ||
                            panMotion > touchSlop
                        ) {
                            pastTouchSlop = true
                        }
                    } else {
                        onGesture(
                            event.calculateCentroid(useCurrent = false),
                            panChange,
                            zoomChange,
                            rotationChange
                        )
                    }
                }
            } while (!canceled && event.changes.any { it.pressed })
        }
    }
}

detectTransformGestures 函数中,使用 forEachGesture 函数处理每个手势事件,使用 awaitPointerEventScope 函数等待指针事件。在事件处理过程中,计算缩放、平移和旋转的变化量,并根据触摸阈值判断是否开始处理手势,当满足条件时,调用 onGesture 方法传递手势信息。

九、图像与矢量图的适配与兼容性

9.1 不同屏幕密度的适配

在 Android 开发中,不同的设备具有不同的屏幕密度,为了确保图像和矢量图在各种设备上都能正常显示,需要进行屏幕密度的适配。

9.1.1 图像资源适配

对于图像资源,可以使用不同密度的文件夹来存放不同分辨率的图像。例如,drawable-mdpi 文件夹存放中等密度屏幕的图像,drawable-hdpi 文件夹存放高密度屏幕的图像,drawable-xhdpi 文件夹存放超高密度屏幕的图像等。Android 系统会根据设备的屏幕密度自动选择合适的图像资源。

9.1.2 矢量图的优势

矢量图在不同屏幕密度下具有天然的适配优势,因为矢量图是使用数学公式描述的,不会因为缩放而失真。在 Android Compose 中,使用 ImageVector 显示矢量图时,不需要为不同屏幕密度提供不同的资源。

9.2 不同 Android 版本的兼容性

9.2.1 图像加载库的兼容性

在使用第三方图像加载库(如 Coil 或 Glide)时,需要确保库的版本与目标 Android 版本兼容。大多数现代的图像加载库都支持较新的 Android 版本,但在使用时仍需注意检查文档。

9.2.2 矢量图的兼容性

Android 支持从 API 级别 21 开始使用矢量图。如果需要支持更低的 API 级别,可以使用 VectorDrawableCompat 来兼容旧版本的 Android 系统。在 Android Compose 中,imageVectorResource 函数内部会使用 VectorDrawableCompat 来加载矢量图,确保在不同 Android 版本上的兼容性。

kotlin

// VectorResourceLoader 中使用 VectorDrawableCompat 的部分源码
class VectorResourceLoader(private val resources: Resources) {
    fun load(id: Int): ImageVector {
        // 根据资源 ID 获取矢量图的 XML 文件
        val xml = resources.getXml(id)
        try {
            // 使用 VectorDrawableCompat 解析 XML 文件
            val vectorDrawable = VectorDrawableCompat.createFromXmlInner(resources, xml, null)
            if (vectorDrawable != null) {
                // 将 VectorDrawableCompat 转换为 ImageVector
                return vectorDrawable.toImageVector()
            }
        } catch (e: Exception) {
            throw IllegalArgumentException("Failed to load vector resource: $id", e)
        }
        throw IllegalArgumentException("Failed to load vector resource: $id")
    }
}

在上述代码中,使用 VectorDrawableCompat.createFromXmlInner 方法解析矢量图的 XML 文件,确保在低版本 Android 系统上也能正常加载矢量图。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Android 小码蜂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值