CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Android 开发入门/课时/传统 View UI
RawBlame
课时目录12 节

Android 开发入门

  1. 01Android 开发概览
  2. 02四大组件
  3. 03传统 View UI
  4. 04Jetpack Compose
  5. 05架构模式
  6. 06依赖注入
  7. 07本地存储
  8. 08网络层
  9. 09异步与响应式
  10. 10Firebase 生态
  11. 11测试与质量
  12. 12综合练习

传统 View UI

RecyclerView 与 Material。

30 分钟

传统 View UI

这一课你将能做到

  1. 用 RecyclerView 实现高性能列表,理解 ViewHolder 回收机制
  2. 使用 Material Design 组件(Card、Dialog、BottomSheet)构建企业级界面
  3. 对比传统 View 体系与 Compose 的优劣,知道何时选择哪种方案

为什么要学

虽然 Compose 是未来,但存量 Android 代码 80% 以上仍是传统 View 体系。RecyclerView 是列表场景的终极方案,Material 组件是 Google 的设计规范落地。掌握它们,才能维护老项目,也才能理解 Compose 底层在解决什么问题。

人话:传统 View 就像 jQuery 时代的 DOM 操作——命令式、手动同步状态;Compose 像 React——声明式、自动 diff。但 jQuery 项目不会一夜消失,RecyclerView 的回收思想甚至影响了 Compose 的 LazyColumn 设计。

面试中,「RecyclerView 优化」和「自定义 View 流程」是资深 Android 工程师的分水岭。

先建立图像

RecyclerView 回收机制:

┌─────────────────────────┐
│  屏幕可见区域              │
│  ┌─────┐ ┌─────┐       │
│  │Item1│ │Item2│       │  ← 可见的 ViewHolder
│  └─────┘ └─────┘       │
│  ┌─────┐ ┌─────┐       │
│  │Item3│ │Item4│       │
│  └─────┘ └─────┘       │
└─────────────────────────┘
         ↓ 滚动
┌─────────────────────────┐
│  回收池 (RecycledViewPool)│
│  ┌─────┐ ┌─────┐       │
│  │Item1│ │Item2│       │  ← 滑出屏幕,等待复用
│  └─────┘ └─────┘       │
└─────────────────────────┘
         ↓ 新数据进入
┌─────────────────────────┐
│  复用旧 ViewHolder        │
│  只绑定新数据,不创建新 View │
└─────────────────────────┘

跟着做

步骤 1:RecyclerView 基础列表

先定义数据模型:

data class Track(
    val slug: String,
    val title: String,
    val lessonCount: Int,
    val isCompleted: Boolean = false
)

创建 Adapter:

class TrackAdapter(
    private val tracks: List<Track>,
    private val onItemClick: (Track) -> Unit
) : RecyclerView.Adapter<TrackAdapter.TrackViewHolder>() {

    class TrackViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val titleText: TextView = itemView.findViewById(R.id.tvTitle)
        val countText: TextView = itemView.findViewById(R.id.tvCount)
        val statusIcon: ImageView = itemView.findViewById(R.id.ivStatus)
        val card: MaterialCardView = itemView.findViewById(R.id.cardView)
    }

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

    override fun onBindViewHolder(holder: TrackViewHolder, position: Int) {
        val track = tracks[position]
        holder.titleText.text = track.title
        holder.countText.text = "${track.lessonCount} 节课"
        holder.statusIcon.setImageResource(
            if (track.isCompleted) R.drawable.ic_check_circle 
            else R.drawable.ic_circle_outline
        )
        holder.card.setOnClickListener { onItemClick(track) }
    }

    override fun getItemCount() = tracks.size
}

在 Activity 中使用:

class TrackListActivity : AppCompatActivity() {
    
    private val tracks = listOf(
        Track("android-essentials", "Android 基础", 12, true),
        Track("kotlin-essentials", "Kotlin 入门", 10),
        Track("compose-advanced", "Compose 进阶", 8)
    )
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_track_list)
        
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = TrackAdapter(tracks) { track ->
            Toast.makeText(this, "点击: ${track.title}", Toast.LENGTH_SHORT).show()
        }
        
        // 添加分割线
        recyclerView.addItemDecoration(
            DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        )
    }
}

布局文件 item_track.xml:

<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="12dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp"
        android:gravity="center_vertical">

        <ImageView
            android:id="@+id/ivStatus"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:src="@drawable/ic_circle_outline"/>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="16dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tvTitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="18sp"
                android:textStyle="bold"/>

            <TextView
                android:id="@+id/tvCount"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="14sp"
                android:textColor="@android:color/darker_gray"/>
        </LinearLayout>
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>

步骤 2:DiffUtil 高效更新

数据变化时,不要直接 notifyDataSetChanged(),用 DiffUtil 计算差异:

class TrackDiffCallback(
    private val oldList: List<Track>,
    private val newList: List<Track>
) : DiffUtil.Callback() {

    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos].slug == newList[newPos].slug
    }

    override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos] == newList[newPos]
    }
}

// 在 Adapter 中使用
fun updateData(newTracks: List<Track>) {
    val diffResult = DiffUtil.calculateDiff(TrackDiffCallback(tracks, newTracks))
    (tracks as MutableList).clear()
    tracks.addAll(newTracks)
    diffResult.dispatchUpdatesTo(this)
}

步骤 3:Material Dialog 与 BottomSheet

// 确认对话框
MaterialAlertDialogBuilder(this)
    .setTitle("删除课程")
    .setMessage("确定要删除「Android 基础」吗?此操作不可撤销。")
    .setPositiveButton("删除") { _, _ ->
        // 执行删除
    }
    .setNegativeButton("取消", null)
    .show()

// BottomSheet
class TrackDetailBottomSheet : BottomSheetDialogFragment() {
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.bottom_sheet_track, container, false)
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.findViewById<Button>(R.id.btnStart).setOnClickListener {
            dismiss()
        }
    }
}

// 显示
TrackDetailBottomSheet().show(supportFragmentManager, "track_detail")

步骤 4:DrawerLayout 侧边导航

class MainActivity : AppCompatActivity() {
    
    private lateinit var drawerLayout: DrawerLayout
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        drawerLayout = findViewById(R.id.drawerLayout)
        val navView = findViewById<NavigationView>(R.id.navView)
        
        // 汉堡菜单按钮
        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        setSupportActionBar(toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        
        val toggle = ActionBarDrawerToggle(
            this, drawerLayout, toolbar,
            R.string.open, R.string.close
        )
        drawerLayout.addDrawerListener(toggle)
        toggle.syncState()
        
        navView.setNavigationItemSelectedListener { menuItem ->
            when (menuItem.itemId) {
                R.id.nav_home -> showToast("首页")
                R.id.nav_settings -> showToast("设置")
                R.id.nav_about -> showToast("关于")
            }
            drawerLayout.closeDrawer(GravityCompat.START)
            true
        }
    }
    
    override fun onBackPressed() {
        if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.closeDrawer(GravityCompat.START)
        } else {
            super.onBackPressed()
        }
    }
}

步骤 5:自定义 View 基础

class ProgressRingView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val rectF = RectF()
    private var progress = 0f
    
    init {
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 12f
        paint.strokeCap = Paint.Cap.ROUND
    }
    
    fun setProgress(value: Float) {
        progress = value.coerceIn(0f, 100f)
        invalidate()  // 触发重绘
    }
    
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val size = width.coerceAtMost(height).toFloat()
        val padding = paint.strokeWidth / 2
        
        rectF.set(
            padding, padding,
            size - padding, size - padding
        )
        
        // 背景圆环
        paint.color = Color.LTGRAY
        canvas.drawArc(rectF, 0f, 360f, false, paint)
        
        // 进度弧
        paint.color = Color.BLUE
        val sweepAngle = progress * 3.6f
        canvas.drawArc(rectF, -90f, sweepAngle, false, paint)
    }
    
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val size = MeasureSpec.getSize(widthMeasureSpec).coerceAtMost(
            MeasureSpec.getSize(heightMeasureSpec)
        )
        setMeasuredDimension(size, size)
    }
}

讲清楚要点

  1. RecyclerView 的核心是回收:onCreateViewHolder 只在需要新 View 时调用,onBindViewHolder 在复用时重新绑定数据。这就是「对象池」模式,避免频繁创建/销毁 View 导致的内存抖动。

  2. ViewHolder 不是可选的:早期 ListView 不用 ViewHolder 会导致 findViewById 每帧执行,性能极差。RecyclerView 强制使用 ViewHolder 模式。

  3. Material Design 是规范,不是库:MaterialCardView、TextInputLayout、BottomNavigationView 都是 Material 组件库的实现。遵循规范能提升用户体验的一致性。

  4. 自定义 View 三步骤:onMeasure(测量尺寸)、onLayout(确定位置,ViewGroup 需要)、onDraw(绘制内容)。invalidate() 触发重绘,requestLayout() 触发重新布局。

  5. 命令式 vs 声明式:传统 View 是命令式——找到 View,设置属性。Compose 是声明式——描述状态,框架自动更新。RecyclerView 的 Adapter 模式其实已经很接近声明式了。

你可能会懵的地方

  • RecyclerView 和 ListView 到底差在哪? ListView 只有一级缓存(ConvertView),RecyclerView 有四级(Scrap、Cache、RecycledViewPool、ViewCacheExtension)。RecyclerView 强制 ViewHolder、支持动画、布局管理器解耦。

  • 为什么 notifyDataSetChanged() 性能差? 它告诉 RecyclerView「所有数据都变了」,导致所有 ViewHolder 重新绑定,没有动画。DiffUtil 精确计算差异,只更新变化的项。

  • Fragment 和自定义 View 怎么选? Fragment 适合有独立生命周期、需要返回栈的「页面级」模块。自定义 View 适合「组件级」的 UI 元素,如进度环、图表。Compose 时代两者都减少使用。

  • XML 布局里的 tools: 命名空间有什么用? tools:text="示例文本" 只在 Android Studio 预览时显示,运行时不会渲染。用于设计时占位。

  • ConstraintLayout 和 LinearLayout 性能差很多吗? 嵌套 LinearLayout 会导致多次测量,性能差。ConstraintLayout 扁平化布局,一次测量。但简单场景 LinearLayout 更易读。

错法对照

错法问题正解
notifyDataSetChanged() 更新列表无动画,性能差DiffUtil 或 ListAdapter
在 onBindViewHolder 里创建对象每帧内存分配,GC 频繁对象复用或提前创建
在 Activity 里直接操作 Adapter 数据数据源与 UI 不同步统一在 ViewModel 管理数据
自定义 View 忘记 invalidate()属性改变后界面不更新修改属性后调用 invalidate()
在 onDraw 里创建 Paint 对象每帧内存分配在 init 或构造函数中创建

卡点急救

  1. RecyclerView 滚动卡顿:检查 onBindViewHolder 是否做了耗时操作(如解析日期、创建对象)。用 TraceView 或 Profiler 定位。

  2. MaterialCardView 圆角不显示:确保 app:cardCornerRadius 生效,且没有父布局裁剪了子 View。检查 android:clipToPadding 和 android:clipChildren。

  3. 自定义 View 在预览中不显示:@JvmOverloads 注解不能少,否则 Android Studio 无法实例化。检查构造函数参数是否匹配。

面试常问

  • RecyclerView 的缓存机制? 四级缓存:Scrap(屏幕内)、Cache(刚滑出,默认 2 个)、ViewCacheExtension(自定义)、RecycledViewPool(共享池)。优先级从高到低,取到 ViewHolder 后重新绑定数据。

  • 如何优化 RecyclerView 性能?

    1. DiffUtil 替代全量刷新;2. 避免 onBindViewHolder 耗时操作;3. 使用 setHasFixedSize(true) 跳过重新测量;4. 图片加载用 Glide/Coil 并取消不可见项;5. 减少布局嵌套。
  • 自定义 View 的绘制流程? measure() → layout() → draw()。onMeasure 确定宽高,onLayout 确定子 View 位置(ViewGroup),onDraw 执行绘制。invalidate 触发重绘,requestLayout 触发重新测量布局。

自测三问

  1. RecyclerView 的 onCreateViewHolder 和 onBindViewHolder 分别在什么时机调用?
  2. 为什么 MaterialAlertDialogBuilder 需要传入 Activity 而不是 Application Context?
  3. 自定义 View 中,invalidate() 和 requestLayout() 的区别?

小结一句话

传统 View 体系是 Android 的「旧大陆」——命令式、手动同步、性能需要精细调优;掌握 RecyclerView 和 Material 组件,才能维护存量代码,也才能理解 Compose 的革新之处。

延伸

  • RecyclerView 官方文档
  • Material Design 组件
  • 下一课:Jetpack Compose——声明式 UI 的现在与未来。
加载本节练习…
← 上一课四大组件下一课 →Jetpack Compose