基于RecyclerView仿今日头条新闻列表

0 阅读1小时+

Android实战:基于RecyclerView仿今日头条新闻列表——从ListView到RecyclerView的完整进阶之路

写在前面:本文基于一个完整的Android教学项目(Chapter03),通过三个递进式项目——ListView基础商品列表、RecyclerView基础动物列表、RecyclerView进阶仿今日头条新闻列表——系统讲解Android列表开发的核心技术。文章将以"仿今日头条"HeadLine项目为主线,深入剖析RecyclerView的多类型Item实现、布局资源设计、控件使用技巧,同时对比ListView与RecyclerView的差异,帮助读者建立完整的Android列表开发知识体系。


目录


第一章 Android列表组件概述

1.1 为什么需要列表组件?

在移动应用开发中,列表是最常见、最基础的UI组件之一。无论是新闻资讯类APP(如今日头条、网易新闻)、社交类APP(如微信朋友圈、QQ空间)、电商类APP(如淘宝、京东的商品列表),还是系统设置页面,几乎每一个APP都离不开列表的身影。

列表组件的核心作用是:以垂直滚动的方式,高效地展示大量结构化的数据条目

想象一下,如果没有列表组件,我们需要手动管理每一个条目的创建、显示、隐藏和回收。当数据量达到几十条甚至上百条时,内存消耗会急剧增加,滑动性能会严重下降。而Android提供的列表组件,通过视图复用机制,只创建屏幕上可见的条目加上少量缓冲条目,当条目滚出屏幕时将其回收复用,从而实现了流畅的滚动体验。

1.2 Android列表组件的演进历程

Android平台上的列表组件经历了多次演进,主要可以分为以下几个阶段:

第一阶段:ListView时代(Android 1.0起)

ListView是Android最早期的列表组件,继承自AbsListView。它通过Adapter模式将数据与视图分离,引入了ViewHolder优化模式和convertView复用机制。ListView在很长一段时间内是Android列表开发的标准方案,几乎所有Android开发者都学习过ListView的使用。

第二阶段:RecyclerView时代(Android 5.0 / API 21起)

2014年,Google在Android 5.0中推出了RecyclerView,作为ListView的官方替代方案。RecyclerView不仅仅是一个简单的列表组件,它是一个高度可扩展的列表框架。与ListView相比,RecyclerView有以下显著优势:

  1. 强制ViewHolder模式:RecyclerView.Adapter强制要求使用ViewHolder,不再需要手动处理convertView的复用逻辑
  2. 灵活的布局管理:通过LayoutManager,可以轻松实现线性列表、网格列表、瀑布流等多种布局
  3. 内置动画支持:支持Item的增删动画,可以自定义动画效果
  4. 局部刷新能力:支持notifyItemChanged()等局部刷新方法,性能更优
  5. 装饰器机制:通过ItemDecoration可以方便地添加分割线、边距等装饰效果

第三阶段:现代列表组件(Jetpack Compose时代)

随着声明式UI的兴起,Jetpack Compose提供了LazyColumnLazyRow等现代列表组件,进一步简化了列表开发的复杂度。但在实际开发中,RecyclerView仍然是目前使用最广泛的列表组件。

1.3 本章教学项目的三个递进案例

本章涉及的Chapter03项目包含了三个递进式的教学案例,它们构成了一个完整的学习路径:

项目组件复杂度核心知识点
ListViewListView入门BaseAdapter、ViewHolder、convertView复用
RecyclerViewRecyclerView进阶RecyclerView.Adapter、LayoutManager、ViewHolder
HeadLineRecyclerView实战多类型Item、数据封装、真实业务场景

这三个项目的设计思路非常清晰:

  • ListView项目:用最简单的方式理解列表的基本工作原理
  • RecyclerView项目:掌握官方推荐组件的基础用法
  • HeadLine项目:在真实业务场景中应用RecyclerView的高级特性

接下来,我们将从ListView项目开始,逐步深入到HeadLine项目的每一个细节。


第二章 ListView基础入门——从购物商城项目说起

2.1 ListView项目概述

ListView项目位于Chapter03/ListView/目录下,是一个简单的购物商城列表Demo。它展示了6种商品(桌子、苹果、蛋糕、线衣、猕猴桃、围巾),每种商品包含名称、价格和图片三个信息。

虽然这个项目非常简单,但它涵盖了ListView开发的所有核心概念,是理解Android列表机制的绝佳起点。

2.2 ListView项目的代码结构

ListView/
├── app/
│   └── src/main/
│       ├── java/cn/edu/listview/
│       │   └── MainActivity.java          # 主Activity,包含ListView和自定义Adapter
│       └── res/
│           ├── layout/
│           │   ├── activity_main.xml      # 主页面布局
│           │   └── list_item.xml          # 列表Item布局
│           └── values/
│               ├── colors.xml             # 颜色资源
│               ├── strings.xml            # 字符串资源
│               └── styles.xml             # 样式资源

2.3 ListView的XML布局分析

2.3.1 主页面布局(activity_main.xml)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:text="购物商城"
        android:textSize="18sp"
        android:textColor="#FFFFFF"
        android:background="#FF8F03"
        android:gravity="center"/>
    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

这个布局文件使用了LinearLayout(线性布局)作为根布局,方向为垂直(vertical)。页面包含两个子元素:

  1. 标题栏TextView:高度45dp,橙色背景(#FF8F03),白色文字居中显示"购物商城"
  2. ListView控件:宽度填满父容器,高度自适应(wrap_content)

这里涉及到的关键属性说明:

  • android:layout_width="match_parent":宽度与父容器相同,即填满整个屏幕宽度
  • android:layout_height="wrap_content":高度根据内容自适应
  • android:orientation="vertical":子元素垂直排列
  • android:gravity="center":内容在控件内居中对齐

image.png

2.3.2 列表Item布局(list_item.xml)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">
    <ImageView
        android:id="@+id/iv"
        android:layout_width="120dp"
        android:layout_height="90dp"
        android:layout_centerVertical="true"/>
    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_toRightOf="@+id/iv"
        android:layout_centerVertical="true">
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="桌子"
            android:textSize="20sp"
            android:textColor="#000000" />
        <TextView
            android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="价格:"
            android:textSize="20sp"
            android:layout_marginTop="10dp"
            android:layout_below="@+id/title"
            android:textColor="#FF8F03" />
        <TextView
            android:id="@+id/price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="1000"
            android:textSize="20sp"
            android:layout_below="@+id/title"
            android:layout_toRightOf="@+id/tv_price"
            android:textColor="#FF8F03"
            android:layout_marginTop="10dp"/>
    </RelativeLayout>
</RelativeLayout>

这个Item布局使用了RelativeLayout(相对布局),这是ListView项目中非常经典的一种布局方式。相对布局允许子元素相对于父容器或其他兄弟元素进行定位。

布局结构分析:

  • 根布局:RelativeLayout,内边距16dp
  • 左侧图片:ImageView,固定尺寸120dp×90dp,垂直居中
  • 右侧信息区:嵌套的RelativeLayout,位于图片右侧(layout_toRightOf
    • 商品名称:title TextView,黑色20sp文字
    • 价格标签:tv_price TextView,橙色文字,位于标题下方
    • 价格数值:price TextView,橙色文字,位于标题下方、价格标签右侧

这里用到的RelativeLayout关键属性:

  • android:layout_toRightOf="@+id/iv":位于id为iv的控件右侧
  • android:layout_below="@+id/title":位于id为title的控件下方
  • android:layout_centerVertical="true":垂直方向居中

2.4 ListView的Java代码分析

2.4.1 MainActivity核心逻辑
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};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mListView = findViewById(R.id.lv);
        MyBaseAdapter mAdapter = new MyBaseAdapter();
        mListView.setAdapter(mAdapter);
    }
}

MainActivity的代码非常简洁,主要做了四件事:

  1. 定义三个平行数组(titles、prices、icons),分别存储商品名称、价格和图片资源ID
  2. 加载布局文件
  3. 找到ListView控件
  4. 创建并设置自定义适配器

这里使用平行数组来存储数据是一种简单但不推荐的做法。在实际开发中,应该使用实体类来封装数据(就像HeadLine项目中的NewsBean那样)。

2.4.2 自定义BaseAdapter

ListView项目的核心在于自定义适配器MyBaseAdapter,它继承自BaseAdapter,需要实现四个抽象方法:

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;
    }

    @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;
        TextView price;
        ImageView iv;
    }
}

四个核心方法详解:

  1. getCount():返回列表总条目数。ListView通过这个方法知道需要显示多少个Item。这里直接返回titles数组的长度,即6条。
  2. getItem(int position):返回指定位置的数据对象。在简单场景下返回数组元素即可。
  3. getItemId(int position):返回指定位置条目的唯一ID。通常直接返回position即可满足需求。
  4. getView(int position, View convertView, ViewGroup parent):这是最核心的方法,负责创建和配置每个列表Item的View。

getView方法的工作原理:

当ListView需要显示一个Item时,就会调用getView方法。这个方法接收三个参数:

  • position:当前Item在列表中的位置(从0开始)
  • convertView:已经滚出屏幕的旧Item View,可以拿来复用。如果为null,说明没有可复用的View
  • parent:父容器(即ListView本身)

convertView复用机制:

这是ListView性能优化的关键。当用户滚动列表时,滚出屏幕的Item View不会被销毁,而是被放入回收池中。当新的Item需要显示时,ListView会先从回收池中查找可用的convertView。如果有,就直接复用;如果没有(convertView为null),才创建新的View。

ViewHolder模式:

ViewHolder是一个内部类,用来缓存Item中的子控件引用。它的作用是避免每次调用getView时都执行findViewById。因为findViewById需要遍历整个View树来查找控件,是一个相对耗时的操作。通过ViewHolder,我们只在创建新View时执行一次findViewById,之后复用ViewHolder中缓存的引用。

ViewHolder的工作流程:

  1. 当convertView为null时,创建ViewHolder,执行findViewById,然后通过convertView.setTag(holder)将ViewHolder绑定到View上
  2. 当convertView不为null时,通过convertView.getTag()直接获取之前保存的ViewHolder
  3. 使用ViewHolder中的引用来设置数据

2.5 ListView的局限性

虽然ListView能够完成基本的列表展示任务,但它存在以下局限性:

  1. 需要手动处理视图复用:开发者必须自己判断convertView是否为null,自己管理ViewHolder,容易出错
  2. 布局方式单一:只能实现垂直列表,如果需要网格或瀑布流,需要换用GridView或StaggeredGridView
  3. 没有内置动画:Item的增删没有默认动画效果
  4. 刷新机制粗糙:只能通过notifyDataSetChanged()全局刷新,无法局部刷新
  5. 扩展性差:难以自定义分割线、Item装饰等

正是这些局限性,促使Google开发了RecyclerView来替代ListView。


第三章 RecyclerView基础进阶——从动物列表到新闻列表

3.1 RecyclerView项目概述

RecyclerView项目位于Chapter03/RecyclerView/目录下,是一个动物信息列表Demo。它展示了5种动物(小猫、哈士奇、小黄鸭、小鹿、老虎)的信息,每种动物包含名称、图片和介绍文字。

这个项目是ListView项目的升级版,展示了RecyclerView的基础用法。它为下一个HeadLine项目(多类型新闻列表)打下了基础。

3.2 RecyclerView项目的代码结构

RecyclerView/
├── app/
│   └── src/main/
│       ├── java/cn/edu/recyclerview/
│       │   └── MainActivity.java          # 主Activity,包含RecyclerView和内部Adapter
│       └── res/
│           ├── layout/
│           │   ├── activity_main.xml      # 主页面布局
│           │   └── recycler_item.xml      # 列表Item布局
│           └── values/
│               ├── colors.xml
│               ├── strings.xml
│               └── styles.xml

3.3 RecyclerView与ListView的关键区别

在深入代码之前,我们先理解RecyclerView与ListView的几个关键区别:

区别一:职责分离

ListView将大部分职责集中在一个组件中,而RecyclerView采用了职责分离的设计思想:

职责ListViewRecyclerView
视图创建与绑定Adapter的getView()onCreateViewHolder() + onBindViewHolder()
布局管理内置垂直布局LayoutManager(可替换)
视图复用手动处理convertView自动管理
动画效果ItemAnimator(可自定义)
装饰效果手动实现ItemDecoration(可自定义)

区别二:LayoutManager

RecyclerView通过LayoutManager来控制Item的布局方式,这是它最灵活的地方:

  • LinearLayoutManager:线性布局,实现垂直或水平列表
  • GridLayoutManager:网格布局,实现网格列表
  • StaggeredGridLayoutManager:瀑布流布局,实现不等高的网格列表

区别三:ViewHolder强制化

RecyclerView.Adapter的泛型参数就是ViewHolder类型,强制开发者使用ViewHolder模式。而ListView的Adapter中ViewHolder是可选的优化手段。

3.4 RecyclerView项目的代码分析

3.4.1 主页面布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/id_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </androidx.recyclerview.widget.RecyclerView>
</RelativeLayout>

主页面非常简单,只有一个RecyclerView控件,使用RelativeLayout作为根布局。注意RecyclerView的完整类名是androidx.recyclerview.widget.RecyclerView,这是AndroidX包中的组件。

image.png

3.4.2 Item布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:gravity="center"
    android:orientation="horizontal">
    <ImageView
        android:id="@+id/iv"
        android:layout_width="120dp"
        android:layout_height="90dp"
        android:src="@drawable/siberiankusky"/>
    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp">
        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:textColor="#FF8F03"
            android:text="哈士奇"/>
        <TextView
            android:id="@+id/introduce"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:layout_marginTop="10dp"
            android:layout_below="@+id/name"
            android:textColor="#FF716C6D"
            android:maxLines="2"
            android:ellipsize="end"
            android:text="西伯利亚雪橇犬,常见别名哈士奇,昵称为二哈。"/>
    </RelativeLayout>
</LinearLayout>

这个Item布局与ListView项目的布局类似,但有一些值得注意的细节:

  • 使用了android:maxLines="2"限制介绍文字最多显示2行
  • 使用了android:ellipsize="end"让超出部分显示省略号(...)
  • 这些属性在新闻类APP中非常常见,因为新闻标题和介绍通常都需要限制显示行数
3.4.3 MainActivity与HomeAdapter
public class MainActivity extends AppCompatActivity {
    private RecyclerView mRecyclerView;
    private HomeAdapter mAdapter;
    private String[] names = {"小猫", "哈士奇", "小黄鸭", "小鹿", "老虎"};
    private int[] icons = {R.drawable.cat, R.drawable.siberianhusky,
            R.drawable.yellowduck, R.drawable.fawn, R.drawable.tiger};
    private String[] introduces = {...};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = findViewById(R.id.id_recyclerview);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mAdapter = new HomeAdapter();
        mRecyclerView.setAdapter(mAdapter);
    }

    class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder> {
        @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            MyViewHolder holder = new MyViewHolder(LayoutInflater.from(MainActivity.this)
                .inflate(R.layout.recycler_item, parent, false));
            return holder;
        }

        @Override
        public void onBindViewHolder(MyViewHolder holder, int position) {
            holder.name.setText(names[position]);
            holder.iv.setImageResource(icons[position]);
            holder.introduce.setText(introduces[position]);
        }

        @Override
        public int getItemCount() {
            return names.length;
        }

        class MyViewHolder extends RecyclerView.ViewHolder {
            TextView name;
            ImageView iv;
            TextView introduce;

            public MyViewHolder(View view) {
                super(view);
                name = view.findViewById(R.id.name);
                iv = view.findViewById(R.id.iv);
                introduce = view.findViewById(R.id.introduce);
            }
        }
    }
}

RecyclerView.Adapter的三个核心方法:

  1. onCreateViewHolder(ViewGroup parent, int viewType):创建ViewHolder。当RecyclerView需要新的ViewHolder时调用这个方法。这里通过LayoutInflater加载recycler_item.xml布局,然后创建MyViewHolder实例。注意第三个参数false表示不立即将View添加到parent中,因为RecyclerView会在合适的时机自动添加。
  2. onBindViewHolder(MyViewHolder holder, int position):绑定数据。当ViewHolder需要显示数据时调用。这里直接通过holder中的控件引用设置数据,不需要再findViewById。
  3. getItemCount():返回Item总数,等同于ListView的getCount()。

与ListView的getView方法对比:

RecyclerView将ListView的getView方法拆分成了两个方法:

  • onCreateViewHolder ≈ getView中convertView == null的部分(创建View和ViewHolder)
  • onBindViewHolder ≈ getView中设置数据的部分

这种拆分的好处是职责更清晰,创建和绑定分离,代码更易读易维护。

3.5 从基础到实战的跨越

RecyclerView项目虽然比ListView项目更规范,但它仍然是一个简单的Demo——所有Item都是同一种布局。而真实的新闻APP(如今日头条)中,列表中的Item往往有多种不同的布局样式:有的新闻只有文字,有的新闻配一张图,有的新闻配三张图,还有的新闻是视频缩略图。

这就是HeadLine项目要解决的问题:如何在同一个RecyclerView中展示多种不同类型的Item。这也是从学习Demo到真实业务场景的关键跨越。


第四章 HeadLine项目整体架构分析

4.1 项目背景与目标

HeadLine项目位于Chapter03/HeadLine/目录下,是一个模拟今日头条新闻列表的实战项目。它展示了6条不同类型的新闻,包括:

  • 置顶无图新闻(第一条)
  • 单图新闻(第二、四、六条)
  • 三图新闻(第三、五条)

这个项目的核心价值在于:

  1. 多类型Item实现:同一个列表中混合展示不同布局的Item
  2. 面向对象数据封装:使用NewsBean实体类替代零散数组
  3. 真实业务场景模拟:模拟了新闻APP的常见数据结构和展示逻辑

4.2 三个项目的递进关系

理解HeadLine项目的最佳方式是将其与前两个项目对比:

维度ListView项目RecyclerView项目HeadLine项目
列表组件ListViewRecyclerViewRecyclerView
数据类型平行数组平行数组NewsBean实体类
Item类型单一类型单一类型多类型(3种)
布局文件1个Item布局1个Item布局2个Item布局
ViewHolder手动实现1个ViewHolder2个ViewHolder
业务场景购物商城动物信息新闻资讯
代码行数~136行~132行~321行(3个文件)

从这三个项目的对比可以清晰地看到Android列表开发的演进路径:

  • 组件演进:ListView → RecyclerView
  • 数据演进:平行数组 → 实体类封装
  • 布局演进:单一布局 → 多类型布局
  • 场景演进:教学Demo → 真实业务

4.3 HeadLine项目的文件结构

HeadLine/
├── app/
│   ├── build.gradle                          # 模块级构建配置
│   └── src/main/
│       ├── AndroidManifest.xml               # 应用清单文件
│       ├── java/cn/edu/headline/
│       │   ├── MainActivity.java             # 主Activity(137行)
│       │   ├── NewsAdapter.java              # 多类型适配器(184行)
│       │   └── NewsBean.java                 # 新闻实体类(136行)
│       └── res/
│           ├── layout/
│           │   ├── activity_main.xml         # 主页面布局(73行)
│           │   ├── list_item_one.xml         # 单图/无图Item布局(73行)
│           │   ├── list_item_two.xml         # 三图Item布局(74行)
│           │   └── title_bar.xml             # 公共标题栏布局(35行)
│           ├── drawable-hdpi/
│           │   ├── food.png                  # 美食图片
│           │   ├── takeout.png               # 外卖图片
│           │   ├── e_sports.png              # 电竞图片
│           │   ├── sleep1.png                # 睡眠相关图片1
│           │   ├── sleep2.png                # 睡眠相关图片2
│           │   ├── sleep3.png                # 睡眠相关图片3
│           │   ├── fruit1.png                # 水果图片1
│           │   ├── fruit2.png                # 水果图片2
│           │   ├── fruit3.png                # 水果图片3
│           │   ├── top.png                   # 置顶标签图标
│           │   └── search_bg.png             # 搜索框背景
│           ├── drawable/
│           │   └── ic_launcher_background.xml
│           ├── drawable-v24/
│           │   └── ic_launcher_foreground.xml
│           ├── mipmap-anydpi-v26/
│           │   ├── ic_launcher.xml
│           │   └── ic_launcher_round.xml
│           └── values/
│               ├── colors.xml                # 颜色定义
│               ├── strings.xml               # 字符串定义
│               └── styles.xml                # 样式定义

4.4 项目技术栈

HeadLine项目使用的核心技术包括:

  • AndroidX:使用AndroidX兼容库(androidx.recyclerview.widget.RecyclerView
  • RecyclerView:官方推荐的列表组件
  • LinearLayoutManager:线性布局管理器
  • 多类型Item:通过getItemViewType()实现
  • 面向对象设计:NewsBean实体类封装数据
  • Style复用:通过styles.xml定义公共样式
  • 布局复用:通过<include>标签复用标题栏

4.5 项目运行效果描述

运行HeadLine项目后,用户将看到一个仿今日头条的新闻列表页面:

  1. 顶部标题栏:红色背景,左侧显示"仿今日头条"文字,右侧有一个灰色圆角搜索框
  2. 分类标签栏:白色背景,水平排列多个分类标签(推荐、抗疫、小视频、北京、视频、热点、娱乐),其中"推荐"标签为红色高亮
  3. 新闻列表:浅灰色背景,包含6条新闻:
    • 第一条:置顶新闻,无图片,左侧显示标题和来源信息,左上角有"置顶"标签
    • 第二条:单图新闻,左侧文字+右侧一张图片
    • 第三条:三图新闻,标题在上方,下方三张等宽图片,底部显示来源信息
    • 第四条:单图新闻(同第二条样式)
    • 第五条:三图新闻(同第三条样式)
    • 第六条:单图新闻(同第二条样式)

image.png


第五章 项目工程结构与Gradle配置

5.1 Gradle构建系统简介

Gradle是Android项目的构建工具,负责编译代码、打包资源、生成APK等任务。一个标准的Android Gradle项目包含以下关键文件:

  • 根目录build.gradle:项目级配置,定义所有模块共用的配置(如仓库地址、插件版本)
  • 模块build.gradle:模块级配置,定义当前模块的编译参数、依赖库等
  • settings.gradle:定义项目中包含哪些模块
  • gradle.properties:Gradle的全局属性配置
  • gradle/wrapper/:Gradle Wrapper相关文件,确保项目使用指定版本的Gradle

5.2 根目录build.gradle

// 模块级配置,全局配置已放在根目录build.gradle文件中

HeadLine项目的根目录build.gradle内容为空(只有一行注释),说明全局配置可能在更上层的Chapter03根目录中。

5.3 模块级build.gradle详解

apply plugin: 'com.android.application'

android {
    namespace 'cn.edu.headline'
    compileSdk 34
    defaultConfig {
        applicationId "cn.edu.headline"
        minSdkVersion 21
        targetSdkVersion 34
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test:runner:1.5.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
}

逐项解析:

apply plugin: 'com.android.application' 声明这是一个Android应用模块(相对于库模块com.android.library)。应用模块可以独立运行,生成APK文件。

namespace 'cn.edu.headline' 定义应用的包名(命名空间)。这是Android Gradle Plugin 7.0+引入的新写法,替代了之前在defaultConfig中通过applicationId定义包名的方式。

compileSdk 34 指定编译时使用的Android SDK版本为34(Android 14)。这意味着可以使用Android 14的所有API。

minSdkVersion 21 指定应用支持的最低Android版本为API 21(Android 5.0 Lollipop)。低于此版本的设备无法安装此应用。选择API 21作为最低版本是一个合理的选择,因为:

  • Android 5.0覆盖了绝大多数活跃设备
  • 可以使用RecyclerView、Material Design等现代API
  • 不需要兼容过时的Android 4.x版本

targetSdkVersion 34 指定应用的目标SDK版本为34。这告诉Android系统:"我已经针对Android 14进行了测试和优化"。系统会根据这个值启用相应的兼容性行为。

versionCode 1versionName "1.0"

  • versionCode:内部版本号,整数,每次发布新版本必须递增
  • versionName:展示给用户的版本号,字符串,如"1.0"、"2.1.0"

dependencies依赖项详解:

依赖版本作用
fileTree-引入libs目录下的所有.jar文件
appcompat1.6.1AndroidX兼容库,提供AppCompatActivity等组件
constraintlayout2.1.4约束布局库(虽然本项目未直接使用)
junit4.13.2单元测试框架
runner1.5.2Android测试运行器
espresso-core3.5.1UI自动化测试框架
recyclerview1.3.2RecyclerView组件库,本项目核心依赖

其中androidx.recyclerview:recyclerview:1.3.2是本项目最重要的依赖,没有它就无法使用RecyclerView组件。

5.4 settings.gradle

// 通常包含 include ':app'

settings.gradle文件定义了项目中包含的模块。对于HeadLine这样的单模块项目,通常只有一行include ':app'

5.5 gradle.properties

# AndroidX迁移标志
android.useAndroidX=true
android.enableJetifier=true

这两个属性非常重要:

  • android.useAndroidX=true:启用AndroidX,使用新的androidx包替代旧的android.support包
  • android.enableJetifier=true:自动将第三方库中的support包引用迁移到androidx包

5.6 AndroidManifest.xml详解

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.edu.headline">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.NoActionBar">

        <activity android:name="cn.edu.headline.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

关键配置说明:

  • android:allowBackup="true":允许应用数据备份
  • android:icon:应用图标(方形)
  • android:roundIcon:应用图标(圆形,用于圆形启动器)
  • android:label:应用名称,引用strings.xml中的app_name
  • android:supportsRtl="true":支持从右到左的布局(如阿拉伯语)
  • android:theme:应用主题,使用NoActionBar隐藏系统默认ActionBar,因为项目自定义了标题栏

Activity声明:

  • android:name:Activity的完整类名
  • android:exported="true":允许其他应用启动此Activity(因为它是LAUNCHER)
  • intent-filter:声明此Activity为应用入口
    • MAIN action:主入口
    • LAUNCHER category:显示在桌面应用列表中

第六章 布局资源深度解析

6.1 Android布局系统概述

Android的UI界面由View和ViewGroup组成。View是所有UI组件的基类(如TextView、ImageView、Button等),ViewGroup是View的容器,可以包含多个子View(如LinearLayout、RelativeLayout等)。

布局资源文件(XML)定义了UI的结构和外观。Android提供了多种布局类型,每种布局有不同的子元素排列方式。

6.2 主页面布局(activity_main.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:background="@color/light_gray_color"
    android:orientation="vertical">

    <!-- 引入公共标题栏布局 -->
    <include layout="@layout/title_bar" />

    <!-- 新闻分类标签栏 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@android:color/white"
        android:orientation="horizontal">

        <TextView style="@style/tvStyle" android:text="推荐"
            android:textColor="@android:color/holo_red_dark" />
        <TextView style="@style/tvStyle" android:text="抗疫"
            android:textColor="@color/gray_color" />
        <TextView style="@style/tvStyle" android:text="小视频"
            android:textColor="@color/gray_color" />
        <TextView style="@style/tvStyle" android:text="北京"
            android:textColor="@color/gray_color" />
        <TextView style="@style/tvStyle" android:text="视频"
            android:textColor="@color/gray_color" />
        <TextView style="@style/tvStyle" android:text="热点"
            android:textColor="@color/gray_color" />
        <TextView style="@style/tvStyle" android:text="娱乐"
            android:textColor="@color/gray_color" />
    </LinearLayout>

    <!-- 分割线 -->
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#eeeeee" />

    <!-- 新闻列表RecyclerView -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

布局层次结构:

LinearLayout (垂直方向, 根布局)
├── include (title_bar.xml)          ← 公共标题栏
├── LinearLayout (水平方向)           ← 分类标签栏
│   ├── TextView "推荐" (红色高亮)
│   ├── TextView "抗疫"
│   ├── TextView "小视频"
│   ├── TextView "北京"
│   ├── TextView "视频"
│   ├── TextView "热点"
│   └── TextView "娱乐"
├── View (1dp分割线)
└── RecyclerView (rv_list)           ← 新闻列表

逐层解析:

第一层:根布局LinearLayout

  • android:orientation="vertical":子元素垂直排列
  • android:background="@color/light_gray_color":浅灰色背景(#eeeeee),这是新闻类APP常用的背景色,可以让白色卡片更突出
  • android:layout_width/height="match_parent":填满整个屏幕

第二层:标题栏(include标签)

  • <include layout="@layout/title_bar" />:引入外部布局文件
  • 这是Android布局复用的重要机制,避免在多个页面中重复编写相同的标题栏代码

第三层:分类标签栏

  • 水平LinearLayout,高度40dp,白色背景
  • 包含7个TextView,每个代表一个新闻分类
  • 所有TextView共用@style/tvStyle样式
  • "推荐"标签使用红色文字(holo_red_dark),表示当前选中状态
  • 其他标签使用灰色文字(gray_color),表示未选中状态

第四层:分割线

  • 一个普通的View控件,高度1dp,浅灰色背景
  • 用于分隔分类标签栏和新闻列表,增加视觉层次感

第五层:RecyclerView

  • android:id="@+id/rv_list":控件ID,在Java代码中通过此ID找到控件
  • android:layout_width/height="match_parent":填满剩余空间
  • 这是页面的核心列表控件,所有新闻Item都在这里展示

6.3 标题栏布局(title_bar.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="50dp"
    android:background="#d33d3c"
    android:orientation="horizontal"
    android:paddingLeft="10dp"
    android:paddingRight="10dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="仿今日头条"
        android:textColor="@android:color/white"
        android:textSize="22sp" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="35dp"
        android:layout_gravity="center_vertical"
        android:layout_marginStart="15dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="15dp"
        android:background="@drawable/search_bg"
        android:gravity="center_vertical"
        android:textColor="@android:color/black"
        android:hint="搜你想搜的"
        android:textColorHint="@color/gray_color"
        android:textSize="14sp"
        android:paddingLeft="30dp" />
</LinearLayout>

布局分析:

标题栏使用了水平LinearLayout,包含两个子元素:

  1. APP名称TextView
    • android:layout_gravity="center":在父容器中垂直居中
    • android:text="仿今日头条":显示文字
    • android:textColor="@android:color/white":白色文字
    • android:textSize="22sp":较大字号,突出品牌
  2. 搜索框EditText
    • android:layout_width="match_parent":占据剩余所有宽度
    • android:layout_height="35dp":固定高度
    • android:background="@drawable/search_bg":圆角灰色背景(这是一个PNG图片资源)
    • android:hint="搜你想搜的":提示文字,输入内容后消失
    • android:textColorHint="@color/gray_color":提示文字颜色为灰色
    • android:paddingLeft="30dp":左侧内边距,为搜索图标留出空间

关于<include>标签:

<include>是Android布局复用的重要机制。它允许将一个布局文件嵌入到另一个布局文件中。使用<include>的好处:

  • 代码复用:多个页面可以共用同一个标题栏布局
  • 维护方便:修改标题栏只需要修改一个文件
  • 减少冗余:避免在多个布局文件中重复相同的代码

6.4 单图/无图Item布局(list_item_one.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="90dp"
    android:layout_marginBottom="8dp"
    android:background="@android:color/white"
    android:padding="8dp">

    <!-- 左侧文本信息容器 -->
    <LinearLayout
        android:id="@+id/ll_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- 新闻标题 -->
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="280dp"
            android:layout_height="wrap_content"
            android:maxLines="2"
            android:textColor="#3c3c3c"
            android:textSize="16sp" />

        <!-- 底部信息栏 -->
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <!-- 置顶标签图标 -->
            <ImageView
                android:id="@+id/iv_top"
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:layout_alignParentBottom="true"
                android:src="@drawable/top" />

            <!-- 来源/评论/时间信息 -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:layout_toRightOf="@id/iv_top"
                android:orientation="horizontal">

                <TextView android:id="@+id/tv_name" style="@style/tvInfo" />
                <TextView android:id="@+id/tv_comment" style="@style/tvInfo" />
                <TextView android:id="@+id/tv_time" style="@style/tvInfo" />
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>

    <!-- 右侧新闻图片 -->
    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="90dp"
        android:layout_toRightOf="@id/ll_info"
        android:padding="3dp" />
</RelativeLayout>

布局层次结构:

RelativeLayout (根布局, 90dp高, 白色背景)
├── LinearLayout (ll_info, 左侧文本区, 垂直方向)
│   ├── TextView (tv_title, 标题, 最多2行)
│   └── RelativeLayout (底部信息栏)
│       ├── ImageView (iv_top, 置顶标签)
│       └── LinearLayout (水平方向)
│           ├── TextView (tv_name, 来源)
│           ├── TextView (tv_comment, 评论数)
│           └── TextView (tv_time, 发布时间)
└── ImageView (iv_img, 右侧新闻图片)

关键设计点:

  1. 固定高度90dp:单图Item的高度固定为90dp,保证列表的整齐统一
  2. 底部间距8dpandroid:layout_marginBottom="8dp"让每条新闻之间有适当的间隔
  3. 白色背景:与页面的浅灰色背景形成对比,产生卡片效果
  4. 标题宽度280dp:限制标题宽度,为右侧图片留出空间
  5. 标题最多2行android:maxLines="2"防止标题过长影响布局
  6. 置顶标签iv_top默认显示,在Java代码中根据position控制显示/隐藏
  7. 图片在右侧android:layout_toRightOf="@id/ll_info"让图片位于文本右侧

6.5 三图Item布局(list_item_two.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:layout_marginBottom="8dp"
    android:background="@android:color/white">

    <!-- 新闻标题 -->
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxLines="2"
        android:padding="8dp"
        android:textColor="#3c3c3c"
        android:textSize="16sp" />

    <!-- 三张图片容器 -->
    <LinearLayout
        android:id="@+id/ll_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_title"
        android:orientation="horizontal">

        <ImageView android:id="@+id/iv_img1" style="@style/ivImg"/>
        <ImageView android:id="@+id/iv_img2" style="@style/ivImg"/>
        <ImageView android:id="@+id/iv_img3" style="@style/ivImg"/>
    </LinearLayout>

    <!-- 底部信息 -->
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/ll_img"
        android:orientation="vertical"
        android:padding="8dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView android:id="@+id/tv_name" style="@style/tvInfo" />
            <TextView android:id="@+id/tv_comment" style="@style/tvInfo" />
            <TextView android:id="@+id/tv_time" style="@style/tvInfo" />
        </LinearLayout>
    </LinearLayout>
</RelativeLayout>

布局层次结构:

RelativeLayout (根布局, 高度自适应, 白色背景)
├── TextView (tv_title, 标题, 顶部)
├── LinearLayout (ll_img, 三张图片, 水平方向, 标题下方)
│   ├── ImageView (iv_img1, 图片1)
│   ├── ImageView (iv_img2, 图片2)
│   └── ImageView (iv_img3, 图片3)
└── LinearLayout (底部信息区, 图片下方)
    └── LinearLayout (水平方向)
        ├── TextView (tv_name, 来源)
        ├── TextView (tv_comment, 评论数)
        └── TextView (tv_time, 发布时间)

与单图布局的对比:

特性单图布局(list_item_one)三图布局(list_item_two)
高度固定90dp自适应(wrap_content)
标题位置左侧顶部,全宽
图片位置右侧,单张标题下方,三张水平排列
底部信息位置标题下方左侧图片下方
置顶标签有(iv_top)
图片样式直接设置宽高使用@style/ivImg样式

三张图片的等宽实现:

三张图片通过@style/ivImg样式实现等宽分布:

<style name="ivImg">
    <item name="android:layout_width">0dp</item>
    <item name="android:layout_height">90dp</item>
    <item name="android:layout_weight">1</item>
</style>

这里使用了LinearLayout的layout_weight属性来实现等宽分布:

  • layout_width="0dp":宽度设为0,让weight来决定宽度
  • layout_weight="1":每个图片占据相同的权重比例
  • 三个图片的weight都是1,所以它们会平分父容器的宽度

这是Android中实现等宽分布的经典技巧。

6.6 布局设计原则总结

通过分析HeadLine项目的四个布局文件,我们可以总结出以下Android布局设计原则:

  1. 选择合适的布局类型
    • 简单线性排列 → LinearLayout
    • 相对定位 → RelativeLayout
    • 复杂约束 → ConstraintLayout
  2. 减少布局嵌套层级:嵌套越深,测量和布局耗时越长。HeadLine项目中最多嵌套3层,是一个合理的深度。
  3. 使用样式复用:通过styles.xml定义公共样式,避免在每个控件中重复设置相同的属性。
  4. 使用布局复用:通过<include>标签复用标题栏布局。
  5. 合理使用margin和padding
    • margin:控件外边距,控制控件之间的距离
    • padding:控件内边距,控制内容与控件边缘的距离
  6. 使用资源引用:颜色、字符串等使用资源引用而非硬编码,便于统一管理和多语言支持。

第七章 控件使用详解

7.1 Android控件体系概述

Android的UI组件都继承自android.view.View类。View是Android UI的基本构建块,代表屏幕上的一个矩形区域。ViewGroup是View的子类,可以包含多个子View。

HeadLine项目中使用的控件包括:

  • TextView:文本显示控件
  • ImageView:图片显示控件
  • EditText:文本输入控件
  • View:基础视图控件(用于分割线)
  • LinearLayout:线性布局容器
  • RelativeLayout:相对布局容器
  • RecyclerView:高级列表控件

7.2 TextView——文本显示的核心控件

TextView是Android中最常用的控件之一,用于显示文本内容。HeadLine项目中大量使用了TextView。

7.2.1 TextView在HeadLine中的使用场景
位置ID用途关键属性
标题栏APP名称"仿今日头条"textSize=22sp, textColor=white
分类标签无(7个)新闻分类标签style=@style/tvStyle
单图Item标题tv_title新闻标题maxLines=2, textSize=16sp
单图Item来源tv_name新闻来源/作者style=@style/tvInfo
单图Item评论tv_comment评论数style=@style/tvInfo
单图Item时间tv_time发布时间style=@style/tvInfo
三图Item标题tv_title新闻标题maxLines=2, padding=8dp
三图Item来源tv_name新闻来源/作者style=@style/tvInfo
三图Item评论tv_comment评论数style=@style/tvInfo
三图Item时间tv_time发布时间style=@style/tvInfo
7.2.2 TextView关键属性详解

文本内容设置:

  • android:text:设置显示的文本内容
  • android:hint:设置提示文字(仅EditText有效,但概念类似)

文本样式设置:

  • android:textSize:文字大小,单位通常为sp(如16sp)。sp单位会根据系统字体设置自动缩放
  • android:textColor:文字颜色,可以是颜色值(#3c3c3c)或颜色资源引用(@color/gray_color)
  • android:maxLines:最大显示行数,超出部分截断
  • android:ellipsize:截断方式,如"end"表示末尾显示省略号

布局属性:

  • android:layout_width:控件宽度
  • android:layout_height:控件高度
  • android:gravity:内容在控件内的对齐方式
  • android:layout_gravity:控件在父容器中的对齐方式
7.2.3 新闻标题的maxLines属性

在新闻类APP中,标题通常需要限制显示行数,避免过长的标题破坏布局。HeadLine项目中:

<TextView
    android:id="@+id/tv_title"
    android:layout_width="280dp"
    android:layout_height="wrap_content"
    android:maxLines="2"
    android:textColor="#3c3c3c"
    android:textSize="16sp" />

android:maxLines="2"表示标题最多显示2行,如果文字超过2行,第3行及以后的文字会被截断。这个属性对于新闻标题非常重要,因为:

  1. 保证列表Item的高度可控
  2. 避免超长标题导致的布局错乱
  3. 提升视觉一致性

7.3 ImageView——图片显示的核心控件

ImageView用于在界面上显示图片资源。HeadLine项目中使用了多个ImageView来展示新闻图片、置顶标签等。

7.3.1 ImageView在HeadLine中的使用场景
位置ID用途图片来源
单图Itemiv_top置顶标签图标@drawable/top
单图Itemiv_img新闻配图动态设置(Java代码)
三图Itemiv_img1新闻图片1动态设置(Java代码)
三图Itemiv_img2新闻图片2动态设置(Java代码)
三图Itemiv_img3新闻图片3动态设置(Java代码)
7.3.2 ImageView关键属性详解

图片来源设置:

  • android:src:设置图片资源,可以是drawable资源引用或Java代码中动态设置
  • android:background:设置背景图片,与src不同,background会填充整个控件区域

图片缩放类型(scaleType): 虽然HeadLine项目中没有显式设置scaleType,但了解这个属性很重要:

  • fitXY:拉伸图片填充控件,可能变形
  • fitCenter:保持比例缩放,使图片完全显示在控件内
  • centerCrop:保持比例缩放,使图片填满控件,可能裁剪
  • centerInside:保持比例缩放,使图片完全显示,不裁剪

尺寸设置:

  • 单图Item中的iv_img:layout_width="match_parent"layout_height="90dp"
  • 三图Item中的iv_img1/2/3:通过样式设置layout_width="0dp"layout_height="90dp"layout_weight="1"
7.3.3 动态设置图片

在Java代码中,通过setImageResource()方法动态设置图片:

((MyViewHolder1) holder).iv_img.setImageResource(bean.getImgList().get(0));

这里bean.getImgList().get(0)返回的是图片资源的ID(如R.drawable.food),是一个int类型的值。setImageResource()会根据这个ID从资源文件中加载对应的图片。

7.4 EditText——文本输入控件

EditText是TextView的子类,允许用户输入和编辑文本。HeadLine项目中仅在标题栏使用了一个搜索框:

<EditText
    android:layout_width="match_parent"
    android:layout_height="35dp"
    android:layout_gravity="center_vertical"
    android:background="@drawable/search_bg"
    android:gravity="center_vertical"
    android:hint="搜你想搜的"
    android:textColorHint="@color/gray_color"
    android:textSize="14sp"
    android:paddingLeft="30dp" />

关键属性:

  • android:hint:提示文字,用户输入内容后自动隐藏
  • android:textColorHint:提示文字颜色
  • android:background:自定义背景(圆角灰色图片)
  • android:paddingLeft="30dp":为搜索图标留出空间

7.5 View——基础视图控件

View是所有UI控件的基类,也可以直接用作简单的视觉元素。HeadLine项目中使用View作为分割线:

<View
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:background="#eeeeee" />

这是一个非常常见的用法:创建一个高度为1dp、背景为浅灰色的View,作为视觉分隔线。相比使用ImageView或带背景的TextView,这种方式更加轻量。

7.6 LinearLayout——线性布局容器

LinearLayout将子元素按单一方向(水平或垂直)排列。HeadLine项目中多次使用:

垂直排列示例(主页面根布局):

<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- 子元素从上到下排列 -->
</LinearLayout>

水平排列示例(分类标签栏):

<LinearLayout
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="40dp">
    <!-- 子元素从左到右排列 -->
</LinearLayout>

关键属性:

  • android:orientation:排列方向,vertical(垂直)或horizontal(水平)
  • android:gravity:子元素在容器内的对齐方式
  • android:layout_weight:子元素的权重,用于分配剩余空间

7.7 RelativeLayout——相对布局容器

RelativeLayout允许子元素相对于父容器或其他兄弟元素进行定位。HeadLine项目中的Item布局都使用了RelativeLayout。

关键定位属性:

属性说明示例
layout_toRightOf在指定控件右侧@id/ll_info
layout_toLeftOf在指定控件左侧@id/xxx
layout_below在指定控件下方@id/tv_title
layout_above在指定控件上方@id/xxx
layout_alignParentBottom与父容器底部对齐true
layout_alignParentTop与父容器顶部对齐true
layout_centerVertical垂直居中true
layout_centerHorizontal水平居中true

7.8 RecyclerView——高级列表控件

RecyclerView是HeadLine项目的核心控件,它的使用方式与前两个项目类似,但功能更强大。

在XML中的声明:

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

在Java代码中的配置:

mRecyclerView = findViewById(R.id.rv_list);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new NewsAdapter(MainActivity.this, NewsList);
mRecyclerView.setAdapter(mAdapter);

RecyclerView的配置分为三步:

  1. 找到控件实例
  2. 设置LayoutManager(布局管理器)
  3. 设置Adapter(适配器)

这三步是RecyclerView使用的标准流程,缺一不可。


第八章 RecyclerView核心机制

8.1 RecyclerView的架构设计

RecyclerView之所以比ListView更强大,核心在于它的可插拔架构设计。RecyclerView本身只负责View的回收和复用,其他功能都委托给不同的组件:

┌─────────────────────────────────────────┐
│            RecyclerView                  │
├─────────────────────────────────────────┤
│  Adapter      → 数据绑定与ViewHolder创建  │
│  LayoutManager→ 布局测量与排列            │
│  ItemAnimator → Item增删动画             │
│  ItemDecoration→ 分割线、装饰效果         │
└─────────────────────────────────────────┘

这种设计使得RecyclerView具有极高的灵活性:

  • 换LayoutManager就能改变布局方式
  • 换ItemAnimator就能改变动画效果
  • 添加ItemDecoration就能添加分割线

8.2 LayoutManager详解

LayoutManager是RecyclerView的核心组件之一,负责:

  1. 测量和布局Item
  2. 管理Item的滚动
  3. 处理Item的可见性
  4. 决定何时回收不再可见的Item

LinearLayoutManager

HeadLine项目使用的是LinearLayoutManager,它实现线性布局(垂直或水平列表):

mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

LinearLayoutManager的常用配置:

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL); // 垂直方向(默认)
layoutManager.setReverseLayout(false); // 不反转布局
mRecyclerView.setLayoutManager(layoutManager);

其他LayoutManager:

LayoutManager效果适用场景
LinearLayoutManager线性列表新闻列表、聊天列表
GridLayoutManager网格列表图片画廊、商品网格
StaggeredGridLayoutManager瀑布流Pinterest风格布局

8.3 RecyclerView.Adapter详解

RecyclerView.Adapter是数据与视图之间的桥梁。与ListView的BaseAdapter相比,RecyclerView.Adapter有以下特点:

方法对比:

ListView BaseAdapterRecyclerView.Adapter说明
getCount()getItemCount()返回Item总数
getView()onCreateViewHolder() + onBindViewHolder()创建View并绑定数据
-getItemViewType()返回Item类型(多类型时重写)
-onCreateViewHolder(parent, viewType)根据类型创建ViewHolder

RecyclerView.Adapter的生命周期:

  1. RecyclerView首次展示时,调用getItemCount()获取总数
  2. 对于每个可见Item,调用getItemViewType()获取类型
  3. 根据类型调用onCreateViewHolder()创建ViewHolder
  4. 调用onBindViewHolder()绑定数据到ViewHolder
  5. 滚动时,复用已有的ViewHolder,只调用onBindViewHolder()更新数据

8.4 ViewHolder机制

ViewHolder是RecyclerView性能优化的核心。它的作用是缓存Item中子控件的引用,避免重复执行findViewById

RecyclerView中的ViewHolder:

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);
    }
}

ViewHolder的工作流程:

  1. RecyclerView需要显示一个Item时,先检查回收池中是否有可用的ViewHolder
  2. 如果有,直接复用;如果没有,调用onCreateViewHolder()创建新的ViewHolder
  3. 拿到ViewHolder后,调用onBindViewHolder()将数据绑定到ViewHolder中的控件上
  4. 当Item滚出屏幕时,ViewHolder被放入回收池,等待下次复用

为什么ViewHolder能提升性能?

  • findViewById是一个相对耗时的操作,它需要遍历View树来查找控件
  • 通过ViewHolder缓存控件引用,每个ViewHolder只执行一次findViewById
  • 在快速滚动场景下,可以大幅减少UI线程的工作量,避免卡顿

8.5 多类型Item实现原理

RecyclerView支持在同一个列表中展示不同类型的Item,这是通过以下三个步骤实现的:

步骤一:重写getItemViewType()

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

这个方法返回当前位置Item的类型值。HeadLine项目中返回的是NewsBean的type属性(1=单图,2=三图)。

步骤二:在onCreateViewHolder()中根据viewType创建不同的ViewHolder

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

步骤三:在onBindViewHolder()中根据ViewHolder类型绑定不同的数据

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    NewsBean bean = NewsList.get(position);
    if (holder instanceof MyViewHolder1) {
        // 单图/无图数据绑定
    } else if (holder instanceof MyViewHolder2) {
        // 三图数据绑定
    }
}

这里使用了instanceof关键字来判断ViewHolder的具体类型,然后进行类型转换和数据绑定。

8.6 RecyclerView的回收复用机制详解

RecyclerView的回收复用机制比ListView更加精细和高效。它使用了多级缓存策略:

四级缓存体系:

  1. mAttachedScrap:屏幕内的ViewHolder缓存,用于数据集不变时的快速复用(如notifyItemChanged)
  2. mCachedViews:屏幕外的ViewHolder缓存,默认2个,保存完整的数据状态
  3. ViewCacheExtension:开发者自定义的缓存层,一般不使用
  4. RecycledViewPool:回收池,按viewType分类存储ViewHolder,默认每种类型5个

复用流程:

需要显示新Item
    ↓
查找mAttachedScrap(屏幕内缓存)
    ↓ 未找到
查找mCachedViews(屏幕外缓存)
    ↓ 未找到
查找RecycledViewPool(回收池)
    ↓ 未找到
调用onCreateViewHolder()创建新的ViewHolder
    ↓
调用onBindViewHolder()绑定数据
    ↓
显示到屏幕上

当Item滚出屏幕时:

Item滚出屏幕
    ↓
ViewHolder放入mCachedViews(如果未满2个)
    ↓ mCachedViews满了
最旧的ViewHolder移入RecycledViewPool
    ↓ 回收池满了
ViewHolder被销毁,等待GC回收

这种多级缓存策略确保了:

  • 频繁复用的ViewHolder能快速找到
  • 内存占用可控,不会无限缓存
  • 不同类型的ViewHolder分开管理

第九章 NewsAdapter多类型Item实现

9.1 NewsAdapter整体架构

NewsAdapter是HeadLine项目的核心类,负责将NewsBean数据列表转换为RecyclerView可展示的多种类型的Item视图。

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

    // 构造方法
    public NewsAdapter(Context context, List<NewsBean> NewsList) {...}

    // 创建ViewHolder
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(...) {...}

    // 获取Item类型
    @Override
    public int getItemViewType(int position) {...}

    // 绑定数据
    @Override
    public void onBindViewHolder(...) {...}

    // 获取Item总数
    @Override
    public int getItemCount() {...}

    // 内部类:单图ViewHolder
    class MyViewHolder1 extends RecyclerView.ViewHolder {...}

    // 内部类:三图ViewHolder
    class MyViewHolder2 extends RecyclerView.ViewHolder {...}
}

9.2 泛型参数RecyclerView.ViewHolder

public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>

这里使用了RecyclerView.ViewHolder作为泛型参数,而不是具体的MyViewHolder1或MyViewHolder2。这是因为Adapter需要支持多种类型的ViewHolder,使用基类RecyclerView.ViewHolder可以同时兼容所有子类。

如果只有一种类型的Item,可以写成:

public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.MyViewHolder>

9.3 getItemViewType()方法

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

这是多类型Item实现的第一个关键方法。它根据数据中的type字段返回对应的类型值:

  • 返回1 → 单图/无图类型
  • 返回2 → 三图类型

这个返回值会传递给onCreateViewHolder()的viewType参数,决定创建哪种类型的ViewHolder。

类型值的设计原则:

  • 类型值应该是非负整数
  • 不同类型的值必须互不相同
  • 类型值不需要连续(可以是1、2、5、10)

9.4 onCreateViewHolder()方法

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

LayoutInflater详解:

LayoutInflater.from(mContext).inflate()是Android中加载XML布局的标准方式。它有三个参数:

  1. resource:布局资源ID(如R.layout.list_item_one)
  2. root:父容器(parent),用于生成正确的LayoutParams
  3. attachToRoot:是否立即添加到父容器中。这里传false,因为RecyclerView会在合适的时机自动添加

为什么attachToRoot要传false?

如果传true,布局会立即被添加到parent中。但RecyclerView需要自己管理Item的添加时机和位置,所以必须传false,让RecyclerView在内部处理添加逻辑。

9.5 onBindViewHolder()方法

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

instanceof关键字的使用:

instanceof用于判断对象是否是某个类的实例。这里通过holder instanceof MyViewHolder1判断当前ViewHolder是单图类型还是三图类型,然后进行相应的数据绑定。

View.VISIBLE和View.GONE的区别:

  • View.VISIBLE:控件可见,正常显示
  • View.GONE:控件不可见,且不占据布局空间(相当于从布局中移除)
  • View.INVISIBLE:控件不可见,但仍占据布局空间

在HeadLine项目中,第一条新闻(position==0)是置顶新闻,需要显示置顶标签(iv_top设为VISIBLE)并隐藏图片(iv_img设为GONE)。其他新闻则相反。

9.6 MyViewHolder1——单图/无图ViewHolder

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);
    }
}

MyViewHolder1缓存了单图布局中的6个控件:

  • iv_top:置顶标签ImageView
  • iv_img:新闻图片ImageView
  • title:标题TextView
  • name:来源TextView
  • comment:评论数TextView
  • time:发布时间TextView

9.7 MyViewHolder2——三图ViewHolder

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);
    }
}

MyViewHolder2缓存了三图布局中的7个控件:

  • iv_img1/2/3:三张新闻图片ImageView
  • title/name/comment/time:与MyViewHolder1相同的文本控件

9.8 多类型Item的完整数据流

理解NewsAdapter的工作流程,最好的方式是追踪一条新闻从数据到展示的完整路径:

以第三条新闻(三图新闻)为例:

1. RecyclerView需要显示第3个Item(position=2)
   ↓
2. 调用getItemViewType(2) → 返回NewsList.get(2).getType() → 返回23. 调用onCreateViewHolder(parent, 2)
   → viewType==2 → 加载list_item_two.xml → 创建MyViewHolder2
   ↓
4. 调用onBindViewHolder(holder, 2)
   → holder instanceof MyViewHolder2 → 进入三图分支
   → 获取bean = NewsList.get(2)
   → 设置title、name、comment、time
   → 设置iv_img1、iv_img2、iv_img3的图片
   ↓
5. Item显示到屏幕上

以第一条新闻(置顶无图新闻)为例:

1. RecyclerView需要显示第1个Item(position=0)
   ↓
2. 调用getItemViewType(0) → 返回NewsList.get(0).getType() → 返回13. 调用onCreateViewHolder(parent, 1)
   → viewType==1 → 加载list_item_one.xml → 创建MyViewHolder1
   ↓
4. 调用onBindViewHolder(holder, 0)
   → holder instanceof MyViewHolder1 → 进入单图分支
   → position==0 → iv_top设为VISIBLE,iv_img设为GONE
   → 设置title、name、comment、time
   → imgList.size()==0 → 直接return,不设置图片
   ↓
5. Item显示到屏幕上(无图片,有置顶标签)

第十章 NewsBean数据模型设计

10.1 为什么需要实体类?

在ListView和RecyclerView基础项目中,数据是通过平行数组存储的:

private String[] titles = {...};
private String[] prices = {...};
private int[] icons = {...};

这种方式在简单Demo中可以工作,但在实际项目中存在严重问题:

  1. 数据分散:一条新闻的信息分散在多个数组中,难以管理
  2. 容易出错:数组长度不一致会导致数据错位
  3. 难以扩展:新增字段需要新增数组,修改逻辑复杂
  4. 不符合面向对象思想:数据没有封装,缺乏类型安全

NewsBean实体类解决了以上所有问题,将一条新闻的所有属性封装在一个对象中。

10.2 NewsBean类完整分析

public class NewsBean {
    private int id;              // 新闻唯一标识ID
    private String title;        // 新闻标题文本
    private List<Integer> imgList; // 新闻图片资源ID列表
    private String name;         // 新闻发布来源/作者名称
    private String comment;      // 新闻评论数显示文本
    private String time;         // 新闻发布时间显示文本
    private int type;            // 新闻类型:1=单图,2=三图

    // Getter和Setter方法
    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; }
}

10.3 字段设计详解

private int id 新闻的唯一标识ID。在真实项目中,这个ID通常由服务器分配,用于标识每条新闻。在Demo中,使用循环索引+1作为ID。

private String title 新闻标题。这是新闻最重要的文本信息,展示在Item的最显眼位置。

private List<Integer> imgList 新闻图片资源ID列表。这是一个巧妙的设计:

  • 空列表(size=0)→ 无图新闻(置顶新闻)
  • 1个元素(size=1)→ 单图新闻
  • 3个元素(size=3)→ 三图新闻

通过列表的大小,可以判断新闻的图片数量,进而决定使用哪种布局。

private String name 新闻来源/作者名称。如"央视新闻客户端"、"味美食记"等。

private String comment 评论数显示文本。注意这里使用String而不是int,因为显示格式是"9884评"这样的文本,而不是纯数字。

private String time 发布时间显示文本。如"6小时前"、"刚刚"等相对时间格式。

private int type 新闻类型标识。1表示单图布局,2表示三图布局。这个字段直接决定了Adapter使用哪种ViewHolder来渲染这条新闻。

10.4 面向对象封装的优势

使用NewsBean相比平行数组的优势:

维度平行数组NewsBean实体类
数据组织分散在多个数组集中在一个对象
类型安全弱(数组索引可能越界)强(属性访问)
扩展性差(需要新增数组)好(只需新增字段)
可维护性低(多处同步修改)高(单点修改)
传递方式需要传递多个数组传递一个List
网络对接需要手动映射可直接使用JSON解析

在真实开发中,NewsBean这样的实体类通常会配合JSON解析库(如Gson、Fastjson)使用,服务器返回的JSON数据可以直接映射为NewsBean对象列表。


第十一章 MainActivity主逻辑分析

11.1 MainActivity整体职责

MainActivity是HeadLine项目的入口Activity,负责:

  1. 加载页面布局
  2. 组装新闻数据
  3. 初始化RecyclerView
  4. 设置适配器

11.2 数据定义

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};

数据说明:

  • titles:6条新闻的标题,涵盖了社会、美食、健康、生活、农业、电竞等多个领域
  • names:6条新闻的来源,模拟了不同的自媒体账号
  • comments:评论数,从18到9884不等,模拟真实的热度差异
  • times:发布时间,从"刚刚"到"6小时前",模拟不同的时间跨度
  • icons1:单图新闻的图片资源(3张)
  • icons2:三图新闻的图片资源(6张,每条三图新闻用3张)
  • types:每条新闻的类型(1=单图,2=三图)

11.3 onCreate()方法

@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);
}

执行流程:

  1. super.onCreate(savedInstanceState):调用父类的onCreate方法,完成Activity的基础初始化
  2. setContentView(R.layout.activity_main):加载主页面布局
  3. setData():组装新闻数据,将零散的数组合并为NewsBean对象列表
  4. findViewById(R.id.rv_list):找到RecyclerView控件
  5. setLayoutManager(new LinearLayoutManager(this)):设置线性布局管理器
  6. new NewsAdapter(MainActivity.this, NewsList):创建适配器
  7. setAdapter(mAdapter):绑定适配器

11.4 setData()方法详解

private void setData() {
    NewsList = new ArrayList<NewsBean>();
    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]);
        bean.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);
    }
}

方法分析:

这个方法的核心任务是将分散在多个数组中的数据组装成结构化的NewsBean对象列表。

循环部分(公共属性设置):

  • 遍历所有新闻(6条)
  • 为每条新闻设置id、title、name、comment、time、type这些公共属性
  • 这些属性对所有新闻都是通用的,所以放在循环中统一设置

switch部分(图片设置):

  • 根据新闻位置(i)的不同,设置不同的图片列表
  • 每条新闻的图片数量和来源都不同,所以需要单独处理

各条新闻的图片配置:

位置(i)类型图片数量图片来源
0置顶无图0张空列表
1单图1张icons1[0] = food
2三图3张icons2[0/1/2] = sleep1/2/3
3单图1张icons1[1] = takeout
4三图3张icons2[3/4/5] = fruit1/2/3
5单图1张icons1[2] = e_sports

真实开发中的替代方案:

在真实项目中,setData()方法会被替换为从网络API获取数据:

private void loadNewsFromServer() {
    // 使用Retrofit等网络框架请求API
    api.getNewsList(new Callback<List<NewsBean>>() {
        @Override
        public void onResponse(List<NewsBean> newsList) {
            NewsList = newsList;
            mAdapter.notifyDataSetChanged();
        }
    });
}

或者从本地数据库读取:

private void loadNewsFromDatabase() {
    NewsList = newsDao.getAllNews();
    mAdapter.notifyDataSetChanged();
}

11.5 数据流全景图

┌─────────────────────────────────────────────────────┐
│                    MainActivity                       │
│                                                      │
│  定义数组(titles/names/comments/times/icons/types)    │
│         ↓                                            │
│     setData()                                        │
│         ↓                                            │
│  List<NewsBean> NewsList (结构化数据)                  │
│         ↓                                            │
│  new NewsAdapter(context, NewsList)                  │
│         ↓                                            │
│  mRecyclerView.setAdapter(mAdapter)                  │
└─────────────────────────────────────────────────────┘
         ↓
┌─────────────────────────────────────────────────────┐
│                    NewsAdapter                        │
│                                                      │
│  getItemViewType(position) → 获取类型                 │
│         ↓                                            │
│  onCreateViewHolder(viewType) → 创建对应ViewHolder          │
│         ↓                                            │
│  onBindViewHolder(holder, position) → 绑定数据        │
│         ↓                                            │
│  RecyclerView展示Item                                │
└─────────────────────────────────────────────────────┘

第十二章 样式与主题系统

12.1 Android样式系统概述

Android的样式系统允许开发者将UI属性(如颜色、字体、尺寸等)定义为可复用的资源。使用样式的好处:

  • 减少重复代码:多个控件共用相同的样式时,只需定义一次
  • 统一视觉风格:确保整个APP的UI风格一致
  • 便于维护:修改样式只需修改一处

12.2 styles.xml完整分析

<resources>
    <!-- 应用主题 -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <!-- 分类标签样式 -->
    <style name="tvStyle">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">match_parent</item>
        <item name="android:padding">10dp</item>
        <item name="android:gravity">center</item>
        <item name="android:textSize">15sp</item>
    </style>

    <!-- 底部信息文字样式 -->
    <style name="tvInfo">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginLeft">8dp</item>
        <item name="android:layout_gravity">center_vertical</item>
        <item name="android:textSize">14sp</item>
        <item name="android:textColor">@color/gray_color</item>
    </style>

    <!-- 三图Item图片样式 -->
    <style name="ivImg">
        <item name="android:layout_width">0dp</item>
        <item name="android:layout_height">90dp</item>
        <item name="android:layout_weight">1</item>
        <item name="android:layout_toRightOf">@id/ll_info</item>
    </style>
</resources>

12.3 自定义样式详解

tvStyle——分类标签样式:

这个样式被7个分类标签TextView共用:

<style name="tvStyle">
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">match_parent</item>
    <item name="android:padding">10dp</item>
    <item name="android:gravity">center</item>
    <item name="android:textSize">15sp</item>
</style>

使用方式:

<TextView style="@style/tvStyle" android:text="推荐"
    android:textColor="@android:color/holo_red_dark" />

注意:虽然使用了tvStyle,但textColor在布局文件中单独设置,因为"推荐"标签需要红色,其他标签需要灰色。这是样式覆盖的典型用法。

tvInfo——底部信息文字样式:

这个样式被所有新闻Item的来源、评论、时间TextView共用:

<style name="tvInfo">
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:layout_marginLeft">8dp</item>
    <item name="android:layout_gravity">center_vertical</item>
    <item name="android:textSize">14sp</item>
    <item name="android:textColor">@color/gray_color</item>
</style>

使用方式:

<TextView android:id="@+id/tv_name" style="@style/tvInfo" />
<TextView android:id="@+id/tv_comment" style="@style/tvInfo" />
<TextView android:id="@+id/tv_time" style="@style/tvInfo" />

三个TextView共用同一个样式,代码非常简洁。

ivImg——三图Item图片样式:

<style name="ivImg">
    <item name="android:layout_width">0dp</item>
    <item name="android:layout_height">90dp</item>
    <item name="android:layout_weight">1</item>
    <item name="android:layout_toRightOf">@id/ll_info</item>
</style>

这个样式使用了layout_weight="1"实现三张图片等宽分布。注意layout_toRightOf属性实际上在RelativeLayout中才有意义,而ivImg用在LinearLayout的子元素中,这个属性会被忽略。这是一个小的设计瑕疵,不影响功能。

12.4 颜色资源(colors.xml)

<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
    <color name="light_gray_color">#eeeeee</color>
    <color name="gray_color">#828282</color>
</resources>

颜色说明:

颜色名色值用途
colorPrimary#008577主题主色(青绿色)
colorPrimaryDark#00574B主题深色(深青绿)
colorAccent#D81B60强调色(粉红色)
light_gray_color#eeeeee浅灰色,页面背景
gray_color#828282灰色,辅助文字颜色

12.5 字符串资源(strings.xml)

<resources>
    <string name="app_name">HeadLine</string>
</resources>

目前只定义了应用名称。在实际项目中,应该将所有硬编码的字符串都提取到strings.xml中,以支持多语言国际化。

12.6 主题配置

在AndroidManifest.xml中,应用主题设置为:

android:theme="@style/Theme.AppCompat.NoActionBar"

使用NoActionBar主题的原因是项目自定义了标题栏(title_bar.xml),不需要系统默认的ActionBar。这是新闻类APP的常见做法,因为自定义标题栏可以实现更丰富的功能(如搜索框、菜单等)。


第十三章 图片资源与Drawable管理

13.1 Android图片资源概述

Android支持多种图片格式的资源文件:

  • PNG:无损压缩,支持透明,最适合图标和界面元素
  • JPG/JPEG:有损压缩,不支持透明,适合照片
  • WebP:Google推出的现代图片格式,压缩率更高
  • XML Drawable:用XML定义的矢量图形,可无限缩放

13.2 HeadLine项目的图片资源

drawable-hdpi/
├── food.png          # 美食图片(单图新闻第2条)
├── takeout.png       # 外卖小哥图片(单图新闻第4条)
├── e_sports.png      # 电竞比赛图片(单图新闻第6条)
├── sleep1.png        # 睡眠相关图片1(三图新闻第3条)
├── sleep2.png        # 睡眠相关图片2(三图新闻第3条)
├── sleep3.png        # 睡眠相关图片3(三图新闻第3条)
├── fruit1.png        # 水果图片1(三图新闻第5条)
├── fruit2.png        # 水果图片2(三图新闻第5条)
├── fruit3.png        # 水果图片3(三图新闻第5条)
├── top.png           # 置顶标签图标
└── search_bg.png     # 搜索框圆角背景

图片资源密度说明:

图片放在drawable-hdpi目录下,hdpi表示高密度屏幕(约240dpi)。在完整的项目中,应该为不同密度的屏幕提供不同尺寸的图片:

密度目录缩放比例适用屏幕
ldpidrawable-ldpi0.75x低密度屏幕
mdpidrawable-mdpi1.0x中密度屏幕
hdpidrawable-hdpi1.5x高密度屏幕
xhdpidrawable-xhdpi2.0x超高密度屏幕
xxhdpidrawable-xxhdpi3.0x超超高密度屏幕
xxxhdpidrawable-xxxhdpi4.0x超超超高密度屏幕

13.3 图片在代码中的引用

XML中引用:

android:src="@drawable/top"
android:background="@drawable/search_bg"

Java代码中引用:

private int[] icons1 = {R.drawable.food, R.drawable.takeout, R.drawable.e_sports};

R.drawable.xxx是Android自动生成的资源ID,是一个int类型的常量。通过资源ID,可以在XML和Java代码中引用图片资源。

13.4 图片加载方式对比

HeadLine项目使用的是最基础的图片加载方式——setImageResource()

holder.iv_img.setImageResource(bean.getImgList().get(0));

这种方式适合加载本地drawable资源。但在真实项目中,新闻图片通常来自网络URL,这时需要使用专业的图片加载库:

图片加载库特点适用场景
GlideGoogle推荐,API简洁,自动缓存大多数场景
PicassoSquare出品,简单易用简单图片加载
FrescoFacebook出品,内存优化最好大量图片场景
CoilKotlin协程优先,轻量级Kotlin项目

使用Glide加载网络图片的示例:

Glide.with(mContext)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .into(holder.iv_img);

第十四章 性能优化与最佳实践

14.1 RecyclerView性能优化

RecyclerView本身已经做了大量性能优化,但在实际开发中,还可以进一步优化:

1. 使用DiffUtil进行局部刷新

当数据发生变化时,不要使用notifyDataSetChanged()全局刷新,而是使用DiffUtil进行局部刷新:

DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
    @Override
    public int getOldListSize() { return oldList.size(); }
    @Override
    public int getNewListSize() { return newList.size(); }
    @Override
    public boolean areItemsTheSame(int oldPos, int newPos) {
        return oldList.get(oldPos).getId() == newList.get(newPos).getId();
    }
    @Override
    public boolean areContentsTheSame(int oldPos, int newPos) {
        return oldList.get(oldPos).equals(newList.get(newPos));
    }
});
result.dispatchUpdatesTo(mAdapter);

2. 使用setHasFixedSize()

如果RecyclerView的尺寸不会因为数据变化而改变,可以设置:

mRecyclerView.setHasFixedSize(true);

这可以避免不必要的布局测量,提升性能。

3. 图片加载优化

  • 使用专业的图片加载库(Glide/Picasso)
  • 设置合适的图片尺寸,避免加载过大的图片
  • 使用内存缓存和磁盘缓存

4. 预加载(Prefetch)

RecyclerView支持预加载,在用户滚动到下一个Item之前提前创建ViewHolder:

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setItemPrefetchEnabled(true);

14.2 内存优化

1. 避免内存泄漏

在Adapter中持有Context引用时,注意避免内存泄漏:

// 不推荐:持有Activity的强引用
private Context mContext;

// 推荐:使用ApplicationContext
private Context mContext;
public NewsAdapter(Context context, List<NewsBean> list) {
    this.mContext = context.getApplicationContext();
}

2. 图片内存管理

  • 及时回收不再使用的Bitmap
  • 使用inSampleSize对大图进行采样
  • 使用专业的图片加载库自动管理内存

14.3 布局优化

1. 减少布局嵌套

HeadLine项目中最多嵌套3层,是一个合理的深度。嵌套越深,测量和布局耗时越长。

2. 使用merge标签

当使用<include>引入布局时,如果引入的布局根元素与父容器类型相同,可以使用<merge>标签减少一层嵌套:

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 子元素直接作为include的父容器的子元素 -->
</merge>

3. 使用ViewStub延迟加载

对于不立即显示的布局部分,可以使用ViewStub延迟加载,减少初始渲染时间。

14.4 代码规范建议

1. 命名规范

  • 类名:大驼峰(NewsAdapter、NewsBean)
  • 变量名:小驼峰(mRecyclerView、mAdapter)
  • 常量名:全大写+下划线(MAX_ITEM_COUNT)
  • 资源ID:小写+下划线(tv_title、iv_img)

2. 注释规范

HeadLine项目的注释非常详细,每个类、方法、关键字段都有中文注释。这是非常好的习惯,特别是对于教学项目。

3. 代码结构

  • 成员变量声明在类顶部
  • 构造方法紧随其后
  • 公共方法在前,私有方法在后
  • 内部类放在最后

第十五章 从ListView到RecyclerView的迁移指南

15.1 为什么要从ListView迁移到RecyclerView?

在Android开发的历史长河中,ListView曾经是最主流的列表组件。然而,随着应用复杂度的不断提升和用户对流畅体验要求的提高,ListView的局限性日益凸显。Google在2014年的Android 5.0(API 21)中正式推出了RecyclerView,并明确建议开发者在新项目中使用RecyclerView替代ListView。

官方推荐的原因:

  1. 性能优势:RecyclerView内置了更高效的回收机制,四级缓存体系比ListView的两级缓存更加精细
  2. 灵活性:通过LayoutManager可以轻松切换不同的布局方式,而ListView只能实现垂直列表
  3. 动画支持:RecyclerView内置ItemAnimator,支持增删动画,ListView完全没有内置动画
  4. 局部刷新:RecyclerView支持notifyItemChanged()notifyItemInserted()等局部刷新方法,ListView只能全局刷新
  5. 扩展性:RecyclerView支持自定义ItemDecoration(分割线、装饰)、ItemAnimator(动画)、LayoutManager(布局)

迁移的时机判断:

场景建议
新项目开发直接使用RecyclerView
旧项目维护逐步迁移,优先迁移高频使用的列表页面
简单设置页列表可以保留ListView(改动成本低)
复杂新闻/社交列表必须迁移到RecyclerView

15.2 代码层面的迁移步骤

通过对比Chapter03中的ListView项目和HeadLine项目,我们可以总结出完整的迁移路径。

步骤一:替换布局文件中的控件

ListView的布局:

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

迁移为RecyclerView:

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

注意:RecyclerView的完整类名需要写全(androidx.recyclerview.widget.RecyclerView),因为它不是Android SDK内置的控件,而是support库/AndroidX库中的组件。

步骤二:替换Adapter继承关系

ListView的Adapter继承BaseAdapter:

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; }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 手动处理convertView复用和ViewHolder
    }
}

迁移为RecyclerView.Adapter:

class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 创建ViewHolder
    }
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        // 绑定数据
    }
    @Override
    public int getItemCount() { return titles.length; }
    
    class MyViewHolder extends RecyclerView.ViewHolder {
        // ViewHolder定义
    }
}

步骤三:替换Java代码中的配置

ListView的配置:

mListView = findViewById(R.id.lv);
MyBaseAdapter mAdapter = new MyBaseAdapter();
mListView.setAdapter(mAdapter);

迁移为RecyclerView的配置:

mRecyclerView = findViewById(R.id.rv_list);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
MyAdapter mAdapter = new MyAdapter();
mRecyclerView.setAdapter(mAdapter);

关键区别:RecyclerView多了一步setLayoutManager(),这是必须的,因为RecyclerView本身不负责布局,而是委托给LayoutManager。

步骤四:添加依赖

在build.gradle中添加RecyclerView依赖:

dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
}

15.3 迁移过程中的常见问题

问题一:convertView复用逻辑如何处理?

在ListView中,我们需要手动判断convertView是否为null,然后决定是否创建新View。迁移到RecyclerView后,这部分逻辑完全由RecyclerView自动处理,开发者只需要在onCreateViewHolder()中创建ViewHolder,在onBindViewHolder()中绑定数据即可。

问题二:setTag/getTag如何使用?

ListView中使用convertView.setTag(holder)convertView.getTag()来缓存ViewHolder。RecyclerView中不再需要这种方式,因为ViewHolder本身就是缓存载体,控件引用直接定义在ViewHolder类中。

问题三:notifyDataSetChanged()的替代方案?

ListView中数据变化后只能调用notifyDataSetChanged()全局刷新。RecyclerView提供了更精细的刷新方法:

方法效果性能
notifyDataSetChanged()全局刷新最差
notifyItemChanged(position)刷新指定位置
notifyItemInserted(position)插入动画+刷新
notifyItemRemoved(position)删除动画+刷新
notifyItemRangeChanged(start, count)批量刷新范围
DiffUtil.dispatchUpdatesTo()智能差异刷新最好

15.4 迁移后的收益

完成迁移后,你将获得以下收益:

  1. 代码更简洁:不再需要手动处理convertView复用逻辑,代码量减少约30%
  2. 性能更稳定:RecyclerView的回收机制更加精细,滑动流畅度提升
  3. 功能更丰富:可以轻松实现网格布局、瀑布流、Item动画等高级功能
  4. 维护更方便:职责分离的设计让代码结构更清晰,便于后续扩展
  5. 面向未来:Google持续投入RecyclerView的优化和新功能开发

第十六章 Android布局系统深度剖析

16.1 Android布局的渲染流程

理解Android布局系统的工作原理,对于编写高效的布局至关重要。当一个Activity被创建时,Android系统会经历以下三个主要阶段来渲染UI:

第一阶段:Measure(测量)

在这个阶段,Android从根布局开始,递归地测量每个View和ViewGroup的大小。测量过程遵循"自上而下"的原则:父容器告诉子元素期望的大小限制,子元素根据自己的内容和布局参数决定实际大小,然后回报给父容器。

测量的关键概念:

  • MeasureSpec:封装了父容器对子元素的尺寸要求和模式
  • EXACTLY:父容器已经确定了子元素的确切大小(如layout_width="100dp")
  • AT_MOST:子元素不能超过这个大小(如layout_width="wrap_content")
  • UNSPECIFIED:子元素可以是任意大小

第二阶段:Layout(布局)

测量完成后,Android进入布局阶段,确定每个View在屏幕上的具体位置。父容器根据测量结果和布局参数,为每个子元素分配坐标(left, top, right, bottom)。

不同布局类型的布局策略:

  • LinearLayout:按照orientation方向依次排列
  • RelativeLayout:根据相对定位规则计算位置
  • ConstraintLayout:根据约束条件求解位置

第三阶段:Draw(绘制)

布局确定后,Android进入绘制阶段,将每个View绘制到屏幕上。绘制过程也是"自上而下"的,但绘制顺序是"自下而上"——先绘制子元素,再绘制父元素(这样父元素的背景才不会覆盖子元素)。

绘制流程:

  1. 绘制背景(drawBackground)
  2. 绘制内容(onDraw)
  3. 绘制子元素(dispatchDraw)
  4. 绘制装饰(如滚动条)(onDrawForeground)

16.2 布局性能优化原则

原则一:减少布局层级

布局层级越深,Measure、Layout、Draw三个阶段需要递归遍历的次数就越多,性能就越差。HeadLine项目中,最深的嵌套层级为3-4层,这是一个合理的深度。

优化建议:

  • 优先使用ConstraintLayout,它可以将多层嵌套扁平化
  • 使用<merge>标签减少不必要的根布局
  • 使用<include>复用布局时注意是否引入了额外的嵌套层

原则二:避免过度绘制(Overdraw)

过度绘制指的是屏幕上的同一个像素点在一帧中被绘制了多次。例如,如果父布局有白色背景,子View也有白色背景,那么白色背景就被绘制了两次。

检测过度绘制的方法:

  • 在开发者选项中开启"调试GPU过度绘制"
  • 蓝色:1次过度绘制(可接受)
  • 绿色:2次过度绘制(需要注意)
  • 粉色:3次过度绘制(需要优化)
  • 红色:4次及以上过度绘制(必须优化)

HeadLine项目中的优化点:

  • 根布局设置了浅灰色背景(#eeeeee)
  • 每个Item设置了白色背景(@android:color/white)
  • 这是合理的,因为Item的白色背景覆盖了根布局的灰色背景,形成了卡片效果

原则三:使用合适的布局类型

布局类型性能适用场景
FrameLayout最快单个子元素或层叠布局
LinearLayout简单线性排列
RelativeLayout中等相对定位,两层嵌套可替代LinearLayout嵌套
ConstraintLayout中等偏慢(测量阶段)复杂布局,可扁平化多层嵌套

16.3 HeadLine项目布局设计分析

activity_main.xml的设计思路:

这个布局采用了经典的"顶部固定+内容填充"的设计模式:

  1. 顶部固定区域:标题栏(通过include引入)
  2. 中间固定区域:分类标签栏
  3. 分隔区域:1dp分割线
  4. 内容填充区域:RecyclerView

这种设计模式在新闻类APP中非常常见,它的优点是结构清晰、层次分明。

list_item_one.xml的设计思路:

单图Item采用了"左文右图"的经典新闻列表布局:

  • 左侧:标题 + 底部信息(来源、评论、时间)
  • 右侧:新闻配图

这种布局的优点是信息密度适中,用户可以快速浏览标题和缩略图,找到感兴趣的新闻。

list_item_two.xml的设计思路:

三图Item采用了"上图下文"的布局:

  • 顶部:标题
  • 中间:三张等宽图片
  • 底部:来源、评论、时间

这种布局适合展示图片内容丰富的新闻,三张图片可以给用户更多的视觉信息,帮助判断是否感兴趣。

16.4 dp、sp、px的区别

在Android布局中,尺寸单位的选择非常重要:

单位全称特点使用场景
dp/dipDensity-independent Pixels与屏幕密度无关控件宽高、边距
spScale-independent Pixels与屏幕密度和用户字体偏好有关文字大小
pxPixels物理像素一般不推荐使用

为什么使用dp而不是px?

不同设备的屏幕密度(dpi)不同。如果使用px,在低密度屏幕上看起来合适的大小,在高密度屏幕上会显得很小。dp会根据屏幕密度自动缩放,确保在不同设备上视觉效果一致。

换算关系:px = dp × (dpi / 160)

例如,在hdpi屏幕(240dpi)上,1dp = 1.5px。

为什么文字使用sp而不是dp?

sp在dp的基础上,还会根据用户在系统设置中选择的字体大小进行缩放。如果用户设置了大字体,使用sp的文字会自动变大,而使用dp的文字不会变化。这体现了Android对无障碍(Accessibility)的支持。

16.5 布局中的gravity与layout_gravity

这是一个初学者经常混淆的概念:

  • android:gravity:控制内容控件内部的对齐方式
  • android:layout_gravity:控制控件父容器中的对齐方式

举例说明:

<TextView
    android:layout_width="200dp"
    android:layout_height="100dp"
    android:gravity="center"
    android:layout_gravity="right"
    android:text="Hello" />

在这个例子中:

  • gravity="center":文字"Hello"在TextView内部居中显示
  • layout_gravity="right":TextView本身在父容器的右侧对齐

第十七章 Adapter设计模式与架构思维

17.1 Adapter模式的本质

Adapter(适配器)模式是GoF(Gang of Four)设计模式中的经典模式之一。它的核心思想是:将一个类的接口转换成客户期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。

在Android中,Adapter模式的应用场景是:将数据(Model)和视图(View)解耦

数据源(数组/集合/数据库) → Adapter → 列表视图(ListView/RecyclerView)

Adapter充当了数据和视图之间的"翻译官":

  • 列表视图问Adapter:"你有多少条数据?" → Adapter回答:getItemCount()
  • 列表视图问Adapter:"第3条数据长什么样?" → Adapter回答:onBindViewHolder(holder, 3)
  • 列表视图问Adapter:"第3条数据应该用什么布局?" → Adapter回答:getItemViewType(3)onCreateViewHolder(viewType)

17.2 ListView Adapter vs RecyclerView Adapter架构对比

ListView的BaseAdapter架构:

BaseAdapter (抽象类)
├── getCount()        → 返回数据总数
├── getItem()         → 返回指定位置的数据
├── getItemId()       → 返回指定位置的ID
└── getView()         → 核心方法,创建View并绑定数据
    ├── 判断convertView是否为null
    ├── 如果为null:加载布局 + 创建ViewHolder + findViewById
    ├── 如果不为null:获取缓存的ViewHolder
    └── 设置数据到控件

RecyclerView的Adapter架构:

RecyclerView.Adapter<VH> (抽象类)
├── getItemCount()          → 返回数据总数
├── getItemViewType()       → 返回Item类型(可选重写)
├── onCreateViewHolder()     → 创建ViewHolder
│   └── 加载布局 + 创建ViewHolder实例
└── onBindViewHolder()      → 绑定数据
    └── 根据ViewHolder类型设置数据

架构对比分析:

维度BaseAdapterRecyclerView.Adapter
方法数量4个必须实现3个必须实现
职责划分getView承担所有职责创建和绑定分离
复用处理手动判断convertView自动管理
多类型支持需要重写getViewTypeCount+getItemViewType只需重写getItemViewType
泛型约束强制指定ViewHolder类型
代码可读性中等(getView方法较长)高(职责分离)

17.3 NewsAdapter的设计亮点

HeadLine项目中的NewsAdapter有几个值得学习的设计亮点:

亮点一:使用RecyclerView.ViewHolder作为泛型参数

public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>

这里没有使用具体的MyViewHolder1或MyViewHolder2,而是使用了它们的基类RecyclerView.ViewHolder。这样做的好处是可以同时兼容多种类型的ViewHolder。

亮点二:通过instanceof判断ViewHolder类型

if (holder instanceof MyViewHolder1) {
    // 单图数据绑定
} else if (holder instanceof MyViewHolder2) {
    // 三图数据绑定
}

使用instanceof关键字判断ViewHolder的具体类型,然后进行类型转换和数据绑定。这是Java中处理多态的经典方式。

亮点三:将置顶逻辑放在onBindViewHolder中

if (position == 0) {
    ((MyViewHolder1) holder).iv_top.setVisibility(View.VISIBLE);
    ((MyViewHolder1) holder).iv_img.setVisibility(View.GONE);
} else {
    ((MyViewHolder1) holder).iv_top.setVisibility(View.GONE);
    ((MyViewHolder1) holder).iv_img.setVisibility(View.VISIBLE);
}

置顶新闻(第一条)没有图片,但有置顶标签。这个特殊的展示逻辑放在onBindViewHolder中处理,根据position动态控制控件的显示/隐藏。

17.4 Adapter中的MVC/MVP/MVVM思想

虽然HeadLine项目没有使用MVP或MVVM架构,但Adapter的设计本身就体现了MVC(Model-View-Controller)的思想:

  • Model(模型):NewsBean实体类,封装新闻数据
  • View(视图):XML布局文件(list_item_one.xml、list_item_two.xml)
  • Controller(控制器):NewsAdapter,负责将Model的数据绑定到View上

在更复杂的项目中,可以进一步演进为:

  • MVP:引入Presenter层,处理业务逻辑
  • MVVM:引入ViewModel和DataBinding,实现数据驱动UI

17.5 Adapter的扩展设计模式

模式一:多ViewHolder的工厂模式

当Item类型很多时(比如5种以上),onCreateViewHolder中的if-else会变得冗长。可以使用工厂模式优化:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    return ViewHolderFactory.createViewHolder(mContext, parent, viewType);
}

class ViewHolderFactory {
    static RecyclerView.ViewHolder createViewHolder(Context context, ViewGroup parent, int viewType) {
        switch (viewType) {
            case 1: return new MyViewHolder1(LayoutInflater.from(context).inflate(R.layout.item_type1, parent, false));
            case 2: return new MyViewHolder2(LayoutInflater.from(context).inflate(R.layout.item_type2, parent, false));
            case 3: return new MyViewHolder3(LayoutInflater.from(context).inflate(R.layout.item_type3, parent, false));
            default: throw new IllegalArgumentException("Unknown viewType: " + viewType);
        }
    }
}

模式二:接口回调实现Item点击事件

在Adapter中定义接口,将点击事件传递给Activity/Fragment处理:

public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    public interface OnItemClickListener {
        void onItemClick(int position);
    }
    
    private OnItemClickListener mListener;
    
    public void setOnItemClickListener(OnItemClickListener listener) {
        this.mListener = listener;
    }
    
    // 在onBindViewHolder中设置点击监听
    holder.itemView.setOnClickListener(v -> {
        if (mListener != null) {
            mListener.onItemClick(position);
        }
    });
}

在MainActivity中使用:

mAdapter.setOnItemClickListener(position -> {
    NewsBean news = NewsList.get(position);
    // 跳转到新闻详情页
    Intent intent = new Intent(MainActivity.this, NewsDetailActivity.class);
    intent.putExtra("news_id", news.getId());
    startActivity(intent);
});

第十八章 真实项目中的新闻列表实现方案

18.1 从教学项目到生产项目的差距

HeadLine项目是一个优秀的教学案例,但它与真实的商业项目之间还存在一定的差距。了解这些差距,有助于我们将所学知识应用到实际开发中。

教学项目的特点:

  • 数据硬编码在代码中
  • 图片使用本地drawable资源
  • 不考虑网络异常、数据为空等边界情况
  • 没有分页加载机制
  • 没有下拉刷新功能
  • 没有错误处理和重试机制

生产项目的要求:

  • 数据从网络API获取
  • 图片从网络URL加载
  • 完善的异常处理
  • 分页加载(上拉加载更多)
  • 下拉刷新
  • Loading状态、空状态、错误状态
  • 数据缓存策略

18.2 网络数据对接方案

步骤一:定义API接口

使用Retrofit定义新闻API接口:

public interface NewsApi {
    @GET("api/news/list")
    Call<NewsResponse> getNewsList(
        @Query("category") String category,
        @Query("page") int page,
        @Query("pageSize") int pageSize
    );
}

步骤二:创建Retrofit实例

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build();
NewsApi newsApi = retrofit.create(NewsApi.class);

步骤三:请求数据并更新列表

newsApi.getNewsList("recommend", 1, 20).enqueue(new Callback<NewsResponse>() {
    @Override
    public void onResponse(Call<NewsResponse> call, Response<NewsResponse> response) {
        if (response.isSuccessful() && response.body() != null) {
            NewsList = response.body().getNewsList();
            mAdapter.notifyDataSetChanged();
        }
    }
    
    @Override
    public void onFailure(Call<NewsResponse> call, Throwable t) {
        // 处理网络错误
        showErrorPage();
    }
});

18.3 图片加载方案

生产项目中,新闻图片来自网络URL,需要使用专业的图片加载库。以Glide为例:

添加依赖:

implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'

在Adapter中使用:

// 替换原来的 setImageResource()
Glide.with(mContext)
    .load(bean.getImgUrl())  // 网络图片URL
    .placeholder(R.drawable.placeholder)  // 加载中占位图
    .error(R.drawable.error)  // 加载失败占位图
    .centerCrop()  // 裁剪方式
    .into(holder.iv_img);

Glide的优势:

  1. 自动管理内存缓存和磁盘缓存
  2. 自动处理生命周期,Activity销毁时自动取消加载
  3. 支持GIF、WebP等多种格式
  4. API简洁易用
  5. 自动根据ImageView的尺寸缩放图片,避免OOM

18.4 下拉刷新实现

使用SwipeRefreshLayout实现下拉刷新:

布局修改:

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

Java代码:

SwipeRefreshLayout swipeRefresh = findViewById(R.id.swipe_refresh);
swipeRefresh.setOnRefreshListener(() -> {
    // 执行刷新逻辑
    refreshNewsList();
});

private void refreshNewsList() {
    // 请求最新数据
    loadNewsFromServer(1, new Callback() {
        @Override
        public void onSuccess(List<NewsBean> newsList) {
            NewsList.clear();
            NewsList.addAll(newsList);
            mAdapter.notifyDataSetChanged();
            swipeRefresh.setRefreshing(false); // 停止刷新动画
        }
        
        @Override
        public void onError() {
            swipeRefresh.setRefreshing(false);
            Toast.makeText(MainActivity.this, "刷新失败", Toast.LENGTH_SHORT).show();
        }
    });
}

18.5 上拉加载更多实现

通过RecyclerView的滚动监听实现上拉加载:

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (dy > 0) { // 向下滚动
            LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
            int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
            int totalItemCount = layoutManager.getItemCount();
            
            // 当最后一个可见Item接近列表底部时,加载更多
            if (lastVisibleItem >= totalItemCount - 3 && !isLoading) {
                loadMoreNews();
            }
        }
    }
});

private void loadMoreNews() {
    isLoading = true;
    currentPage++;
    // 请求下一页数据
    loadNewsFromServer(currentPage, new Callback() {
        @Override
        public void onSuccess(List<NewsBean> newsList) {
            int startPosition = NewsList.size();
            NewsList.addAll(newsList);
            mAdapter.notifyItemRangeInserted(startPosition, newsList.size());
            isLoading = false;
        }
    });
}

18.6 多状态页面管理

真实的新闻列表需要管理多种状态:

┌─────────────────────────────────────┐
│           多状态页面管理              │
├─────────────────────────────────────┤
│  Loading → 加载中(转圈动画)         │
│  Success → 正常展示(RecyclerView)   │
│  Empty   → 数据为空(空状态提示)      │
│  Error   → 网络错误(错误提示+重试)   │
└─────────────────────────────────────┘

实现方案:

private void showLoading() {
    loadingView.setVisibility(View.VISIBLE);
    recyclerView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
}

private void showContent() {
    loadingView.setVisibility(View.GONE);
    recyclerView.setVisibility(View.VISIBLE);
    emptyView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
}

private void showEmpty() {
    loadingView.setVisibility(View.GONE);
    recyclerView.setVisibility(View.GONE);
    emptyView.setVisibility(View.VISIBLE);
    errorView.setVisibility(View.GONE);
}

private void showError() {
    loadingView.setVisibility(View.GONE);
    recyclerView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
    errorView.setVisibility(View.VISIBLE);
}

18.7 今日头条真实架构分析

真实的今日头条APP的列表架构远比HeadLine项目复杂,但核心原理是相通的。以下是今日头条新闻列表的核心架构:

┌──────────────────────────────────────────────────────┐
│                  今日头条新闻列表架构                    │
├──────────────────────────────────────────────────────┤
│  UI层                                                 │
│  ├── SwipeRefreshLayout(下拉刷新)                    │
│  ├── RecyclerView(新闻列表)                          │
│  │   ├── LinearLayoutManager(线性布局)                │
│  │   ├── MultiTypeAdapter(多类型适配器)               │
│  │   │   ├── TextItem(纯文本新闻)                    │
│  │   │   ├── SingleImageItem(单图新闻)               │
│  │   │   ├── ThreeImageItem(三图新闻)                │
│  │   │   ├── VideoItem(视频新闻)                     │
│  │   │   ├── AdItem(广告)                           │
│  │   │   └── LargeImageItem(大图新闻)                │
│  │   └── ItemDecoration(分割线)                      │
│  └── LoadingFooter(加载更多Footer)                   │
├──────────────────────────────────────────────────────┤
│  数据层                                                │
│  ├── Repository(数据仓库)                            │
│  │   ├── RemoteDataSource(网络数据源)                │
│  │   └── LocalDataSource(本地缓存数据源)              │
│  ├── NewsBean(新闻实体类)                            │
│  └── PagingConfig(分页配置)                          │
├──────────────────────────────────────────────────────┤
│  业务层                                                │
│  ├── NewsViewModel(ViewModel)                       │
│  ├── RefreshUseCase(刷新用例)                        │
│  └── LoadMoreUseCase(加载更多用例)                   │
└──────────────────────────────────────────────────────┘

可以看到,HeadLine项目虽然简单,但它涵盖了UI层最核心的RecyclerView多类型Item实现,这是整个架构的基础。


第十九章 项目总结与扩展思考

19.1 三个项目的知识总结

通过Chapter03的三个递进项目,我们学习了Android列表开发的完整知识体系:

ListView项目(入门):

  • ListView的基本使用
  • BaseAdapter的四个核心方法
  • ViewHolder优化模式
  • convertView复用机制
  • RelativeLayout布局

RecyclerView项目(进阶):

  • RecyclerView的基本使用
  • RecyclerView.Adapter的三个核心方法
  • LayoutManager的配置
  • ViewHolder强制模式
  • 创建与绑定分离的设计思想

HeadLine项目(实战):

  • 多类型Item实现(getItemViewType)
  • 多个ViewHolder管理
  • 面向对象数据封装(NewsBean)
  • 复杂布局设计(4个布局文件)
  • 样式与主题系统
  • 真实业务场景模拟

19.2 HeadLine项目的核心知识点回顾

  1. RecyclerView多类型Item:通过重写getItemViewType()返回不同类型,在onCreateViewHolder()中根据viewType创建不同的ViewHolder,在onBindViewHolder()中根据ViewHolder类型绑定不同的数据。
  2. 布局资源设计:主页面布局(activity_main.xml)、单图Item布局(list_item_one.xml)、三图Item布局(list_item_two.xml)、公共标题栏(title_bar.xml),四个布局文件各司其职。
  3. 控件使用:TextView显示文本、ImageView显示图片、EditText实现搜索、View作为分割线、LinearLayout和RelativeLayout组织布局、RecyclerView展示列表。
  4. 样式复用:通过styles.xml定义tvStyle、tvInfo、ivImg三种样式,在多个控件中复用,减少重复代码。
  5. 数据封装:使用NewsBean实体类封装新闻数据,替代零散数组,提升代码可维护性。

19.3 项目可扩展方向

HeadLine项目虽然完整实现了新闻列表的基本功能,但还有很多可以扩展的方向:

1. 下拉刷新与上拉加载

  • 使用SwipeRefreshLayout实现下拉刷新
  • 使用RecyclerView的滚动监听实现上拉加载更多

2. 点击事件处理

  • 为每个Item添加点击事件监听
  • 点击后跳转到新闻详情页

3. 网络数据对接

  • 使用Retrofit请求新闻API
  • 使用Gson解析JSON数据
  • 使用Glide加载网络图片

4. 分类标签交互

  • 实现分类标签的点击切换
  • 根据选中的分类加载对应的新闻

5. 搜索功能

  • 实现搜索框的输入监听
  • 根据关键词过滤新闻列表

6. 数据库缓存

  • 使用Room数据库缓存新闻数据
  • 实现离线阅读功能

19.4 从教学到实战的建议

HeadLine项目是一个优秀的教学案例,它展示了RecyclerView多类型Item的核心实现。但从教学项目到真实的生产项目,还需要注意以下几点:

  1. 数据源:教学项目使用硬编码数据,真实项目需要从网络API获取
  2. 图片加载:教学项目使用setImageResource(),真实项目需要使用Glide等图片加载库
  3. 错误处理:教学项目不考虑异常情况,真实项目需要处理网络错误、数据异常等
  4. 分页加载:教学项目数据量小,真实项目需要实现分页加载避免一次性加载过多数据
  5. 空状态处理:当没有数据时,需要显示空状态提示页面
  6. 加载状态:数据加载中需要显示Loading动画
  7. 性能监控:真实项目需要监控列表的帧率、内存占用等性能指标

19.5 Android列表开发的未来趋势

随着Android开发的不断演进,列表开发方式也在发生变化:

Jetpack Compose的LazyColumn

Jetpack Compose是Google推出的声明式UI框架,其中的LazyColumn相当于RecyclerView的Compose版本:

@Composable
fun NewsList(newsList: List<NewsBean>) {
    LazyColumn {
        items(newsList) { news ->
            NewsItem(news)
        }
    }
}

@Composable
fun NewsItem(news: NewsBean) {
    when (news.type) {
        1 -> SingleImageNewsItem(news)
        2 -> ThreeImageNewsItem(news)
    }
}

Compose的优势:

  • 代码更简洁,不需要Adapter和ViewHolder
  • 声明式语法,UI状态自动同步
  • 更好的类型安全

但RecyclerView在短期内仍将是主流,因为:

  • 大量存量项目使用RecyclerView
  • Compose的学习成本较高
  • RecyclerView的生态系统更加成熟

19.6 学习建议

对于想要深入学习Android列表开发的读者,建议按照以下路径进阶:

第一阶段:基础掌握

  • 完成Chapter03的三个项目
  • 理解ListView和RecyclerView的基本用法
  • 掌握ViewHolder模式和Adapter设计

第二阶段:实战练习

  • 尝试实现下拉刷新和上拉加载
  • 添加Item点击事件处理
  • 使用Glide加载网络图片

第三阶段:架构升级

  • 学习MVVM架构
  • 使用Retrofit进行网络请求
  • 使用Room进行本地缓存

第四阶段:性能优化

  • 学习DiffUtil局部刷新
  • 优化布局层级
  • 监控列表帧率和内存占用

19.7 结语

通过本章对HeadLine项目的全面剖析,我们不仅掌握了RecyclerView多类型Item的实现方法,还深入理解了Android列表组件的演进历程、布局资源的设计原则、控件的使用技巧以及性能优化的最佳实践。

从ListView到RecyclerView,从平行数组到实体类封装,从单一布局到多类型Item,这三个递进式项目构成了一条清晰的学习路径。希望读者能够通过这个项目,建立起完整的Android列表开发知识体系,为后续的真实项目开发打下坚实的基础。

列表开发是Android开发中最基础也是最重要的技能之一。无论是简单的设置页面,还是复杂的新闻资讯页面,都离不开列表组件的使用。掌握了RecyclerView的核心原理,你就掌握了Android列表开发的"内功心法",无论未来技术如何演进,这些核心思想都不会过时。

项目包含三个递进式案例:ListView基础 → RecyclerView基础 → HeadLine实战 适用于Android初学者系统学习列表组件开发