仿今日头条 HeadLine 项目实战:ListView 与 RecyclerView 双列表核心技术全解析

1 阅读10分钟

一、项目介绍

1.1 项目概述

HeadLine 仿今日头条项目是一个典型的新闻资讯类 App 列表页面,实现了和今日头条一致的展示效果:

  • 置顶新闻条目(无图 + 置顶标签)
  • 单张图片新闻条目
  • 三张图片新闻条目
  • 每条新闻包含:标题、来源、评论数、发布时间

本项目核心使用 RecyclerView 实现多类型列表,是 Android 实际开发中最常用的业务场景。

1.2 项目技术栈

  • 开发语言:Java
  • 核心控件:RecyclerView(v7 支持包)
  • 列表样式:多布局(2 种 Item 类型)
  • 数据结构:实体类(NewsBean)+ 集合
  • 布局:LinearLayout、RelativeLayout
  • 适配器:RecyclerView.Adapter(多 ViewHolder 实现)

二、开发环境与依赖

2.1 开发工具

Android Studio

2.2 RecyclerView 依赖

build.gradle(Module) 中添加:

gradle

implementation 'com.android.support:recyclerview-v7:28.0.0'

2.3 资源准备

将图片资源放入 res/drawable

  • food、takeout、e_sports
  • sleep1、sleep2、sleep3、fruit1、fruit2、fruit3

image.png


三、项目核心结构

项目共 3 个核心 Java 文件 + 3 个布局文件:

表格

组件作用
MainActivity主页面,初始化数据、RecyclerView、适配器
NewsBean新闻实体类,封装一条新闻的所有数据
NewsAdapter多布局适配器,加载两种不同列表项
activity_main.xml主界面,放置 RecyclerView
list_item_one.xml单图 / 置顶新闻布局
list_item_two.xml三图新闻布局

image.png


四、实体类 NewsBean(数据封装)

4.1 作用

封装单条新闻的所有字段,让数据结构更清晰。

4.2 完整代码

java

运行

import java.util.List;
public class NewsBean {
    private int id;            
    private String title;      
    private List<Integer> imgList; 
    private String name;       
    private String comment;    
    private String time;       
    private int type;          

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getComment() {
        return comment;
    }
    public void setComment(String comment) {
        this.comment = comment;
    }
    public String getTime() {
        return time;
    }
    public void setTime(String time) {
        this.time = time;
    }
    public List<Integer> getImgList() {
        return imgList;
    }
    public void setImgList(List<Integer> imgList) {
        this.imgList = imgList;
    }
    public int getType() {
        return type;
    }
    public void setType(int type) {
        this.type = type;
    }
}

4.3 关键字段说明

  • type:标记新闻类型(1 = 单图 / 置顶,2 = 三图)
  • imgList:存放新闻图片(可装 1 张或 3 张)
  • title/name/comment/time:标题、来源、评论数、时间

五、主界面 MainActivity(数据 + 初始化)

5.1 核心功能

  • 定义新闻标题、来源、时间、图片等数组
  • 使用 setData() 封装新闻数据为 List<NewsBean>
  • 初始化 RecyclerView 与布局管理器
  • 绑定适配器

5.2 完整代码

java

运行

package cn.edu.headline;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private String[] titles = {
            "各地餐企齐行动,杜绝餐饮浪费",
            "花菜有人焯水,有人直接炒,都错了,看饭店大厨如何做",
            "睡觉时,双脚突然蹬一下,有踩空感,像从高楼坠落,是咋回事?",
            "实拍外卖小哥砸开小吃店的卷帘门救火,灭火后淡定继续送外卖",
            "还没成熟就被迫提前采摘,8毛一斤却没人要,果农无奈:不摘不行",
            "大会、大展、大赛一起来,北京电竞“好嗨哟”"
    };
    private String[] names = {"央视新闻客户端", "味美食记", "民富康健康", "生活小记", "禾木报告", "燕鸣"};
    private String[] comments = {"9884评", "18评", "78评", "678评", "189评", "304评"};
    private String[] times = {"6小时前", "刚刚", "1小时前", "2小时前", "3小时前", "4个小时前"};
    private int[] icons1 = {R.drawable.food, R.drawable.takeout, R.drawable.e_sports};
    private int[] icons2 = {R.drawable.sleep1, R.drawable.sleep2, R.drawable.sleep3, R.drawable.fruit1,R.drawable.fruit2, R.drawable.fruit3};
    private int[] types = {1, 1, 2, 1, 2, 1};

    private RecyclerView mRecyclerView;
    private NewsAdapter mAdapter;
    private List<NewsBean> NewsList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setData();
        mRecyclerView = findViewById(R.id.rv_list);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mAdapter = new NewsAdapter(MainActivity.this, NewsList);
        mRecyclerView.setAdapter(mAdapter);
    }

    private void setData() {
        NewsList = new ArrayList<>();
        NewsBean bean;
        for (int i = 0; i < titles.length; i++) {
            bean = new NewsBean();
            bean.setId(i + 1);
            bean.setTitle(titles[i]);
            bean.setName(names[i]);
            bean.setComment(comments[i]);
           .setTime(times[i]);
            bean.setType(types[i]);

            switch (i) {
                case 0:
                    List<Integer> imgList0 = new ArrayList<>();
                    bean.setImgList(imgList0);
                    break;
                case 1:
                    List<Integer> imgList1 = new ArrayList<>();
                    imgList1.add(icons1[i - 1]);
                    bean.setImgList(imgList1);
                    break;
                case 2:
                    List<Integer> imgList2 = new ArrayList<>();
                    imgList2.add(icons2[i - 2]);
                    imgList2.add(icons2[i - 1]);
                    imgList2.add(icons2[i]);
                    bean.setImgList(imgList2);
                    break;
                case 3:
                    List<Integer> imgList3 = new ArrayList<>();
                    imgList3.add(icons1[i - 2]);
                    bean.setImgList(imgList3);
                    break;
                case 4:
                    List<Integer> imgList4 = new ArrayList<>();
                    imgList4.add(icons2[i - 1]);
                    imgList4.add(icons2[i]);
                    imgList4.add(icons2[i + 1]);
                    bean.setImgList(imgList4);
                    break;
                case 5:
                    List<Integer> imgList5 = new ArrayList<>();
                    imgList5.add(icons1[i - 3]);
                    bean.setImgList(imgList5);
                    break;
            }
            NewsList.add(bean);
        }
    }
}

5.3 关键逻辑说明

  • types[]:标记每条新闻是单图还是三图
  • setData():循环封装新闻数据
  • 下面是示例截图
    147df736aed922e5d520dd2eee81ad9e.png

六、多布局适配器 NewsAdapter(核心)

6.1 适配器作用

根据新闻类型(type)加载两种不同布局,并绑定数据。

6.2 多布局实现思路

  1. 重写 getItemViewType() 返回类型
  2. onCreateViewHolder() 根据类型加载不同布局
  3. onBindViewHolder() 根据类型绑定不同数据

6.3 完整代码

java

运行

package cn.edu.headline;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private Context mContext;
    private List<NewsBean> NewsList;

    public NewsAdapter(Context context, List<NewsBean> NewsList) {
        this.mContext = context;
        this.NewsList = NewsList;
    }

    @Override
    public int getItemViewType(int position) {
        return NewsList.get(position).getType();
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView;
        if (viewType == 1) {
            itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_one, parent, false);
            return new MyViewHolder1(itemView);
        } else {
            itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_two, parent, false);
            return new MyViewHolder2(itemView);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        NewsBean bean = NewsList.get(position);
        if (holder instanceof MyViewHolder1) {
            MyViewHolder1 holder1 = (MyViewHolder1) holder;
            if (position == 0) {
                holder1.iv_top.setVisibility(View.VISIBLE);
                holder1.iv_img.setVisibility(View.GONE);
            } else {
                holder1.iv_top.setVisibility(View.GONE);
                holder1.iv_img.setVisibility(View.VISIBLE);
            }
            holder1.title.setText(bean.getTitle());
            holder1.name.setText(bean.getName());
            holder1.comment.setText(bean.getComment());
            holder1.time.setText(bean.getTime());
            if (bean.getImgList() == null || bean.getImgList().size() == 0) return;
            holder1.iv_img.setImageResource(bean.getImgList().get(0));
        } else if (holder instanceof MyViewHolder2) {
            MyViewHolder2 holder2 = (MyViewHolder2) holder;
            holder2.title.setText(bean.getTitle());
            holder2.name.setText(bean.getName());
            holder2.comment.setText(bean.getComment());
            holder2.time.setText(bean.getTime());
            holder2.iv_img1.setImageResource(bean.getImgList().get(0));
            holder2.iv_img2.setImageResource(bean.getImgList().get(1));
            holder2.iv_img3.setImageResource(bean.getImgList().get(2));
        }
    }

    @Override
    public int getItemCount() {
        return NewsList.size();
    }

    class MyViewHolder1 extends RecyclerView.ViewHolder {
        ImageView iv_top, iv_img;
        TextView title, name, comment, time;

        public MyViewHolder1(View view) {
            super(view);
            iv_top = view.findViewById(R.id.iv_top);
            iv_img = view.findViewById(R.id.iv_img);
            title = view.findViewById(R.id.tv_title);
            name = view.findViewById(R.id.tv_name);
            comment = view.findViewById(R.id.tv_comment);
            time = view.findViewById(R.id.tv_time);
        }
    }

    class MyViewHolder2 extends RecyclerView.ViewHolder {
        ImageView iv_img1, iv_img2, iv_img3;
        TextView title, name, comment, time;

        public MyViewHolder2(View view) {
            super(view);
            iv_img1 = view.findViewById(R.id.iv_img1);
            iv_img2 = view.findViewById(R.id.iv_img2);
            iv_img3 = view.findViewById(R.id.iv_img3);
            title = view.findViewById(R.id.tv_title);
            name = view.findViewById(R.id.tv_name);
            comment = view.findViewById(R.id.tv_comment);
            time = view.findViewById(R.id.tv_time);
        }
    }
}

七、布局文件全解析

7.1 主界面 activity_main.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

147df736aed922e5d520dd2eee81ad9e.png

7.2 单图 / 置顶条目 list_item_one.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <ImageView
        android:id="@+id/iv_top"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/top"
        android:visibility="gone"/>

    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentRight="true"/>

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_title"
        android:layout_toLeftOf="@id/iv_img"
        android:layout_marginTop="5dp">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"
            android:textColor="#888888"/>

        <TextView
            android:id="@+id/tv_comment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:textSize="12sp"
            android:textColor="#888888"/>

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:textSize="12sp"
            android:textColor="#888888"/>
    </LinearLayout>

</RelativeLayout>

image.png

7.3 三图条目 list_item_two.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

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

    <LinearLayout
        android:id="@+id/ll_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_title"
        android:layout_marginTop="5dp">

        <ImageView
            android:id="@+id/iv_img1"
            android:layout_width="0dp"
            android:layout_height="70dp"
            android:layout_weight="1"/>

        <ImageView
            android:id="@+id/iv_img2"
            android:layout_width="0dp"
            android:layout_height="70dp"
            android:layout_weight="1"
            android:layout_marginLeft="3dp"/>

        <ImageView
            android:id="@+id/iv_img3"
            android:layout_width="0dp"
            android:layout_height="70dp"
            android:layout_weight="1"
            android:layout_marginLeft="3dp"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/ll_img"
        android:layout_marginTop="5dp">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"
            android:textColor="#888888"/>

        <TextView
            android:id="@+id/tv_comment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:textSize="12sp"
            android:textColor="#888888"/>

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:textSize="12sp"
            android:textColor="#888888"/>
    </LinearLayout>

</RelativeLayout>

image.png


八、项目中所有控件与布局说明

8.1 布局类型

  • LinearLayout:垂直 / 水平排列控件,底部信息栏、三图横向排列
  • RelativeLayout:相对定位,实现左侧文字 + 右侧图片

8.2 所有控件及作用

表格

控件用途
RecyclerView展示多类型新闻列表
TextView标题、来源、评论数、时间
ImageView新闻图片、置顶标签
ViewHolder缓存控件,优化滑动

九、HeadLine 项目 RecyclerView VS 普通 ListView

表格

功能普通 ListViewHeadLine RecyclerView
布局样式单一布局支持多布局(单图 / 三图 / 置顶)
ViewHolder手动实现系统强制规范
性能一般流畅高效
扩展性强,支持动画、拖拽、刷新
企业使用极少主流

9.1 ListView 概述

ListView 是 Android 原生自带的垂直滚动列表控件,用于展示一组相同样式的数据列表。它的核心特点:

  • 不需要添加依赖
  • 用法简单
  • 采用 BaseAdapter 适配器
  • 支持垂直滚动
  • 需要手动实现 ViewHolder 优化
  • 适合展示单一布局、简单数据(如商品、文字、联系人)

在本阶段学习中,我们使用 ListView 实现了商品列表展示

  • 商品图片
  • 商品名称
  • 商品价格

9.2 ListView 项目核心代码

java

运行

package cn.edu.listview;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {
    private ListView mListView;
    //商品名称与价格数据集合
    private String[] titles = {"桌子", "苹果", "蛋糕", "线衣", "猕猴桃", "围巾"};
    private String[] prices = {"1800元", "10元/kg", "300元", "350元", "10元/kg", "280元"};
    //图片数据集合
    private int[] icons = {R.drawable.table, R.drawable.apple, R.drawable.cake,
            R.drawable.wireclothes, R.drawable.kiwifruit, R.drawable.scarf};

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mListView = findViewById(R.id.lv); //初始化ListView控件
        MyBaseAdapter mAdapter = new MyBaseAdapter(); //创建一个Adapter的实例
        mListView.setAdapter(mAdapter);                  //设置Adapter
    }

    class MyBaseAdapter extends BaseAdapter {
        @Override
        public int getCount() {   //获取条目的总数
            return titles.length; //返回条目的总数
        }

        @Override
        public Object getItem(int position) {
            return titles[position]; //返回条目的数据对象
        }

        @Override
        public long getItemId(int position) {
            return position; //返回条目的Id
        }
        
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = View.inflate(MainActivity.this, R.layout.list_item, null);
                holder = new ViewHolder();
                holder.title = convertView.findViewById(R.id.title);
                holder.price = convertView.findViewById(R.id.price);
                holder.iv = convertView.findViewById(R.id.iv);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.title.setText(titles[position]);
            holder.price.setText(prices[position]);
            holder.iv.setBackgroundResource(icons[position]);
            return convertView;
        }

        class ViewHolder {
            TextView title, price;
            ImageView iv;
        }
    }
}

9.3 ListView 布局文件

9.3.1 主界面布局 activity_main.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

9.3.2 列表项布局 list_item.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <ImageView
        android:id="@+id/iv"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentLeft="true"/>

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/iv"
        android:layout_marginLeft="10dp"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <TextView
        android:id="@+id/price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/iv"
        android:layout_below="@id/title"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:textSize="16sp"
        android:textColor="@android:color/holo_red_light"/>

</RelativeLayout>

image.png


9.4 ListView 四大核心方法

ListView 适配器必须重写以下 4 个方法:

表格

方法名作用
getCount()返回列表总条数
getItem()返回对应位置数据
getItemId()返回条目 ID
getView()渲染每个列表项(最核心)

所有列表的显示都依靠 getView () 完成。


9.5 ListView 优化:ViewHolder 原理(重点)

ListView 优化是 Android 基础必考知识点:

优化 1:convertView 复用

  • 屏幕外的条目会被系统回收
  • 滑回来时直接复用,不重新创建
  • 减少内存消耗

优化 2:ViewHolder 缓存控件

  • 避免每次都执行 findViewById(非常耗性能)
  • 把控件引用存到 ViewHolder 中
  • 通过 setTag /getTag 存取

java

运行

if (convertView == null) {
    // 创建布局 + 创建ViewHolder
} else {
    // 直接复用
    holder = (ViewHolder) convertView.getTag();
}

9.6 ListView 使用步骤(固定模板)

  1. 在 XML 中写 ListView
  2. 准备数据(数组 / 集合)
  3. 编写列表项布局
  4. 自定义 BaseAdapter
  5. 重写 4 个方法
  6. 使用 ViewHolder 优化
  7. 在 Activity 中设置适配器

9.7 ListView 和本项目 RecyclerView 全面对比(超级重点)

9.7.1 功能对比

表格

功能ListViewRecyclerView(头条项目使用)
布局样式只能垂直线性 / 网格 / 瀑布流
多布局非常麻烦简单支持(本项目 2 种布局)
ViewHolder手动写系统强制标准
性能一般
动画不支持支持
企业使用几乎不用主流
依赖系统自带需要添加依赖
分割线自带需要自定义
点击事件自带需要手动写

9.7.2 适用场景

  • ListView:学习用、简单列表、课程入门
  • RecyclerView:企业开发、新闻、商品、复杂列表(如本头条项目)

9.7.3 为什么头条项目要用 RecyclerView?

  1. 支持多布局(置顶、单图、三图)
  2. 滑动更流畅
  3. 便于扩展
  4. 符合行业标准
  5. ViewHolder 更规范