你可能没有那么了解 RecycleView

2,576 阅读21分钟

本系列为小说《逆袭西二旗》的技术讲解,用于详细说明剧情里涉及的开发细节。

RecyclerView 的内部工作原理

00.png

RecyclerView 是一个实用且灵活的 Android 组件,旨在通过复用 ItemView(而非重复创建新 View)来高效显示大型数据集。它通过一种类似对象池的机制(即 ViewHolder 模式)管理 View,从而实现了这种高效性。

面试问题

与 ListView 相比,RecyclerView 的 ViewHolder 模式如何提升性能?

请解释 ViewHolder 从创建到被回收的生命周期。

什么是 RecycledViewPool?如何使用它来优化 ItemView 的渲染?

核心概念

  1. View 复用:RecyclerView 会复用现有 View,而不是为数据集中的每个 Item 创建新 View。当一个 View 滚动出可见区域时,它会被添加到一个 View 池(称为 RecyclerView.RecycledViewPool)中,而不是被销毁。当新 Item 进入可见区域时,RecyclerView 会从该池中获取一个可用的 View(如果有的话),从而避免了 inflate 带来的开销。
  2. ViewHolder 模式:RecyclerView 使用 ViewHolder 来存储 Item 布局中 View 的引用。这避免了在绑定过程中频繁调用 findViewById(),通过减少布局遍历和 View 查找来提升性能。
  3. Adapter 的作用:RecyclerView.Adapter 作为数据源和 RecyclerView 之间的桥梁。Adapter 的 onBindViewHolder() 方法会将数据绑定到被复用的 View 上,因此只需要更新可见的 Item。
  4. RecycledViewPool:RecycledViewPool 充当对象池,存储未使用的 View。它允许 RecyclerView 在多个列表或具有相似 View 类型的 Section 之间复用 View,进一步优化内存使用。

复用机制的工作流程

  1. 滚动与 Item 可见性:当用户滚动时,移出屏幕的 Item 会从 RecyclerView 中分离,但不会被销毁。相反,它们会被添加到 RecycledViewPool。
  2. 将数据重新绑定到复用的 View:当新 Item 进入可见区域时,RecyclerView 首先检查 RecycledViewPool 中是否有可用的对应类型 View。如果找到匹配项,它会复用该 View,并通过 onBindViewHolder() 将新数据绑定到其上。
  3. 如果没有可用 View 则 inflate:如果池中没有合适的 View,RecyclerView 会通过 onCreateViewHolder() inflate 一个新 View。
  4. 高效内存使用:通过复用 View,RecyclerView 减少了内存分配和垃圾回收,这在处理大型数据集或频繁滚动的场景中可能导致性能问题。

下面是一个基础 RecyclerView.Adapter 实现示例:

class MyAdapter(private val dataList: List<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.textView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.textView.text = dataList[position]
    }

    override fun getItemCount(): Int = dataList.size
}

RecyclerView 对象池方式有下面这些优势:

  1. 提升性能:复用 View 减少了 inflate 新布局的开销,使滚动更流畅,性能更好。
  2. 高效内存管理:对象池减少了内存分配,并通过复用 View 避免了频繁的垃圾回收。
  3. 可定制性:RecycledViewPool 可以定制,以管理每种类型 View 的最大数量,允许开发者针对特定场景优化行为。

总结

RecyclerView 采用了高效的对象池机制,未使用的 View 存储在 RecycledViewPool 中,并在需要时复用。这种设计与 ViewHolder 模式相结合,减少了内存使用和布局开销,提升了性能,使其成为 Android 应用中显示大型数据集的必备工具。

如果你还记得设计模式中享元模式的话,RecyclerView 就是这种思想的优良实现。

享元模式要素RecyclerView 中的对应实现
内部状态ItemView 的布局结构(XML 定义的视图层级) —— 这些是可复用、不可变的部分
外部状态每个 item 的具体数据(如文本内容、图片 URL、颜色等) —— 在 onBindViewHolder() 中动态绑定
享元工厂RecyclerView.RecycledViewPool + LayoutManager 的回收/复用逻辑 —— 负责创建、缓存和分发 ViewHolder
享元对象RecyclerView.ViewHolder 实例 —— 封装了 itemView,本身不包含业务数据

进阶:不同类型的 Item

RecyclerView 具有高度的灵活性,支持在同一个列表中显示多种 Item 类型。要实现这一点,你需要结合自定义 Adapter、ItemView 类型和适当的布局。关键在于正确区分 Item 类型并进行绑定。

实现多 Item 类型需要如下步骤:

  1. 定义 Item 类型:每种 Item 类型由唯一的标识符表示,通常是一个整数常量。这些标识符允许 Adapter 在创建和绑定 View 时区分不同的 Item 类型。
  2. 重写 getItemViewType() :在 Adapter 中重写此方法,为数据集中的每个 Item 返回适当的类型。该方法帮助 RecyclerView 确定需要布局的类型。
  3. 处理多个 ViewHolder :为每种 Item 类型创建单独的 ViewHolder 类。每个 ViewHolder 负责将数据绑定到其对应的 View。
  4. 根据 View 类型布局:在 onCreateViewHolder() 方法中,根据 getItemViewType() 返回的 View 类型进行布局。
  5. 相应地绑定数据:在 onBindViewHolder() 方法中,检查 Item 类型并使用对应的 ViewHolder 绑定数据。

下面是一个实现多 Item 类型的示例:

class MultiTypeAdapter(private val items: List<ListItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        const val TYPE_HEADER = 0
        const val TYPE_CONTENT = 1
    }

    override fun getItemViewType(position: Int): Int {
        return when (items[position]) {
            is ListItem.Header -> TYPE_HEADER
            is ListItem.Content -> TYPE_CONTENT
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            TYPE_HEADER -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_header, parent, false)
                HeaderViewHolder(view)
            }
            TYPE_CONTENT -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_content, parent, false)
                ContentViewHolder(view)
            }
            else -> throw IllegalArgumentException("Invalid view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is HeaderViewHolder -> holder.bind(items[position] as ListItem.Header)
            is ContentViewHolder -> holder.bind(items[position] as ListItem.Content)
        }
    }

    override fun getItemCount(): Int = items.size

    class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val title: TextView = itemView.findViewById(R.id.headerTitle)

        fun bind(item: ListItem.Header) {
            title.text = item.title
        }
    }

    class ContentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val content: TextView = itemView.findViewById(R.id.contentText)

        fun bind(item: ListItem.Content) {
            content.text = item.text
        }
    }
}

sealed class ListItem {
    data class Header(val title: String) : ListItem()
    data class Content(val text: String) : ListItem()
}

要点提示

  1. 效率:RecyclerView 会复用 ViewHolder,处理多种 Item 类型不会影响性能。每种 Item 类型通过 getItemViewType() 方法和正确的绑定得到高效管理。
  2. 清晰分离:每种 Item 类型都有自己的布局和 ViewHolder,确保关注点分离和代码更清晰。
  3. 可扩展性:添加新的 Item 类型只需最小的改动。你只需定义一个新布局、一个 ViewHolder,并调整 getItemViewType() 和 onCreateViewHolder() 中的逻辑。

总而言之,要在 RecyclerView 中实现多种 Item 类型,需结合 getItemViewType() 确定每个 Item 的类型、为每种类型使用单独的 ViewHolder,以及为每种类型使用不同的布局。这种方法确保 RecyclerView 在支持统一列表中的多样内容时,仍然保持高效和可扩展。

进阶:提升性能

你可以通过使用 ListAdapter 和 DiffUtil 来提升 RecyclerView 的性能。

DiffUtil 是 Android 中的一个工具类,用于计算两个列表之间的差异,并更高效地更新 RecyclerView.Adapter。

通过利用 DiffUtil,你可以避免不必要地调用 notifyDataSetChanged(),这会导致列表中所有 Item 都被低效重绘。相反,DiffUtil 能在细粒度级别识别变化并仅更新受影响的 Item。

RecyclerView 的默认行为是在数据变化时重新绑定并重绘所有可见 Item,即使大多数 Item 保持不变。

这样做会降低性能,尤其是在处理大型数据集时。DiffUtil 通过计算所需的最小更新集(插入、删除和修改)并直接应用于 Adapter 来优化这一点。

使用 DiffUtil 的步骤入校

  1. 创建 DiffUtil 回调:实现自定义的 DiffUtil.ItemCallback 或扩展 DiffUtil.Callback。该类定义了如何计算新旧列表之间的差异。
  2. 向 Adapter 提供列表更新:当新数据到达时,将其传递给 Adapter,并使用 DiffUtil 计算差异。通过 submitList()(如果你使用 ListAdapter)或 notifyItemChanged()(对于自定义 Adapter)等方法将这些变化应用到 Adapter。
  3. 将 DiffUtil 与 RecyclerView.Adapter 绑定:将 DiffUtil 集成到 Adapter 中,自动处理更新。

我们来看下为 RecyclerView 实现 DiffUtil 的示例:

class MyDiffUtilCallback : DiffUtil.ItemCallback<MyItem>() {
    override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
        // 检查 Item 是否代表相同的数据(例如,相同的 ID)
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
        // 检查 Item 的内容是否相同
        return oldItem == newItem
    }
}

class MyAdapter : ListAdapter<MyItem, MyViewHolder>(MyDiffUtilCallback()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
}

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val textView: TextView = itemView.findViewById(R.id.textView)

    fun bind(item: MyItem) {
        textView.text = item.name
    }
}

// 使用示例
val adapter = MyAdapter()
recyclerView.adapter = adapter

val oldList = listOf(MyItem(1, "Old Item"), MyItem(2, "Another Item"))
val newList = listOf(MyItem(1, "Updated Item"), MyItem(3, "New Item"))

// 自动计算差异并更新 RecyclerView
adapter.submitList(newList)

DiffUtil 有如下主要优势:

  1. 提升性能:无需刷新整个列表,仅更新修改过的 Item,减少了渲染开销。
  2. 细粒度更新:单独处理 Item 的插入、删除和修改,使动画更流畅、更自然。
  3. 与 ListAdapter 无缝集成:ListAdapter 是 Android Jetpack 库中现成的 Adapter,它直接集成了 DiffUtil,减少了样板代码。

不过注意:

  1. 大型列表的开销:虽然 DiffUtil 很高效,但计算非常大的列表之间的差异可能会消耗大量计算资源。在这种情况下,请谨慎使用。
  2. 不可变数据:确保你的数据模型是不可变的。当 DiffUtil 尝试计算变化时,可变数据可能导致不一致。如果对这项有疑问,看这里。

总之,在 RecyclerView 中使用 DiffUtil 可以通过仅应用必要的更新(而非重绘整个列表)来提升性能。借助 DiffUtil.ItemCallback 和 ListAdapter,你可以高效管理更新,创建更流畅的动画,并提升 UI 的整体响应速度。这种方法在处理频繁变化的数据集或大型列表时尤其有价值。

ConstraintLayout

ConstraintLayout 是 Android 中引入的一种灵活且强大的布局,用于创建复杂的响应式用户界面,无需嵌套多层布局。它允许你通过相对于其他 View 或父容器的约束来定义 View 的位置和尺寸。这消除了深度嵌套的 View 层级,提升了性能和可读性。

ConstraintLayout 简直就是单层布局的杀手锏,配合 RecycleView 优化布局,也能提升不少布局的性能。

面试问题

与嵌套的 LinearLayout 和 RelativeLayout 相比,ConstraintLayout 如何提升性能?

请提供一个场景说明何时使用 ConstraintLayout 会更高效。

请解释 ConstraintLayout 中 match_constraint(0dp) 的行为。它与 wrap_content 和 match_parent 有何不同?在什么情况下会使用它?

核心特性

  1. 基于约束的定位:View 可以相对于同级 View 或父布局,通过对齐、居中、锚定等约束进行定位。
  2. 灵活的尺寸控制:提供 match_constraint、wrap_content 和固定尺寸等选项,便于设计响应式布局。
  3. 链与参考线支持:链可以让多个 View 在水平或垂直方向上等距分组;参考线则支持基于固定或百分比位置的对齐。
  4. 屏障与分组:屏障会根据引用 View 的尺寸动态调整位置;分组可简化多个 View 的可见性变更。
  5. 性能提升:减少了多层嵌套布局的需求,使布局渲染更快,性能更优。

以下代码演示了一个包含 TextView 和 Button 的简单布局,其中 Button 位于 TextView 下方并水平居中。

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello, World!"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me"
        app:layout_constraintTop_toBottomOf="@id/title"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

优势

  1. 扁平化 View 层级:与嵌套的 LinearLayout 或 RelativeLayout 不同,ConstraintLayout 支持扁平化层级,提升渲染性能并简化布局管理。
  2. 响应式设计:提供百分比约束、屏障等工具,可适配不同屏幕尺寸和方向的布局。
  3. 内置工具支持:Android Studio 的布局编辑器为 ConstraintLayout 提供了可视化设计界面,便于创建和调整约束,不过我这里需要吐糟一句,这个工具一点都不好用,因为很多对齐的尺寸边距都是硬编码,没有复用。
  4. 高级功能:链、参考线和屏障无需额外代码或嵌套布局即可简化复杂 UI 设计。

局限性

任何布局方式都有局限性:

  1. 简单布局的冗余性:对于简单布局,使用 LinearLayout 或 FrameLayout 就足够了,此时 ConstraintLayout 可能显得过于复杂。
  2. 学习曲线:需要理解约束和高级功能,对初学者可能有一定挑战。

适用场景

  1. 响应式 UI:适合需要精确对齐并适配不同屏幕尺寸的设计。
  2. 复杂布局:适合包含多个重叠元素或精细定位需求的 UI。
  3. 性能优化:通过用单一的扁平结构替换嵌套层级,帮助优化布局性能。

总结

ConstraintLayout 是一种多功能且高效的布局,用于设计 Android UI。它消除了嵌套布局的需求,提供了强大的定位和对齐工具,并提升了性能。虽然存在一定的学习曲线,但掌握 ConstraintLayout 可以让开发者高效创建响应式且视觉吸引力强的布局。

SurfaceView vs TextureView

11.png

我相信大多数开发者都碰见过 SurfaceView 的布局问题。

SurfaceView 是一种特殊的 View,它提供了一个专用的绘制表面,专为渲染在独立线程中处理的场景设计,常见于视频播放、自定义图形渲染或游戏等对性能要求极高的任务。

面试问题

如何正确管理 SurfaceView 的生命周期,以确保高效的资源管理并避免内存泄漏?

如果需要显示带有旋转和缩放等变换的实时相机预览,你会在 SurfaceView 和 TextureView 之间选择哪一个?请结合实际考量说明理由。

SurfaceView

SurfaceView 的一个关键特性是它在主线程之外创建了一个独立的绘制表面,这使得高效渲染成为可能,而不会阻塞其他 UI 操作。

Surface 通过 SurfaceHolder 回调方法创建和管理,你可以在这些回调中启动或停止按需渲染。例如,你可以在游戏循环中使用 SurfaceView 进行连续绘制,或使用底层 API 绘制图形。

class CustomSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
    init {
        holder.addCallback(this)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // 在此处开始渲染或绘制
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        // 处理表面变化
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // 在此处停止渲染或释放资源
    }
}

看这里,教会你如何在 Compose 中使用 SurfaceView。

虽然 SurfaceView 在连续渲染时效率很高,但它在缩放、旋转等变换方面存在限制,因此更适合高性能场景,而不太适合动态 UI 交互。

TextureView

TextureView 提供了另一种离屏渲染方式,但与 SurfaceView 不同,它能无缝集成到 UI 层级中。这意味着 TextureView 可以被变换或动画,支持旋转、缩放和 alpha 混合等特性。它常用于显示实时相机预览或使用自定义变换播放视频。

class CustomTextureView(context: Context) : TextureView(context), TextureView.SurfaceTextureListener {
    init {
        surfaceTextureListener = this
    }

    override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
        // 开始渲染或使用 SurfaceTexture
    }

    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
        // 处理表面尺寸变化
    }

    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
        // 释放资源或停止渲染
        return true
    }

    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
        // 处理表面纹理更新
    }
}

与 SurfaceView 不同,TextureView 在主线程上运行。虽然这使得它在连续渲染时效率略低,但它能更好地与其他 UI 组件集成,并支持实时变换。

区别

这两种组件的主要区别在于渲染方式和 UI 集成:

  • SurfaceView 在独立线程中运行,适合视频播放或游戏等连续渲染场景。它还创建了一个独立的渲染窗口,确保性能,但代价是无法被变换或动画。
  • TextureView 与其他 UI 组件共享同一窗口,因此可以被缩放、旋转或动画,使其在 UI 相关场景中更灵活。但由于它在主线程上运行,在高频渲染任务中可能不如 SurfaceView 高效。

总结

SurfaceView 最适合性能优先的场景,如游戏或连续视频渲染。而 TextureView 更适合需要无缝 UI 集成和视觉变换的场景,如动画视频或显示实时相机预览。选择哪一个取决于你的应用是优先考虑性能还是 UI 灵活性。

Dp 与 Sp

在设计 Android 用户界面时,你需要考虑 UI 组件如何适应不同的屏幕尺寸和分辨率。用于此目的的两个基本单位是 Dp(密度无关像素) 和 Sp(缩放无关像素) 。两者都有助于确保在不同设备上的一致性,但用途不同。

面试问题

使用 Sp 作为文本大小时可能会出现哪些潜在的布局溢出问题?如何防止这些问题?

什么是 Dp

Dp(Density-independent Pixels,密度无关像素)是一种用于 UI 元素(如内边距、外边距和宽度)的度量单位。它旨在确保 UI 组件在不同屏幕密度的设备上具有一致的物理尺寸。在 160 DPI(每英寸点数)的屏幕上,1 Dp 等于 1 个物理像素,Android 会自动缩放 Dp 以匹配设备的密度。

例如,如果你指定一个 Button 的宽度为 100 Dp,它在低密度和高密度屏幕上看起来的大小大致相同(注意不是完全相同,这太难了),尽管渲染它所需的像素数量会有所不同。

什么是 Sp

Sp(Scale-independent Pixels,缩放无关像素)专门用于文本大小。它的行为与 Dp 类似,但额外考虑了用户的字体大小偏好。这意味着 Sp 会同时根据屏幕密度和设备的辅助功能设置进行缩放,使其成为确保文本可读性和可访问性的理想选择。

例如,如果你将 TextView 的大小设置为 16 Sp,它会根据屏幕密度进行适当缩放,并且如果用户增大了系统字体大小,它也会相应调整。

主要区别

主要区别在于它们的缩放行为:

  1. 用途:Dp 用于尺寸(例如,按钮大小、内边距),Sp 用于文本大小。
  2. 可访问性:Sp 尊重用户定义的字体大小偏好,而 Dp 则不。
  3. 一致性:两者都会随屏幕密度缩放,但 Sp 确保文本对所有用户来说都是可访问和可读的。

取舍

  • 使用 Dp 用于 UI 组件(如 View 尺寸、外边距和内边距),以在不同设备上保持一致的布局。
  • 使用 Sp 用于文本,以在保持视觉一致性的同时尊重用户的辅助功能偏好。

总结

Dp 确保 UI 组件在不同设备上的物理尺寸一致,而 Sp 则根据屏幕密度和用户偏好调整文本大小。这种区分对于创建视觉吸引力强且可访问的 Android 应用至关重要。

进阶:使用 Sp 单位时如何处理布局溢出

使用 Sp(缩放无关像素)对于确保 Android 上的文本可访问性至关重要,因为它会根据屏幕密度和用户字体偏好进行缩放。然而,用户定义的大字体大小可能导致文本元素超出预期边界,这会破坏布局,尤其是在按钮、标签或紧凑屏幕等受限空间中。正确处理这些场景对于维护用户友好的体验至关重要。

下面介绍一下防止布局溢出的策略。

当用户显著增大系统字体大小时,Sp 单位可能导致文本元素超出预期边界。这会破坏布局,尤其是在按钮、标签或紧凑屏幕等受限空间中。

  1. 正确设置 wrap_content:确保基于文本的组件(如 TextView 或 Button)的大小设置为 wrap_content。这允许容器根据文本大小动态扩展,避免文本截断或溢出。

    <TextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:textSize="16sp"
         android:text="Sample Text" />
    
  2. 为 TextView 使用 minLines 或 maxLines:要控制文本扩展的行为,请使用 minLines 和 maxLines 属性,确保文本保持可读性而不破坏布局。结合 ellipsize 优雅地处理溢出。

    <TextView
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:textSize="16sp"
         android:maxLines="2"
         android:ellipsize="end"
         android:text="This is a long sample text that might break the layout if not handled properly." />
    
  3. 为关键 UI 组件使用固定大小:在需要确保一致大小的场景中,考虑为按钮等关键组件使用 Dp。这确保组件大小在文本缩放时保持稳定。在这些组件中,谨慎使用 Sp 作为文本大小以减少布局溢出。

    <Button
        android:layout_width="100dp"
        android:layout_height="50dp"
        android:textSize="14sp"
        android:text="Button" />
    
  4. 测试极端字体大小:始终使用设备设置中最大的系统字体大小测试你的应用。识别会溢出或重叠的 UI 组件,并优化其布局以优雅地容纳更大的文本。

  5. 考虑使用约束进行动态调整:使用 ConstraintLayout 增加定位和大小调整的灵活性。为文本元素定义约束,避免与其他 UI 元素重叠,即使在文本缩放时也是如此。

    <ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <TextView
            android:id="@+id/sampleText"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:text="Dynamic Layout Example" />
    
    </ConstraintLayout>
    
  6. 使用 Dp 而不是 Sp:虽然作为开发者可以这么做,但是这是投机取巧的做法。当然,这也取决于团队的方法。
    一些公司选择为文本大小使用 Dp 而不是 Sp,以防止用户调整字体大小导致的布局问题。然而,这种方法可能会影响用户体验,因为 Sp 是专门为支持辅助功能而设计的,可确保文本根据用户的可读性偏好进行调整。

总之,要处理 Sp 导致的布局溢出,请结合 wrap_content、设置约束和定义文本扩展限制等最佳实践。使用极端字体大小测试并动态管理布局,可确保你的应用在视觉上保持一致并对所有用户可访问。

点九图

点九图片是一种特殊格式的 PNG 图片,它可以在拉伸或缩放时不丢失视觉质量,使其成为 Android 中创建灵活且可适配 UI 组件的必备工具。它主要用于按钮、背景和容器等需要动态调整大小以适应不同屏幕尺寸和内容尺寸的元素。

面试问题

点九图与普通 PNG 图片有何不同?在哪些场景下需要使用九图图片?

核心特性

  1. 可拉伸区域:点九图可以定义可拉伸的区域,同时保持图片其他部分的完整性。这是通过在图片最外层1像素的边框中绘制黑线(引导线)实现的。
  2. 内容区域定义:这些黑线还指定了图片内部的内容区域,确保文本或其他 UI 元素在可绘制对象内正确对齐。
  3. 动态调整大小:它们会按比例调整大小,确保 UI 元素在不同屏幕尺寸的设备上都能保持美观的显示效果。

以下代码演示了如何将九图图片用作按钮的背景:

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/button_background"
    android:text="Click Me" />

这和普通图片的使用没什么两样。

点九图的局限性

  1. 手动创建:需要仔细创建和测试,以确保正确的缩放和对齐。
  2. 适用场景有限:最适合矩形或方形元素,对于复杂或不规则形状的效果较差。

总结

点九图是一种灵活且高效的方案,用于在 Android 中创建可缩放且视觉一致的 UI 组件。通过定义可拉伸区域和内容区域,它确保按钮和背景等元素能无缝适配不同屏幕尺寸和动态内容,同时保持美观的显示效果。

不过,点九图最好的学习方法,就是尝试自己做一次!就什么都明白了。