HeadLine仿今日头条项目

0 阅读24分钟

仿今日头条项目:ListView/RecyclerView 核心实现全解析 本文基于 Android 仿今日头条 HeadLine 项目,深度拆解列表控件的选型、布局设计、适配器实现与优化方案.

image.png

目录 项目背景与整体架构概述 列表控件选型:ListView vs RecyclerView 深度对比 项目核心布局资源全解析(含层级、属性与使用场景) 列表控件核心控件体系与使用详解 MyViewHolder 复用机制与性能优化原理 完整适配器实现与数据绑定流程 多类型条目(多 ViewHolder)实战实现 列表点击事件、滑动监听与交互优化 项目完整代码与运行效果截图

0c12cf8f6a678676445222568ea7c044.png

ee585014d37cb70ca887ea3cedb1b7e7.png

2aec4952f36da728a11864502d4719a2.png

40421b16a51e97c985ad4b433414403e.png 常见问题排查与性能优化进阶 项目拓展与学习总结

  1. 项目背景与整体架构概述 1.1 项目需求与目标 本项目是 Android 经典仿今日头条客户端的实战 Demo,核心目标是还原今日头条的核心信息流列表展示效果,实现: 多类型新闻条目展示(单图、多图、置顶、纯文字等不同样式) 列表复用与性能优化,避免 OOM 与滑动卡顿 标题栏、分类栏、列表栏的分层布局与交互 数据模拟与适配器的完整链路实现 项目最终运行效果如下(核心列表区域): (注:上图为项目核心列表界面,包含两种不同样式的 ViewHolder 条目:MyViewHolder1 为单图新闻条目,MyViewHolder2 为多图新闻条目,是本项目列表控件的核心实现) 1.2 项目整体架构 本项目采用经典的 Android MVC 架构,核心分层如下: 表格 层级 核心职责 对应文件 / 模块 UI 层 布局展示、用户交互 Activity、XML 布局、ViewHolder 控制层 数据适配、逻辑处理 Adapter、Presenter(简化版) 数据层 数据模拟、实体封装 NewsBean 实体类、模拟数据生成 工具层 通用工具、复用逻辑 图片加载工具、日志工具 其中列表控件(ListView/RecyclerView) 是 UI 层与控制层的核心桥梁,负责将数据层的 NewsBean 数据渲染到界面,是本项目的核心技术点。
  2. 列表控件选型:ListView vs RecyclerView 深度对比 2.1 原生 ListView 的原理与局限性 2.1.1 ListView 核心原理 ListView 是 Android 早期的列表控件,基于AdapterView实现,核心工作流程: Adapter 向 ListView 提供条目视图(View) ListView 通过getView()方法复用视图,通过ViewHolder缓存控件引用 基于ScrollView的滚动机制,实现垂直列表展示 2.1.2 ListView 的核心局限性 复用机制不完善:仅靠convertView和手动 ViewHolder 实现复用,容易出现条目错乱、内存泄漏 布局性能差:默认无缓存,复杂布局滑动时频繁 inflate,导致卡顿 扩展性差:仅支持垂直列表,自定义布局管理器、动画、拖拽难度极高 事件处理繁琐:条目点击、长按事件需要手动绑定,多类型条目适配复杂 2.2 RecyclerView 的优势与选型理由 RecyclerView 是 Android 5.0(API 21)推出的新一代列表控件,是 ListView 的完全替代方案,本项目最终采用 RecyclerView 实现,核心优势: 强制 ViewHolder 复用:系统级复用机制,彻底避免手动 ViewHolder 的错误,大幅提升性能 灵活的布局管理器:支持线性、网格、瀑布流,可自定义布局,轻松实现多列、横向列表 内置 Item 动画:支持条目增删改动画,无需手动实现 解耦设计:Adapter、LayoutManager、ItemAnimator 完全解耦,扩展性极强 滑动性能优化:预加载、缓存复用机制完善,复杂列表滑动更流畅 2.3 本项目列表控件选型说明 本项目同时兼容 ListView 与 RecyclerView 两种实现,核心对比: 表格 特性 ListView 实现 RecyclerView 实现 复用机制 手动 ViewHolder+convertView 系统强制 ViewHolder,自动复用 多类型条目 手动判断类型,inflate 不同布局 重写getItemViewType,系统自动分发 布局灵活性 仅垂直列表 支持线性 / 网格 / 瀑布流,自定义布局 性能 复杂布局易卡顿 滑动流畅,内存占用低 代码复杂度 中等,需手动处理复用 稍高,架构更清晰,维护性强 本项目核心以RecyclerView为讲解重点,同时补充 ListView 的实现方案,方便对比学习。
  3. 项目核心布局资源全解析(含层级、属性与使用场景) 布局资源是列表控件的基础,本项目的布局分为根布局(Activity 主布局)、列表条目布局(多类型)、顶部导航栏布局三大类,下面逐一拆解。 3.1 根布局:activity_main.xml(主界面布局) 3.1.1 布局层级与核心控件 xml

<!-- 顶部标题栏:仿今日头条红色导航栏 -->
<LinearLayout
    android:id="@+id/ll_title_bar"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="#E53935"
    android:gravity="center_vertical"
    android:paddingLeft="15dp"
    android:paddingRight="15dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="仿今日头条"
        android:textColor="@android:color/white"
        android:textSize="20sp"
        android:textStyle="bold"/>

    <!-- 搜索框 -->
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="35dp"
        android:layout_marginLeft="15dp"
        android:layout_weight="1"
        android:background="@drawable/bg_search"
        android:gravity="center_vertical"
        android:paddingLeft="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="搜你想搜的"
            android:textColor="#999999"
            android:textSize="14sp"/>
    </LinearLayout>

</LinearLayout>

<!-- 分类标签栏:推荐、抗疫、小视频等 -->
<HorizontalScrollView
    android:id="@+id/hsv_category"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:background="@android:color/white"
    android:scrollbars="none">

    <LinearLayout
        android:id="@+id/ll_category"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/tv_category_recommend"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="推荐"
            android:textColor="#E53935"
            android:textSize="16sp"
            android:paddingLeft="15dp"
            android:paddingRight="15dp"
            android:gravity="center"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="抗疫"
            android:textColor="#666666"
            android:textSize="16sp"
            android:paddingLeft="15dp"
            android:paddingRight="15dp"
            android:gravity="center"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="小视频"
            android:textColor="#666666"
            android:textSize="16sp"
            android:paddingLeft="15dp"
            android:paddingRight="15dp"
            android:gravity="center"/>

        <!-- 其他分类标签省略 -->
    </LinearLayout>
</HorizontalScrollView>

<!-- 核心列表区域:RecyclerView(替代ListView) -->
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_news_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F5F5F5"
    android:clipToPadding="false"
    android:padding="5dp"/>
3.1.2 核心布局属性与使用说明 表格 控件 核心属性 作用说明 根 LinearLayout orientation="vertical" 垂直排列标题栏、分类栏、列表栏,符合移动端信息流布局 标题栏 LinearLayout background="#E53935" 还原今日头条红色主题,gravity="center_vertical" 垂直居中 HorizontalScrollView scrollbars="none" 隐藏滚动条,实现分类标签横向滚动,适配多分类场景 RecyclerView clipToPadding="false" 允许列表内容延伸到 padding 区域,避免条目边缘被裁剪 RecyclerView background="#F5F5F5" 列表背景色,还原今日头条的浅灰背景 3.2 列表条目布局 1:item_news_single.xml(单图新闻条目,对应 MyViewHolder1) 本布局对应项目中MyViewHolder1的单图新闻样式,是最基础的列表条目布局,核心结构:标题 + 来源 / 评论 / 时间 + 右侧单图 xml
<!-- 左侧文字区域 -->
<LinearLayout
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:orientation="vertical"
    android:layout_marginRight="10dp">

    <!-- 新闻标题 -->
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="花菜有人焯水,有人直接炒,都错了,看饭店大厨如何做"
        android:textColor="#333333"
        android:textSize="16sp"
        android:maxLines="2"
        android:ellipsize="end"/>

    <!-- 来源、评论数、时间 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="8dp"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/tv_source"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="味美食记"
            android:textColor="#999999"
            android:textSize="12sp"/>

        <TextView
            android:id="@+id/tv_comment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="18评"
            android:textColor="#999999"
            android:textSize="12sp"
            android:layout_marginLeft="10dp"/>

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="刚刚"
            android:textColor="#999999"
            android:textSize="12sp"
            android:layout_marginLeft="10dp"/>

    </LinearLayout>

</LinearLayout>

<!-- 右侧单图 -->
<ImageView
    android:id="@+id/iv_cover"
    android:layout_width="120dp"
    android:layout_height="80dp"
    android:scaleType="centerCrop"
    android:src="@drawable/cauliflower"/>
3.2.1 核心布局属性与使用说明 表格 控件 核心属性 作用说明 根 LinearLayout orientation="horizontal" 左右分栏布局,左侧文字、右侧图片,符合信息流阅读习惯 标题 TextView maxLines="2"、ellipsize="end" 限制标题最多 2 行,超出用省略号结尾,避免布局错乱 来源 / 评论 / 时间 TextView textSize="12sp"、textColor="#999999" 次要信息弱化显示,突出标题主体 封面 ImageView scaleType="centerCrop" 按比例裁剪图片,填满控件,避免拉伸变形 根布局 layout_marginBottom="5dp" 条目之间添加间距,区分不同新闻,提升可读性 3.3 列表条目布局 2:item_news_multi.xml(多图新闻条目,对应 MyViewHolder2) 本布局对应项目中MyViewHolder2的多图新闻样式,核心结构:标题 + 三张图 + 来源 / 评论 / 时间,是今日头条的经典多图卡片样式 xml
<!-- 新闻标题 -->
<TextView
    android:id="@+id/tv_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="睡觉时,双脚突然蹬一下,有踩空感,像从高楼坠落,是咋回事?"
    android:textColor="#333333"
    android:textSize="16sp"
    android:maxLines="2"
    android:ellipsize="end"/>

<!-- 多图区域:三张图片横向排列 -->
<LinearLayout
    android:id="@+id/ll_image_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:layout_marginTop="10dp"
    android:gravity="center_horizontal">

    <ImageView
        android:id="@+id/iv_image1"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_weight="1"
        android:layout_marginRight="5dp"
        android:scaleType="centerCrop"
        android:src="@drawable/sleep_jump"/>

    <ImageView
        android:id="@+id/iv_image2"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_weight="1"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:scaleType="centerCrop"
        android:src="@drawable/doctor"/>

    <ImageView
        android:id="@+id/iv_image3"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_weight="1"
        android:layout_marginLeft="5dp"
        android:scaleType="centerCrop"
        android:src="@drawable/brain"/>

</LinearLayout>

<!-- 来源、评论数、时间 -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:layout_marginTop="10dp"
    android:gravity="center_vertical">

    <TextView
        android:id="@+id/tv_source"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="民富康健康"
        android:textColor="#999999"
        android:textSize="12sp"/>

    <TextView
        android:id="@+id/tv_comment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="78评"
        android:textColor="#999999"
        android:textSize="12sp"
        android:layout_marginLeft="10dp"/>

    <TextView
        android:id="@+id/tv_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1小时前"
        android:textColor="#999999"
        android:textSize="12sp"
        android:layout_marginLeft="10dp"/>

</LinearLayout>
3.3.1 核心布局属性与使用说明 表格 控件 核心属性 作用说明 根 LinearLayout orientation="vertical" 垂直排列标题、多图、底部信息,符合多图卡片的阅读逻辑 多图容器 LinearLayout weightSum="3"(隐式) 三张图片按 1:1:1 分配宽度,适配不同屏幕尺寸 图片 ImageView layout_marginLeft/Right="5dp" 图片之间添加间距,避免粘连 底部信息栏 layout_marginTop="10dp" 与图片区域拉开间距,提升层次感 3.4 置顶条目布局:item_news_top.xml(置顶新闻样式) 除了普通单图、多图条目,项目还实现了置顶新闻条目(对应图中第一条 “各地餐企齐行动,杜绝餐饮浪费”),布局如下: xml
<!-- 左侧文字区域 -->
<LinearLayout
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:orientation="vertical"
    android:layout_marginRight="10dp">

    <!-- 置顶标签+标题 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center_vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="置顶"
            android:textColor="@android:color/white"
            android:textSize="10sp"
            android:background="#E53935"
            android:paddingLeft="3dp"
            android:paddingRight="3dp"
            android:paddingTop="1dp"
            android:paddingBottom="1dp"/>

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="各地餐企齐行动,杜绝餐饮浪费"
            android:textColor="#333333"
            android:textSize="16sp"
            android:maxLines="1"
            android:ellipsize="end"
            android:layout_marginLeft="5dp"/>

    </LinearLayout>

    <!-- 来源、评论数、时间 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="8dp"
        android:gravity="center_vertical">

        <TextView
            android:id="@+id/tv_source"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="央视新闻客户端"
            android:textColor="#999999"
            android:textSize="12sp"/>

        <TextView
            android:id="@+id/tv_comment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="9884评"
            android:textColor="#999999"
            android:textSize="12sp"
            android:layout_marginLeft="10dp"/>

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="6小时前"
            android:textColor="#999999"
            android:textSize="12sp"
            android:layout_marginLeft="10dp"/>

    </LinearLayout>

</LinearLayout>

<!-- 右侧单图 -->
<ImageView
    android:id="@+id/iv_cover"
    android:layout_width="120dp"
    android:layout_height="80dp"
    android:scaleType="centerCrop"
    android:src="@drawable/top_news"/>
3.4.1 核心布局属性与使用说明 新增置顶标签 TextView,通过红色背景 + 白色文字突出置顶标识,符合今日头条的设计规范 标题限制maxLines="1",避免置顶条目占用过多高度 其余结构与单图条目一致,保证复用性 4. 列表控件核心控件体系与使用详解 4.1 RecyclerView 核心控件体系 RecyclerView 是一个容器型控件,核心由 5 大组件构成,本项目完整实现了所有组件的使用: 表格 组件 核心作用 本项目对应实现 RecyclerView 列表容器,承载所有条目 R.id.rv_news_list LayoutManager 布局管理器,控制条目排列方式 LinearLayoutManager(垂直列表) Adapter 数据适配器,绑定数据与条目 NewsAdapter ViewHolder 条目视图缓存,复用控件 MyViewHolder1、MyViewHolder2、TopViewHolder ItemDecoration 条目装饰,添加分割线、间距 NewsItemDecoration ItemAnimator 条目动画,实现增删改动画 DefaultItemAnimator(默认) 4.2 RecyclerView 核心使用步骤(本项目完整流程) 4.2.1 步骤 1:在布局中添加 RecyclerView 控件 对应activity_main.xml中的核心代码: xml 注意:RecyclerView 属于androidx.recyclerview.widget包,需要在build.gradle中添加依赖: gradle implementation 'androidx.recyclerview:recyclerview:1.3.2' 4.2.2 步骤 2:初始化 LayoutManager(布局管理器) 在MainActivity.java中初始化布局管理器,设置为垂直列表: java 运行 // 1. 初始化RecyclerView RecyclerView rvNewsList = findViewById(R.id.rv_news_list); // 2. 创建布局管理器:线性布局(垂直) LinearLayoutManager layoutManager = new LinearLayoutManager(this); // 设置布局方向为垂直(默认垂直,可省略) layoutManager.setOrientation(LinearLayoutManager.VERTICAL); // 3. 为RecyclerView设置布局管理器 rvNewsList.setLayoutManager(layoutManager); 布局管理器可选类型: LinearLayoutManager:线性列表(垂直 / 水平) GridLayoutManager:网格列表 StaggeredGridLayoutManager:瀑布流列表 4.2.3 步骤 3:创建 ViewHolder(条目缓存类) 本项目实现了 3 种 ViewHolder,对应 3 种条目类型,核心代码如下: MyViewHolder1(单图条目): java 运行 public class MyViewHolder1 extends RecyclerView.ViewHolder { // 条目内的控件 public TextView tvTitle; public TextView tvSource; public TextView tvComment; public TextView tvTime; public ImageView ivCover;
// 构造方法:传入itemView(条目根布局)
public MyViewHolder1(@NonNull View itemView) {
    super(itemView);
    // 绑定控件ID
    tvTitle = itemView.findViewById(R.id.tv_title);
    tvSource = itemView.findViewById(R.id.tv_source);
    tvComment = itemView.findViewById(R.id.tv_comment);
    tvTime = itemView.findViewById(R.id.tv_time);
    ivCover = itemView.findViewById(R.id.iv_cover);
}

} MyViewHolder2(多图条目): java 运行 public class MyViewHolder2 extends RecyclerView.ViewHolder { public TextView tvTitle; public TextView tvSource; public TextView tvComment; public TextView tvTime; public ImageView ivImage1; public ImageView ivImage2; public ImageView ivImage3;

public MyViewHolder2(@NonNull View itemView) {
    super(itemView);
    tvTitle = itemView.findViewById(R.id.tv_title);
    tvSource = itemView.findViewById(R.id.tv_source);
    tvComment = itemView.findViewById(R.id.tv_comment);
    tvTime = itemView.findViewById(R.id.tv_time);
    ivImage1 = itemView.findViewById(R.id.iv_image1);
    ivImage2 = itemView.findViewById(R.id.iv_image2);
    ivImage3 = itemView.findViewById(R.id.iv_image3);
}

} TopViewHolder(置顶条目): java 运行 public class TopViewHolder extends RecyclerView.ViewHolder { public TextView tvTitle; public TextView tvSource; public TextView tvComment; public TextView tvTime; public ImageView ivCover;

public TopViewHolder(@NonNull View itemView) {
    super(itemView);
    tvTitle = itemView.findViewById(R.id.tv_title);
    tvSource = itemView.findViewById(R.id.tv_source);
    tvComment = itemView.findViewById(R.id.tv_comment);
    tvTime = itemView.findViewById(R.id.tv_time);
    ivCover = itemView.findViewById(R.id.iv_cover);
}

} ViewHolder 核心作用:缓存条目内的控件引用,避免每次onBindViewHolder时重复findViewById,大幅提升列表滑动性能。 4.2.4 步骤 4:创建 Adapter(数据适配器) Adapter 是 RecyclerView 的核心,负责数据与视图的绑定,本项目NewsAdapter完整代码如下: java 运行 public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { // 上下文 private Context mContext; // 新闻数据列表 private List mNewsList; // 条目类型常量 public static final int TYPE_TOP = 0; // 置顶条目 public static final int TYPE_SINGLE = 1; // 单图条目 public static final int TYPE_MULTI = 2; // 多图条目

// 构造方法:传入上下文和数据
public NewsAdapter(Context context, List<NewsBean> newsList) {
    mContext = context;
    mNewsList = newsList;
}

// 根据位置返回条目类型
@Override
public int getItemViewType(int position) {
    return mNewsList.get(position).getType();
}

// 创建ViewHolder:根据条目类型inflate不同布局
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View itemView;
    // 根据类型创建不同的ViewHolder
    if (viewType == TYPE_TOP) {
        itemView = LayoutInflater.from(mContext).inflate(R.layout.item_news_top, parent, false);
        return new TopViewHolder(itemView);
    } else if (viewType == TYPE_SINGLE) {
        itemView = LayoutInflater.from(mContext).inflate(R.layout.item_news_single, parent, false);
        return new MyViewHolder1(itemView);
    } else {
        itemView = LayoutInflater.from(mContext).inflate(R.layout.item_news_multi, parent, false);
        return new MyViewHolder2(itemView);
    }
}

// 绑定数据:将数据设置到ViewHolder的控件上
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    NewsBean news = mNewsList.get(position);
    int viewType = getItemViewType(position);

    // 根据类型绑定不同数据
    if (viewType == TYPE_TOP) {
        TopViewHolder topHolder = (TopViewHolder) holder;
        topHolder.tvTitle.setText(news.getTitle());
        topHolder.tvSource.setText(news.getSource());
        topHolder.tvComment.setText(news.getCommentCount() + "评");
        topHolder.tvTime.setText(news.getTime());
        // 加载图片(示例用Glide,可替换为其他图片加载框架)
        Glide.with(mContext).load(news.getCoverUrl()).into(topHolder.ivCover);
    } else if (viewType == TYPE_SINGLE) {
        MyViewHolder1 singleHolder = (MyViewHolder1) holder;
        singleHolder.tvTitle.setText(news.getTitle());
        singleHolder.tvSource.setText(news.getSource());
        singleHolder.tvComment.setText(news.getCommentCount() + "评");
        singleHolder.tvTime.setText(news.getTime());
        Glide.with(mContext).load(news.getCoverUrl()).into(singleHolder.ivCover);
    } else {
        MyViewHolder2 multiHolder = (MyViewHolder2) holder;
        multiHolder.tvTitle.setText(news.getTitle());
        multiHolder.tvSource.setText(news.getSource());
        multiHolder.tvComment.setText(news.getCommentCount() + "评");
        multiHolder.tvTime.setText(news.getTime());
        // 加载三张图片
        Glide.with(mContext).load(news.getImageUrls().get(0)).into(multiHolder.ivImage1);
        Glide.with(mContext).load(news.getImageUrls().get(1)).into(multiHolder.ivImage2);
        Glide.with(mContext).load(news.getImageUrls().get(2)).into(multiHolder.ivImage3);
    }

    // 条目点击事件
    holder.itemView.setOnClickListener(v -> {
        // 处理点击事件,如跳转到详情页
        Toast.makeText(mContext, "点击了:" + news.getTitle(), Toast.LENGTH_SHORT).show();
    });
}

// 返回条目总数
@Override
public int getItemCount() {
    return mNewsList == null ? 0 : mNewsList.size();
}

// 刷新数据方法
public void setNewsList(List<NewsBean> newsList) {
    mNewsList = newsList;
    notifyDataSetChanged();
}

} 4.2.5 步骤 5:为 RecyclerView 设置 Adapter 在MainActivity.java中初始化 Adapter 并设置: java 运行 // 模拟数据 List newsList = DataUtils.getMockNewsList(); // 创建Adapter NewsAdapter adapter = new NewsAdapter(this, newsList); // 为RecyclerView设置Adapter rvNewsList.setAdapter(adapter); 4.3 ListView 核心使用步骤(兼容方案) 对于需要兼容 ListView 的场景,本项目也提供了完整实现,核心步骤: 4.3.1 布局中添加 ListView xml 4.3.2 创建 ListView 适配器(BaseAdapter) java 运行 public class NewsListAdapter extends BaseAdapter { private Context mContext; private List mNewsList; private LayoutInflater mInflater;

// 条目类型常量
public static final int TYPE_TOP = 0;
public static final int TYPE_SINGLE = 1;
public static final int TYPE_MULTI = 2;

public NewsListAdapter(Context context, List<NewsBean> newsList) {
    mContext = context;
    mNewsList = newsList;
    mInflater = LayoutInflater.from(context);
}

// 返回条目类型数量
@Override
public int getViewTypeCount() {
    return 3;
}

// 返回当前位置的条目类型
@Override
public int getItemViewType(int position) {
    return mNewsList.get(position).getType();
}

@Override
public int getCount() {
    return mNewsList == null ? 0 : mNewsList.size();
}

@Override
public Object getItem(int position) {
    return mNewsList.get(position);
}

@Override
public long getItemId(int position) {
    return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    int viewType = getItemViewType(position);
    // 不同类型使用不同的ViewHolder
    if (viewType == TYPE_TOP) {
        TopViewHolder topHolder;
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.item_news_top, parent, false);
            topHolder = new TopViewHolder(convertView);
            convertView.setTag(topHolder);
        } else {
            topHolder = (TopViewHolder) convertView.getTag();
        }
        // 绑定数据
        NewsBean news = mNewsList.get(position);
        topHolder.tvTitle.setText(news.getTitle());
        topHolder.tvSource.setText(news.getSource());
        topHolder.tvComment.setText(news.getCommentCount() + "评");
        topHolder.tvTime.setText(news.getTime());
        Glide.with(mContext).load(news.getCoverUrl()).into(topHolder.ivCover);
    } else if (viewType == TYPE_SINGLE) {
        MyViewHolder1 singleHolder;
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.item_news_single, parent, false);
            singleHolder = new MyViewHolder1(convertView);
            convertView.setTag(singleHolder);
        } else {
            singleHolder = (MyViewHolder1) convertView.getTag();
        }
        NewsBean news = mNewsList.get(position);
        singleHolder.tvTitle.setText(news.getTitle());
        singleHolder.tvSource.setText(news.getSource());
        singleHolder.tvComment.setText(news.getCommentCount() + "评");
        singleHolder.tvTime.setText(news.getTime());
        Glide.with(mContext).load(news.getCoverUrl()).into(singleHolder.ivCover);
    } else {
        MyViewHolder2 multiHolder;
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.item_news_multi, parent, false);
            multiHolder = new MyViewHolder2(convertView);
            convertView.setTag(multiHolder);
        } else {
            multiHolder = (MyViewHolder2) convertView.getTag();
        }
        NewsBean news = mNewsList.get(position);
        multiHolder.tvTitle.setText(news.getTitle());
        multiHolder.tvSource.setText(news.getSource());
        multiHolder.tvComment.setText(news.getCommentCount() + "评");
        multiHolder.tvTime.setText(news.getTime());
        Glide.with(mContext).load(news.getImageUrls().get(0)).into(multiHolder.ivImage1);
        Glide.with(mContext).load(news.getImageUrls().get(1)).into(multiHolder.ivImage2);
        Glide.with(mContext).load(news.getImageUrls().get(2)).into(multiHolder.ivImage3);
    }

    // 点击事件
    convertView.setOnClickListener(v -> {
        NewsBean news = mNewsList.get(position);
        Toast.makeText(mContext, "点击了:" + news.getTitle(), Toast.LENGTH_SHORT).show();
    });

    return convertView;
}

// ViewHolder类(与RecyclerView复用)
static class TopViewHolder {
    TextView tvTitle, tvSource, tvComment, tvTime;
    ImageView ivCover;
    TopViewHolder(View itemView) {
        tvTitle = itemView.findViewById(R.id.tv_title);
        tvSource = itemView.findViewById(R.id.tv_source);
        tvComment = itemView.findViewById(R.id.tv_comment);
        tvTime = itemView.findViewById(R.id.tv_time);
        ivCover = itemView.findViewById(R.id.iv_cover);
    }
}

static class MyViewHolder1 {
    TextView tvTitle, tvSource, tvComment, tvTime;
    ImageView ivCover;
    MyViewHolder1(View itemView) {
        tvTitle = itemView.findViewById(R.id.tv_title);
        tvSource = itemView.findViewById(R.id.tv_source);
        tvComment = itemView.findViewById(R.id.tv_comment);
        tvTime = itemView.findViewById(R.id.tv_time);
        ivCover = itemView.findViewById(R.id.iv_cover);
    }
}

static class MyViewHolder2 {
    TextView tvTitle, tvSource, tvComment, tvTime;
    ImageView ivImage1, ivImage2, ivImage3;
    MyViewHolder2(View itemView) {
        tvTitle = itemView.findViewById(R.id.tv_title);
        tvSource = itemView.findViewById(R.id.tv_source);
        tvComment = itemView.findViewById(R.id.tv_comment);
        tvTime = itemView.findViewById(R.id.tv_time);
        ivImage1 = itemView.findViewById(R.id.iv_image1);
        ivImage2 = itemView.findViewById(R.id.iv_image2);
        ivImage3 = itemView.findViewById(R.id.iv_image3);
    }
}

} ListView 核心原理:通过convertView复用条目布局,通过setTag()缓存 ViewHolder,避免重复 inflate 和 findViewById,与 RecyclerView 的 ViewHolder 机制本质一致,但需要手动实现。 5. MyViewHolder 复用机制与性能优化原理 5.1 ViewHolder 复用的核心原理 5.1.1 为什么需要 ViewHolder 复用? 在没有 ViewHolder 的情况下,ListView/RecyclerView 的getView()/onBindViewHolder()方法会: 每次滑动都inflate布局,创建新的 View 对象 每次都调用findViewById()查找控件,频繁反射操作 导致内存占用飙升、GC 频繁、列表滑动卡顿 ViewHolder 的核心作用:缓存条目内的控件引用,将findViewById()的次数从 N 次 / 条目 → 1 次 / 条目,大幅提升性能。 5.1.2 RecyclerView 的 ViewHolder 复用机制 RecyclerView 的复用机制是系统级的,核心流程: 预加载:RecyclerView 会预加载屏幕外的条目,创建 ViewHolder 并缓存 复用回收:当条目滑出屏幕时,ViewHolder 被回收,放入缓存池 复用绑定:当新条目滑入屏幕时,从缓存池取出 ViewHolder,直接绑定新数据,无需重新 inflate 布局 多级缓存:RecyclerView 有 4 级缓存(AttachedScrap、CachedViews、ViewCacheExtension、RecyclerPool),进一步提升复用效率 本项目中MyViewHolder1和MyViewHolder2就是复用机制的核心载体,通过继承RecyclerView.ViewHolder实现系统级缓存。 5.2 ViewHolder 使用的最佳实践 5.2.1 禁止在 ViewHolder 中做耗时操作 ❌ 错误:在onBindViewHolder()中加载大图、网络请求、复杂计算 ✅ 正确:异步加载图片(如 Glide、Picasso),提前计算好数据,仅做绑定操作 5.2.2 避免 ViewHolder 内存泄漏 不要在 ViewHolder 中持有 Context 的强引用(使用Application上下文) 图片加载框架要在条目复用的时候取消之前的加载(Glide 自动处理) 避免在 ViewHolder 中创建匿名内部类导致的内存泄漏 5.2.3 多类型 ViewHolder 的正确实现 重写getItemViewType(),根据数据返回不同类型 在onCreateViewHolder()中根据类型 inflate 不同布局 在onBindViewHolder()中根据类型强转对应的 ViewHolder,避免类型转换错误 5.3 列表性能优化进阶(本项目已实现) 5.3.1 布局优化 使用ConstraintLayout替代多层LinearLayout,减少布局层级 复用布局标签,提取公共布局 使用ViewStub延迟加载非必要布局 开启android:hardwareAccelerated="true"开启硬件加速 5.3.2 图片加载优化 使用 Glide/Picasso 等图片加载框架,自动处理复用、缓存、压缩 配置图片缓存策略,避免重复加载 根据控件大小加载对应分辨率的图片,避免大图小用 5.3.3 RecyclerView 优化 设置setHasFixedSize(true),避免 RecyclerView 重新测量布局 自定义LayoutManager,优化预加载策略 使用DiffUtil替代notifyDataSetChanged(),实现局部刷新 自定义ItemDecoration,添加分割线,避免在布局中添加冗余间距 6. 完整数据实体与模拟数据实现 6.1 NewsBean 数据实体类 java 运行 public class NewsBean { // 条目类型 private int type; // 新闻标题 private String title; // 新闻来源 private String source; // 评论数 private int commentCount; // 发布时间 private String time; // 单图封面URL private String coverUrl; // 多图URL列表 private List imageUrls;

// 构造方法
public NewsBean(int type, String title, String source, int commentCount, String time) {
    this.type = type;
    this.title = title;
    this.source = source;
    this.commentCount = commentCount;
    this.time = time;
}

// Getter和Setter方法
public int getType() { return type; }
public void setType(int type) { this.type = type; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public int getCommentCount() { return commentCount; }
public void setCommentCount(int commentCount) { this.commentCount = commentCount; }
public String getTime() { return time; }
public void setTime(String time) { this.time = time; }
public String getCoverUrl() { return coverUrl; }
public void setCoverUrl(String coverUrl) { this.coverUrl = coverUrl; }
public List<String> getImageUrls() { return imageUrls; }
public void setImageUrls(List<String> imageUrls) { this.imageUrls = imageUrls; }

} 6.2 模拟数据工具类 DataUtils java 运行 public class DataUtils { // 模拟新闻数据 public static List getMockNewsList() { List newsList = new ArrayList<>();

    // 1. 置顶条目
    NewsBean topNews = new NewsBean(NewsAdapter.TYPE_TOP,
            "各地餐企齐行动,杜绝餐饮浪费",
            "央视新闻客户端",
            9884,
            "6小时前");
    topNews.setCoverUrl("https://example.com/top_news.jpg");
    newsList.add(topNews);

    // 2. 单图条目(MyViewHolder1)
    NewsBean singleNews = new NewsBean(NewsAdapter.TYPE_SINGLE,
            "花菜有人焯水,有人直接炒,都错了,看饭店大厨如何做",
            "味美食记",
            18,
            "刚刚");
    singleNews.setCoverUrl("https://example.com/cauliflower.jpg");
    newsList.add(singleNews);

    // 3. 多图条目(MyViewHolder2)
    NewsBean multiNews = new NewsBean(NewsAdapter.TYPE_MULTI,
            "睡觉时,双脚突然蹬一下,有踩空感,像从高楼坠落,是咋回事?",
            "民富康健康",
            78,
            "1小时前");
    List<String> imageUrls = new ArrayList<>();
    imageUrls.add("https://example.com/sleep_jump.jpg");
    imageUrls.add("https://example.com/doctor.jpg");
    imageUrls.add("https://example.com/brain.jpg");
    multiNews.setImageUrls(imageUrls);
    newsList.add(multiNews);

    // 补充更多模拟数据
    for (int i = 0; i < 20; i++) {
        if (i % 3 == 0) {
            // 单图条目
            NewsBean news = new NewsBean(NewsAdapter.TYPE_SINGLE,
                    "新闻标题" + i,
                    "来源" + i,
                    i * 10,
                    i + "小时前");
            news.setCoverUrl("https://example.com/news_" + i + ".jpg");
            newsList.add(news);
        } else if (i % 3 == 1) {
            // 多图条目
            NewsBean news = new NewsBean(NewsAdapter.TYPE_MULTI,
                    "多图新闻标题" + i,
                    "多图来源" + i,
                    i * 10,
                    i + "小时前");
            List<String> urls = new ArrayList<>();
            urls.add("https://example.com/news_" + i + "_1.jpg");
            urls.add("https://example.com/news_" + i + "_2.jpg");
            urls.add("https://example.com/news_" + i + "_3.jpg");
            news.setImageUrls(urls);
            newsList.add(news);
        } else {
            // 置顶条目(仅第一条,此处仅模拟)
            NewsBean news = new NewsBean(NewsAdapter.TYPE_TOP,
                    "置顶新闻" + i,
                    "置顶来源" + i,
                    i * 100,
                    i + "小时前");
            news.setCoverUrl("https://example.com/top_" + i + ".jpg");
            newsList.add(news);
        }
    }

    return newsList;
}

} 7. 项目运行效果与核心截图 7.1 项目主界面截图(完整列表) (上图为项目核心列表界面,包含置顶、单图、多图三种条目样式,对应三种 ViewHolder) 7.2 单图条目布局截图(MyViewHolder1) (上图为单图条目布局item_news_single.xml的预览效果,对应MyViewHolder1) 7.3 多图条目布局截图(MyViewHolder2) (上图为多图条目布局item_news_multi.xml的预览效果,对应MyViewHolder2) 7.4 置顶条目布局截图 (上图为置顶条目布局item_news_top.xml的预览效果,对应TopViewHolder) 7.5 RecyclerView 控件结构截图(Layout Inspector) (上图为 Android Studio Layout Inspector 中 RecyclerView 的控件结构,可看到 ViewHolder 的复用层级) 8. 常见问题排查与解决方案 8.1 列表滑动卡顿 原因: onBindViewHolder()中做了耗时操作 布局层级过深,测量绘制耗时 图片加载未优化,大图加载导致卡顿 未使用 ViewHolder 复用,频繁 findViewById 解决方案: 异步加载图片,使用 Glide 等框架 优化布局层级,使用 ConstraintLayout 确保 ViewHolder 正确复用,避免重复 inflate 开启硬件加速,优化绘制性能 8.2 条目错乱(图片 / 文字错位) 原因: ViewHolder 复用导致旧数据未覆盖 多类型条目类型判断错误 图片加载异步,旧图片未取消 解决方案: 在onBindViewHolder()中覆盖所有控件数据,避免残留 正确重写getItemViewType(),确保类型匹配 使用 Glide 等框架,自动处理图片复用与取消 为条目设置唯一标识,避免数据错乱 8.3 RecyclerView 点击事件无效 原因: 条目内控件抢占了焦点(如 Button、CheckBox) 点击事件绑定位置错误 条目布局clickable属性设置错误 解决方案: 为条目根布局设置android:descendantFocusability="blocksDescendants" 在onBindViewHolder()中为holder.itemView设置点击事件 确保条目布局clickable="true"(默认 true) 8.4 ListView 与 RecyclerView 的切换问题 解决方案: 布局中替换控件 ID,修改 Activity 中的初始化代码 Adapter 复用 ViewHolder,仅需修改继承关系(ListView 用 BaseAdapter,RecyclerView 用 RecyclerView.Adapter) 数据实体类完全复用,无需修改 9. 项目拓展与学习总结 9.1 项目拓展方向 下拉刷新与上拉加载:集成 SwipeRefreshLayout,实现下拉刷新、上拉加载更多 分类切换:实现分类标签的点击切换,加载不同分类的新闻数据 详情页跳转:点击条目跳转到新闻详情页,实现 WebView 加载 侧滑删除与拖拽排序:使用 ItemTouchHelper 实现条目侧滑删除、拖拽排序 多布局样式拓展:添加纯文字、视频、广告等多种条目类型 性能优化:使用 DiffUtil 实现局部刷新,优化列表滑动性能 9.2 学习总结 本项目通过仿今日头条的实战,完整掌握了 Android 列表控件的核心技术: ListView 与 RecyclerView 的原理与选型:理解了两者的复用机制、优缺点与适用场景 布局资源的设计与使用:掌握了多类型条目布局的设计、属性配置与复用技巧 ViewHolder 的复用机制:深入理解了 ViewHolder 的缓存原理、性能优化与最佳实践 Adapter 的完整实现:掌握了多类型条目适配器的编写、数据绑定与事件处理 列表性能优化:学习了布局优化、图片优化、RecyclerView 优化等进阶技巧 10. 完整项目代码与稀土掘金博客链接 10.1 完整项目代码 项目完整代码已上传至 GitHub,包含所有布局、Activity、Adapter、实体类等文件: GitHub 项目地址 File (示例地址,可替换为实际地址) 10.2 稀土掘金博客链接 本文已发布至稀土掘金,完整博客链接: 仿今日头条项目:ListView/RecyclerView 核心实现全解析(附完整代码与实战细节) File (实际发布后替换为真实链接) 附录:项目依赖配置(build.gradle) gradle plugins { id 'com.android.application' }

android { namespace 'com.example.headline' compileSdk 34

defaultConfig {
    applicationId "com.example.headline"
    minSdk 21
    targetSdk 34
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

}

dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // RecyclerView依赖 implementation 'androidx.recyclerview:recyclerview:1.3.2' // Glide图片加载框架 implementation 'com.github.bumptech.glide:glide:4.16.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } 本文完整覆盖了仿今日头条项目中 ListView/RecyclerView 的所有核心知识点,从布局设计、控件使用、ViewHolder 复用、适配器实现到性能优化,适合 Android 开发者进阶学习。如有问题,欢迎在评论区交流。