07.RecyclerView、SwipeRefreshLayout、SmartRefreshLayout

文章目录

RecyclerView、SwipeRefreshLayout、SmartRefreshLayout

1.RecyclerView

RecyclerView 是 Android 中一个功能强大的 UI 组件,用于高效展示大量数据的列表。其名称中的 "Recycler"(回收器)揭示了它最核心的特性:通过复用和回收列表项视图来显著提升性能。

  • 高效展示大量数据
  • 支持灵活的布局方式
  • 提供流畅的滚动体验
  • 内存使用经过深度优化

1.1 布局文件

activity_recycler_view_demo.xml - 主界面布局,包含一个占满屏幕的 RecyclerView:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".demo.newslist.RecyclerViewDemoActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:padding="8dp" />

</LinearLayout>

item_news.xml - 新闻列表项布局,使用 CardView 作为容器,包含标题、图片和内容摘要:

xml 复制代码
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

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

        <ImageView
            android:id="@+id/newsImageView"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:layout_marginTop="8dp"
            android:scaleType="centerCrop" />

        <TextView
            android:id="@+id/contentTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:textSize="14sp" />
    </LinearLayout>
</androidx.cardview.widget.CardView>

1.2 Adapter 代码

Adapter 是 RecyclerView 的核心组件,负责创建列表项视图并将数据绑定到对应的视图上。

kotlin 复制代码
class NewsAdapter(private val list: MutableList<New>) : RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {

    // 1. 创建 ViewHolder
    class NewsViewHolder(val binding : ItemNewsBinding) : RecyclerView.ViewHolder (binding.root)

    // 2. 重写 onCreateViewHolder、getItemCount、onBindViewHolder方法
    // 作用: 创建 ViewHolder 布局并返回
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
        val binding = ItemNewsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return NewsViewHolder(binding)
    }

    // 作用: 返回列表项数量
    override fun getItemCount(): Int {
        return list.size
    }

    // 作用: 绑定数据
    override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
        // 获取当前项数据
        val newItem = list[position]

        // 绑定数据
        holder.binding.titleTextView.text = newItem.title
        holder.binding.contentTextView.text = newItem.hint
        Glide.with(holder.itemView.context).load(newItem.image).into(holder.binding.newsImageView)
    }

    fun updateData(newList: List<New>) {
        list.clear()
        list.addAll(newList)
        notifyDataSetChanged()
    }
}

在 Android 开发中,notifyDataSetChanged()RecyclerView 适配器的一个重要方法,用于通知 Adapter 数据集已发生变化,需要刷新整个列表的显示。

  • 触发 UI 更新 :当 RecyclerView 绑定的数据(ListArray)发生增删改时,调用该方法会强制刷新所有列表项(Item),确保 UI 与数据保持同步。
  • 全量刷新 :该方法会重新执行 AdapteronBindViewHolder(),对所有可见项进行数据绑定,因此性能开销较大。

在实际开发中,应优先使用具体的局部更新方法(如 notifyItemInserted()),将 notifyDataSetChanged() 作为最后手段。

  • 为什么 notifyDataSetChanged() 效率较低?

    • 全量刷新:该方法不会指明数据的具体变化类型(增/删/改/移动),而是简单地通知 Adapter 所有数据"可能已失效"。
    • 连锁反应
      • LayoutManager 必须对所有可见的 ItemView 执行重新绑定(rebind)和重新布局(relayout)。
      • RecyclerView 会对所有可见项重新执行 onBindViewHolder(),即使某些项的数据并未发生变化。
  • 建议优先使用以下细粒度更新方法,明确指定变化类型和位置,以获得更好的性能:

    方法 适用场景 性能优势
    notifyItemInserted(position) 在 position 处新增一项 仅新增项需要布局
    notifyItemRemoved(position) 删除 position 处的项 仅移除项影响布局
    notifyItemChanged(position) position 处的数据更新 仅重绑单项数据
    notifyItemMoved(from, to) 项从 from 移动到 to 仅处理移动动画和布局微调
    notifyItemRangeChanged() 批量更新连续范围内的项 比全量刷新更高效

1.3 Activity 代码

kotlin 复制代码
class RecyclerViewDemoActivity : AppCompatActivity() {
    private lateinit var binding: ActivityRecyclerViewDemoBinding
    private lateinit var newsAdapter: NewsAdapter
    private var newsList = mutableListOf<New>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivityRecyclerViewDemoBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 初始化 RecyclerView
        newsAdapter = NewsAdapter(newsList)

        // 线性布局,垂直方向
        // binding.recyclerView.layoutManager = LinearLayoutManager(this)
        
        // 线性布局,垂直方向,不反转
        // binding.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

        // 网格布局,2列
        binding.recyclerView.layoutManager = GridLayoutManager(this, 2)

        // 瀑布流布局,2列,垂直方向,不反转
        // binding.recyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)

        // 设置适配器
        binding.recyclerView.adapter = newsAdapter

        // 获取数据
        fetchNews()
    }

    private fun fetchNews() {
        RetrofitClient.newsApi.getNews().enqueue(object : Callback<NewsBean> {
            override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) {
                if (response.isSuccessful) {
                    val newsBean = response.body()
                    newsBean?.let{
                        newsAdapter.updateData(it.news.shuffled())
                    }
                    Toasty.success(this@RecyclerViewDemoActivity, "Data fetched successfully", Toast.LENGTH_SHORT).show()
                }else{
                    Log.e("RecyclerViewDemoActivity", "Response not successful: ${response.code()}")
                }
            }

            override fun onFailure(call: Call<NewsBean>, t: Throwable) {
                Log.e("RecyclerViewDemoActivity", "Failure: ${t.message}")
            }
        })
    }
}
  • ViewHolder 模式
    • ViewHolder 用于缓存视图引用,避免在滚动过程中重复调用 findViewById
    • 显著提升列表的滚动性能
  • LayoutManager
    • 负责控制列表项的排列方式,常用的有三种:
      • LinearLayoutManager:线性布局,支持垂直或水平方向滚动
      • GridLayoutManager:网格布局,将列表项按行列排列
      • StaggeredGridLayoutManager:瀑布流布局,呈现错落有致的网格效果
  • Adapter 的三个核心方法
    • onCreateViewHolder:创建 ViewHolder 实例,加载列表项布局
    • onBindViewHolder:将指定位置的数据绑定到对应的 ViewHolder
    • getItemCount:返回数据集合的总数量

2.SwipeRefreshLayout

SwipeRefreshLayout 是 Google 官方提供的下拉刷新控件,允许用户通过下拉手势触发数据刷新操作(类似微信朋友圈的下拉刷新效果)。其主要特点如下:

  • 内置进度条动画(默认为圆形旋转样式)
  • 支持自定义刷新指示器的颜色和样式
  • 使用简单,非常适合搭配列表类控件(如 RecyclerView、ListView)实现下拉刷新
kotlin 复制代码
// 添加依赖
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

2.1 布局文件

activity_swipe_refresh.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".demo.newslist.SwipeRefreshActivity">

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="8dp"
            android:clipToPadding="false"/>
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</LinearLayout>

item_news.xml

xml 复制代码
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

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

        <ImageView
            android:id="@+id/newsImageView"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:layout_marginTop="8dp"
            android:scaleType="centerCrop" />

        <TextView
            android:id="@+id/contentTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:textSize="14sp" />
    </LinearLayout>
</androidx.cardview.widget.CardView>

2.2 Adapter 代码

kotlin 复制代码
package com.fhtt.todoapp.demo.newslist

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.fhtt.todoapp.databinding.ItemNewsBinding
import com.fhtt.todoapp.demo.net.New

class NewsAdapter(private val list: MutableList<New>) : RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {

    // 1. 创建 ViewHolder
    class NewsViewHolder(val binding : ItemNewsBinding) : RecyclerView.ViewHolder (binding.root)

    // 2. 重写 onCreateViewHolder、getItemCount、onBindViewHolder方法
    // 作用: 创建 ViewHolder 布局并返回
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
        val binding = ItemNewsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return NewsViewHolder(binding)
    }

    // 作用: 返回列表项数量
    override fun getItemCount(): Int {
        return list.size
    }

    // 作用: 绑定数据
    override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
        // 获取当前项数据
        val newItem = list[position]

        // 绑定数据
        holder.binding.titleTextView.text = newItem.title
        holder.binding.contentTextView.text = newItem.hint
        Glide.with(holder.itemView.context).load(newItem.image).into(holder.binding.newsImageView)
    }

    fun updateData(newList: List<New>) {
        list.clear()
        list.addAll(newList)
        notifyDataSetChanged()
    }
}

2.3 Activity 代码

kotlin 复制代码
class SwipeRefreshActivity : AppCompatActivity() {
    private lateinit var binding: ActivitySwipeRefreshBinding

    private val adapter by lazy {
        NewsAdapter(mutableListOf())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivitySwipeRefreshBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 设置RecyclerView的适配器
        binding.recyclerView.adapter = adapter

        // 设置下拉刷新监听器
        binding.swipeRefreshLayout.setOnRefreshListener {
            // 获取数据
            fetchNews()
        }

        // 设置刷新时的颜色方案
        binding.swipeRefreshLayout.setColorSchemeResources(
            R.color.red,
            R.color.blue,
            R.color.green,
            R.color.yellow,
            R.color.purple
        )

        // 获取数据
        fetchNews()
        // 刷新数据
        binding.swipeRefreshLayout.isRefreshing = true
    }

    // 网络请求成功或者失败,需要结束刷新
    private fun fetchNews() {
        lifecycleScope.launch {
            delay(2000)

            RetrofitClient.newsApi.getNews().enqueue(object : Callback<NewsBean> {
                override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) {
                    // 停止刷新
                    binding.swipeRefreshLayout.isRefreshing = false

                    if (response.isSuccessful) {
                        val newsBean = response.body()
                        newsBean?.let{
                            adapter.updateData(it.news.shuffled())
                        }
                        Toasty.success(this@SwipeRefreshActivity, "Data fetched successfully", Toast.LENGTH_SHORT).show()
                    }else{
                        Log.e("RecyclerViewDemoActivity", "Response not successful: ${response.code()}")
                    }
                }

                override fun onFailure(call: Call<NewsBean>, t: Throwable) {
                    // 停止刷新
                    binding.swipeRefreshLayout.isRefreshing = false

                    Log.e("RecyclerViewDemoActivity", "Failure: ${t.message}")
                }
            })
        }
    }
}

3.SmartRefreshLayout

SmartRefreshLayout 是一个功能丰富的 Android 智能下拉刷新框架,不仅支持下拉刷新,还内置了上拉加载更多等高级功能,是目前社区中广泛使用的刷新控件。

kotlin 复制代码
// 添加依赖
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")

3.1 布局文件

activity_smart_refresh_demo.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".demo.newslist.SmartRefreshDemoActivity">

    <com.scwang.smart.refresh.layout.SmartRefreshLayout
        android:id="@+id/smartRefresh"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.scwang.smart.refresh.header.ClassicsHeader
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <androidx.recyclerview.widget.RecyclerView
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <com.scwang.smart.refresh.footer.ClassicsFooter
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </com.scwang.smart.refresh.layout.SmartRefreshLayout>

</LinearLayout>

item_refresh_demo.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:id="@+id/titleText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/contentText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:textSize="14sp" />

    </LinearLayout>

</androidx.cardview.widget.CardView>

3.2 数据实体类

kotlin 复制代码
data class RefreshDemoItem(
    val title: String,
    val content: String
)

3.3 Adapter 代码

kotlin 复制代码
package com.fhtt.todoapp.demo.newslist

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.fhtt.todoapp.databinding.ItemRefreshDemoBinding

class RefreshDemoAdapter(private val list: MutableList<RefreshDemoItem>): RecyclerView.Adapter<RefreshDemoAdapter.NewsViewHolder>() {

    // 1. 创建ViewHolder
    class NewsViewHolder(val binding: ItemRefreshDemoBinding) :
        // 代码作用:将ItemRefreshDemoBinding绑定到NewsViewHolder,这是因为NewsViewHolder需要使用ItemRefreshDemoBinding来操作UI
        RecyclerView.ViewHolder(binding.root)

    // 2. 创建ViewHolder布局
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
        val binding = ItemRefreshDemoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return NewsViewHolder(binding)
    }

    // 3. 获取数据项数量
    override fun getItemCount(): Int {
        return list.size
    }

    // 4. 绑定数据
    override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
        val item = list[position]

        holder.binding.apply {
            titleText.text = item.title
            contentText.text = item.content
        }
    }

    // 5. 下拉刷新
    fun refreshData(newList: List<RefreshDemoItem>) {
        list.clear()
        list.addAll(newList)
        notifyDataSetChanged()
    }

    // 6. 上拉加载更多
    fun loadMoreData(newList: List<RefreshDemoItem>) {
        val olderSize = list.size
        list.addAll(newList)
        notifyItemRangeInserted(olderSize, newList.size)
    }

}

3.4 Activity 代码

kotlin 复制代码
package com.fhtt.todoapp.demo.newslist

import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.fhtt.todoapp.R
import com.fhtt.todoapp.databinding.ActivitySmartRefreshDemoBinding
import com.scwang.smart.refresh.layout.api.RefreshLayout
import com.scwang.smart.refresh.layout.listener.OnRefreshLoadMoreListener

class SmartRefreshDemoActivity : AppCompatActivity() {

    private val binding by lazy { ActivitySmartRefreshDemoBinding.inflate(layoutInflater) }
    private val adapter by lazy { RefreshDemoAdapter(mutableListOf()) }
    private var page = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(binding.root)

        binding.recyclerView.adapter = adapter

        // 设置下拉刷新和上拉加载更多的监听
        binding.smartRefresh.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
            override fun onRefresh(refreshLayout: RefreshLayout) {
                // 下拉刷新
                page = 1
                loadData(true)
            }

            override fun onLoadMore(refreshLayout: RefreshLayout) {
                // 上拉加载更多
                page++
                loadData(false)
            }

        })

        // 自动刷新
        binding.smartRefresh.autoRefresh()
    }

    // 加载数据
    fun loadData(isRefresh: Boolean){

        if(page > 3 && !isRefresh){
            binding.smartRefresh.finishLoadMoreWithNoMoreData()
            return
        }

        val newItems = generateData()
        if (isRefresh) {
            adapter.refreshData(newItems)
            binding.smartRefresh.finishRefresh()
        } else {
            adapter.loadMoreData(newItems)
            binding.smartRefresh.finishLoadMore()
        }
    }

    // 生成数据
    fun generateData() : List<RefreshDemoItem> {
        val itemCount = if(page < 3) 10 else 2

        return List(itemCount) { index ->
            var itemIndex = (page - 1) * 10 + index + 1
            RefreshDemoItem("Title $itemIndex", "Content $itemIndex")
        }
    }
}

结束下拉刷新:binding.refreshLayout.finishRefresh()

结束上拉加载更多:binding.refreshLayout.finishLoadMore()

标记没有更多数据:binding.refreshLayout.finishLoadMoreWithNoMoreData()

✨✨✨

相关推荐
三少爷的鞋1 小时前
Kotlin Flow 深入解析:`stateIn()` 的真正核心,其实是 SharingStarted
android
心中有国也有家13 小时前
AtomGit Flutter 鸿蒙客户端:ModalBottomSheet 实战
android·javascript·学习·flutter·华为·harmonyos
YSoup15 小时前
Android Stuidio中下载TRAE插件依赖的JCEF后打不开
android
2401_8949155315 小时前
Geo搜索优化排名源码部署搭建全流程详解
android·开发语言·flask·php·精选
星释16 小时前
鸿蒙智能体开发实战:34.鸿蒙壁纸大师 - 提示词工程与优化
android·华为·harmonyos·鸿蒙
blanks202017 小时前
android 编译问题记录
android
bobuddy17 小时前
平台总线(platform bus)
android
plainGeekDev18 小时前
libs 目录 → Gradle 依赖管理
android·java·kotlin
帅次19 小时前
Android 高级工程师面试:Flutter Widget 体系 近1年高频追问 20 题
android·flutter·面试·element·widget·setstate·renderobject