博客專欄:Android初級入門UI組件與布局
源碼:通過網盤分享的文件:Android入門布局及UI相關案例
鏈接: https://pan.baidu.com/s/1EOuDUKJndMISolieFSvXXg?pwd=4k9n 提取碼: 4k9n
引言
在 Android 應用中,RecyclerView?是最常用的列表展示組件,無論是商品列表、新聞流、視頻推薦頁,還是內容首頁,幾乎無處不在。初學者常常從單一類型列表入門,比如簡單的文字列表或圖文卡片;但一旦進入實際項目,我們面臨的卻是更加復雜的列表結構:
- 同一個頁面中可能出現多種布局樣式:頂部 Banner、大標題、橫向滑動卡片、豎向新聞流……
- 用戶操作或接口更新后,列表數據需要實時刷新,并盡可能做到精準刷新、避免性能浪費
也就是說,多類型布局支持 + 高性能刷新機制,幾乎是中高級 RecyclerView 使用中繞不開的兩個課題。
本篇我們將通過一個實戰 Demo,從零開始實現這樣一個典型的首頁列表:
1. 頁面頂部展示一張 Banner 圖片
2. 中間是「推薦直播間」,每行顯示兩個直播卡片
3. 接著是「最新資訊」,展示若干新聞摘要
4. 支持兩種刷新方式:
- 「全量刷新」:使用?notifyDataSetChanged()
- 「智能刷新」:使用?DiffUtil?精準對比變化項
接下來,我們先來看如何實現多類型布局的支持。
一、支持多類型 Item 的 RecyclerView
1.1 定義多類型數據模型
在實際業務中,RecyclerView 常常需要展示不止一種布局。例如電商首頁中常見的結構就包括:
- 頂部的 Banner 區域;
- 內容分區標題,如“猜你喜歡”、“熱門直播”;
- 模塊內容卡片,如直播間、新聞資訊等。
因此我們要先將這些內容類型抽象成數據模型類,并配合布局實現可復用的多類型渲染。
? 本 Demo 中的四種模型:
類型 | 用途 | 數據類定義 |
---|---|---|
Banner | 頂部大圖 | data class Banner(val imageResId: Int) |
TitleItem | 分組標題(如推薦直播) | data class TitleItem(val text: String) |
LiveItem | 直播卡片 | data class LiveItem(val title: String, val coverResId: Int) |
NewsItem | 新聞摘要卡片 | data class NewsItem(val title: String, val summary: String) |
這四個類分別代表頁面中四種功能塊,我們會將它們依次插入到列表數據源中(統一為?List<Any>),并在 Adapter 中通過類型判斷進行區分渲染。
Banner:
data class Banner(val imageResId: Int
)
TitleItem:
data class TitleItem(val text: String
)
LiveItem:
data class LiveItem(val title: String,val coverResId: Int
)
NewsItem:
data class NewsItem(val title: String,val summary: String
)
1.2 Adapter 與 ViewHolder 實現
有了多類型的數據模型之后,下一步就是在 Adapter 中進行“識別”和“綁定”。在 RecyclerView 中,支持多類型的關鍵機制有兩個:
? getItemViewType(position: Int): Int
這個方法用來告訴 RecyclerView:當前 position 對應的數據項屬于哪種類型,我們可以為每個類型分配一個整數常量:
override fun getItemViewType(position: Int): Int {return when (items[position]) {is Banner -> TYPE_BANNERis TitleItem -> TYPE_TITLEis LiveItem -> TYPE_LIVEis NewsItem -> TYPE_NEWSelse -> throw IllegalArgumentException("未知類型")}
}
這樣 RecyclerView 才知道該使用哪個布局去創建 ViewHolder。
? onCreateViewHolder(parent, viewType)
根據?viewType?創建不同類型的 ViewHolder 和對應布局:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {val inflater = LayoutInflater.from(parent.context)return when (viewType) {TYPE_BANNER -> BannerViewHolder(inflater.inflate(R.layout.item_banner, parent, false))TYPE_TITLE -> TitleViewHolder(inflater.inflate(R.layout.item_title, parent, false))TYPE_LIVE -> LiveViewHolder(inflater.inflate(R.layout.item_live, parent, false))TYPE_NEWS -> NewsViewHolder(inflater.inflate(R.layout.item_news, parent, false))else -> throw IllegalArgumentException("未知 viewType")}
}
? onBindViewHolder(holder, position)
再根據 position 取得數據并綁定到對應的 ViewHolder 上:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {when (val item = items[position]) {is Banner -> (holder as BannerViewHolder).bind(item)is TitleItem -> (holder as TitleViewHolder).bind(item)is LiveItem -> (holder as LiveViewHolder).bind(item)is NewsItem -> (holder as NewsViewHolder).bind(item)}
}
? ViewHolder 示例
每個類型的 ViewHolder 都可定義為內部類,綁定對應控件:
class NewsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {private val title: TextView = itemView.findViewById(R.id.newsTitle)private val summary: TextView = itemView.findViewById(R.id.newsSummary)fun bind(item: NewsItem) {title.text = item.titlesummary.text = item.summary}
}
其他 ViewHolder 如 BannerViewHolder、LiveViewHolder 也類似,全部代碼可以查看博客頂部的demo。
1.3 布局設計與 GridLayoutManager 使用
在基礎用法中,我們通常使用?LinearLayoutManager?來實現 RecyclerView 的豎向排列。但當頁面中存在需要「多列展示」的內容(如直播卡片),我們就可以借助?GridLayoutManager?實現靈活的布局排布。
? 目標排布效果:
Item 類型 | 排布方式 |
---|---|
Banner | 占整行(1 列 * 100%) |
TitleItem | 占整行 |
LiveItem | 每行顯示兩個(2 列) |
NewsItem | 占整行 |
? 使用 GridLayoutManager
在?MainActivity.kt?中設置 RecyclerView 的布局方式:
val layoutManager = GridLayoutManager(this, 2)
這里的?2?表示列表每行最多兩列。
? 控制每種 item 的跨列數:SpanSizeLookup
我們不希望所有 item 都是兩列的,而是只有 LiveItem 兩列,其他類型都應該「占滿整行」。為此我們通過?SpanSizeLookup?來動態指定每個 item 占幾列:
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {override fun getSpanSize(position: Int): Int {return when (adapter.getItemViewType(position)) {HomeAdapter.TYPE_LIVE -> 1 // 每行兩個else -> 2 // 占整行}}
}
這樣就實現了「混合布局」的效果:LiveItem 為兩列,其它都是一列跨兩格。
? item 布局
item_banner.xml:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/bannerImage"android:layout_width="match_parent"android:layout_height="180dp"android:scaleType="centerCrop"android:src="@drawable/placeholder"android:contentDescription="Banner" />
item_title.xml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/titleText"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="推薦直播"android:textSize="18sp"android:textStyle="bold"android:padding="16dp"android:textColor="#222222" />
item_live.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:padding="12dp"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/liveImage"android:layout_width="match_parent"android:layout_height="160dp"android:scaleType="centerCrop"android:src="@drawable/placeholder" /><TextViewandroid:id="@+id/liveTitle"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingTop="8dp"android:text="直播標題"android:textSize="16sp"android:textColor="#333333" />
</LinearLayout>
item_news.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:padding="12dp"android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid:id="@+id/newsTitle"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="資訊標題"android:textSize="16sp"android:textColor="#222222"android:textStyle="bold" /><TextViewandroid:id="@+id/newsSummary"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingTop="4dp"android:text="這是一個資訊概要內容..."android:textSize="14sp"android:textColor="#666666" />
</LinearLayout>
最終效果如下:
二、數據刷新機制的兩種實現方式
RecyclerView 是一個高性能的列表控件,但要想實現真正“流暢”的體驗,光靠展示還不夠 —— 列表的數據經常會更新(新增、修改、刪除),這時候就需要通過刷新機制來同步 UI。
在本 Demo 中,我們實現了兩種刷新方式:
- ? 全量刷新(notifyDataSetChanged())
- ? 智能刷新(DiffUtil)
2.1 全量刷新(notifyDataSetChanged)
這是最常見、也是最基礎的刷新方法。當你調用:
adapter.notifyDataSetChanged()
RecyclerView 會強制重繪整個列表,無論數據變化了多少項。雖然簡單,但這帶來了兩個明顯的問題:
- ? 性能低:所有可見項都要重新綁定;
- ? 無動畫:看不到“哪一項”發生了變化,用戶感知不明顯。
實現如下:
findViewById<Button>(R.id.buttonFullRefresh).setOnClickListener {val newList = originalItems.toMutableList()// 修改兩條 NewsItem 內容newList.replaceAll {if (it is NewsItem && it.title.contains("Kotlin")) {it.copy(summary = "Kotlin 2.0 已正式上線,快來試試吧!")} else if (it is NewsItem && it.title.contains("Compose")) {it.copy(summary = "Compose 新特性:自動響應式刷新")} else it}// 添加兩條新的 LiveItem,插入在第一個 LiveItem 后val insertIndex = newList.indexOfFirst { it is LiveItem }if (insertIndex != -1) {newList.add(insertIndex, LiveItem("直播新品推薦", R.drawable.placeholder))newList.add(insertIndex + 1, LiveItem("夜間慢直播", R.drawable.placeholder))}// 替換原數據并全量刷新originalItems.clear()originalItems.addAll(newList)adapter.notifyDataSetChanged()}
? 使用場景:
- 快速原型開發;
- 頁面結構很小、數據量不大;
- 數據變化劇烈、難以追蹤變化項(如每次全替換)。
2.2 智能刷新(DiffUtil)
DiffUtil?是 Android 官方推薦的列表刷新工具,可以根據「舊數據」與「新數據」之間的差異,計算出:
- 哪些項需要插入、刪除、更新;
- 哪些項保持不變、可復用。
? 核心用法:
val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(adapter)
你需要實現一個?DiffUtil.Callback,并重寫以下方法:
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {// 判斷是否為同一條數據
}override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {// 判斷內容是否有變更
}
實現如下:
findViewById<Button>(R.id.buttonSmartRefresh).setOnClickListener {val newList = originalItems.toMutableList()// 修改兩條 NewsItem 內容newList.replaceAll {if (it is NewsItem && it.title.contains("Kotlin")) {it.copy(summary = "Kotlin 2.0 已正式上線,快來試試吧!")} else if (it is NewsItem && it.title.contains("Compose")) {it.copy(summary = "Compose 新特性:自動響應式刷新")} else it}// 添加兩條新的 LiveItem,插入在第一個 LiveItem 后val insertIndex = newList.indexOfFirst { it is LiveItem }if (insertIndex != -1) {newList.add(insertIndex, LiveItem("直播新品推薦", R.drawable.placeholder))newList.add(insertIndex + 1, LiveItem("夜間慢直播", R.drawable.placeholder))}val diffCallback = object : DiffUtil.Callback() {override fun getOldListSize() = originalItems.sizeoverride fun getNewListSize() = newList.sizeoverride fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {return originalItems[oldItemPosition] == newList[newItemPosition]}override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {return originalItems[oldItemPosition] == newList[newItemPosition]}}val diffResult = DiffUtil.calculateDiff(diffCallback)originalItems.clear()originalItems.addAll(newList)diffResult.dispatchUpdatesTo(adapter)}
? 使用場景:
- 大量數據頻繁變化;
- 想要提升滾動流暢度;
- 希望有插入/刪除動畫過渡。
結語
在本篇實戰中,我們圍繞一個首頁式的列表頁面,完整實現了 RecyclerView 在復雜場景下的兩大關鍵能力:
? 多類型布局支持
- 通過定義多個數據模型(Banner、TitleItem、LiveItem、NewsItem),實現頁面結構化組織;
- 使用?getItemViewType()?方法判斷類型,搭配多個布局文件實現靈活展示;
- 借助?GridLayoutManager?+?SpanSizeLookup?實現不同 item 的排布策略,兼顧視覺和性能。
? 數據刷新機制
- 使用?notifyDataSetChanged()?實現最基礎的全量刷新;
- 使用?DiffUtil?實現性能更優、體驗更佳的智能刷新;
- 對比展示了兩種刷新機制在實現與效果上的差異,方便在項目中做出合理選擇。
📈 可擴展方向
如果你已經掌握了本文的內容,下面這些方向將進一步提升你的列表開發能力:
擴展點 | 描述 |
---|---|
點擊事件封裝 | 為不同類型的 item 添加點擊、長按等事件回調 |
加載更多 / 分頁 | 實現滑動到底部自動加載下一頁內容 |
空數據 / 錯誤狀態處理 | 在數據為空或加載失敗時展示占位 UI |
多類型封裝優化 | 用委托或泛型方式封裝多類型 Adapter,簡化維護 |
使用 Paging3 | 對接分頁接口,自動處理刷新與數據合并 |
Jetpack Compose 實現 | 通過 Compose 編寫聲明式列表,更加現代化 |