Android实战:基于RecyclerView仿今日头条新闻列表——从ListView到RecyclerView的完整进阶之路
写在前面:本文基于一个完整的Android教学项目(Chapter03),通过三个递进式项目——ListView基础商品列表、RecyclerView基础动物列表、RecyclerView进阶仿今日头条新闻列表——系统讲解Android列表开发的核心技术。文章将以"仿今日头条"HeadLine项目为主线,深入剖析RecyclerView的多类型Item实现、布局资源设计、控件使用技巧,同时对比ListView与RecyclerView的差异,帮助读者建立完整的Android列表开发知识体系。
目录
- 第一章 Android列表组件概述
- 第二章 ListView基础入门——从购物商城项目说起
- 第三章 RecyclerView基础进阶——从动物列表到新闻列表
- 第四章 HeadLine项目整体架构分析
- 第五章 项目工程结构与Gradle配置
- 第六章 布局资源深度解析
- 第七章 控件使用详解
- 第八章 RecyclerView核心机制
- 第九章 NewsAdapter多类型Item实现
- 第十章 NewsBean数据模型设计
- 第十一章 MainActivity主逻辑分析
- 第十二章 样式与主题系统
- 第十三章 图片资源与Drawable管理
- 第十四章 性能优化与最佳实践
- 第十五章 从ListView到RecyclerView的迁移指南
- 第十六章 Android布局系统深度剖析
- 第十七章 Adapter设计模式与架构思维
- 第十八章 真实项目中的新闻列表实现方案
- 第十九章 项目总结与扩展思考
第一章 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有以下显著优势:
- 强制ViewHolder模式:RecyclerView.Adapter强制要求使用ViewHolder,不再需要手动处理convertView的复用逻辑
- 灵活的布局管理:通过LayoutManager,可以轻松实现线性列表、网格列表、瀑布流等多种布局
- 内置动画支持:支持Item的增删动画,可以自定义动画效果
- 局部刷新能力:支持
notifyItemChanged()等局部刷新方法,性能更优 - 装饰器机制:通过ItemDecoration可以方便地添加分割线、边距等装饰效果
第三阶段:现代列表组件(Jetpack Compose时代)
随着声明式UI的兴起,Jetpack Compose提供了LazyColumn和LazyRow等现代列表组件,进一步简化了列表开发的复杂度。但在实际开发中,RecyclerView仍然是目前使用最广泛的列表组件。
1.3 本章教学项目的三个递进案例
本章涉及的Chapter03项目包含了三个递进式的教学案例,它们构成了一个完整的学习路径:
| 项目 | 组件 | 复杂度 | 核心知识点 |
|---|---|---|---|
| ListView | ListView | 入门 | BaseAdapter、ViewHolder、convertView复用 |
| RecyclerView | RecyclerView | 进阶 | RecyclerView.Adapter、LayoutManager、ViewHolder |
| HeadLine | RecyclerView | 实战 | 多类型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)。页面包含两个子元素:
- 标题栏TextView:高度45dp,橙色背景(#FF8F03),白色文字居中显示"购物商城"
- ListView控件:宽度填满父容器,高度自适应(wrap_content)
这里涉及到的关键属性说明:
android:layout_width="match_parent":宽度与父容器相同,即填满整个屏幕宽度android:layout_height="wrap_content":高度根据内容自适应android:orientation="vertical":子元素垂直排列android:gravity="center":内容在控件内居中对齐
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的代码非常简洁,主要做了四件事:
- 定义三个平行数组(titles、prices、icons),分别存储商品名称、价格和图片资源ID
- 加载布局文件
- 找到ListView控件
- 创建并设置自定义适配器
这里使用平行数组来存储数据是一种简单但不推荐的做法。在实际开发中,应该使用实体类来封装数据(就像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;
}
}
四个核心方法详解:
getCount():返回列表总条目数。ListView通过这个方法知道需要显示多少个Item。这里直接返回titles数组的长度,即6条。getItem(int position):返回指定位置的数据对象。在简单场景下返回数组元素即可。getItemId(int position):返回指定位置条目的唯一ID。通常直接返回position即可满足需求。getView(int position, View convertView, ViewGroup parent):这是最核心的方法,负责创建和配置每个列表Item的View。
getView方法的工作原理:
当ListView需要显示一个Item时,就会调用getView方法。这个方法接收三个参数:
position:当前Item在列表中的位置(从0开始)convertView:已经滚出屏幕的旧Item View,可以拿来复用。如果为null,说明没有可复用的Viewparent:父容器(即ListView本身)
convertView复用机制:
这是ListView性能优化的关键。当用户滚动列表时,滚出屏幕的Item View不会被销毁,而是被放入回收池中。当新的Item需要显示时,ListView会先从回收池中查找可用的convertView。如果有,就直接复用;如果没有(convertView为null),才创建新的View。
ViewHolder模式:
ViewHolder是一个内部类,用来缓存Item中的子控件引用。它的作用是避免每次调用getView时都执行findViewById。因为findViewById需要遍历整个View树来查找控件,是一个相对耗时的操作。通过ViewHolder,我们只在创建新View时执行一次findViewById,之后复用ViewHolder中缓存的引用。
ViewHolder的工作流程:
- 当convertView为null时,创建ViewHolder,执行findViewById,然后通过
convertView.setTag(holder)将ViewHolder绑定到View上 - 当convertView不为null时,通过
convertView.getTag()直接获取之前保存的ViewHolder - 使用ViewHolder中的引用来设置数据
2.5 ListView的局限性
虽然ListView能够完成基本的列表展示任务,但它存在以下局限性:
- 需要手动处理视图复用:开发者必须自己判断convertView是否为null,自己管理ViewHolder,容易出错
- 布局方式单一:只能实现垂直列表,如果需要网格或瀑布流,需要换用GridView或StaggeredGridView
- 没有内置动画:Item的增删没有默认动画效果
- 刷新机制粗糙:只能通过
notifyDataSetChanged()全局刷新,无法局部刷新 - 扩展性差:难以自定义分割线、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采用了职责分离的设计思想:
| 职责 | ListView | RecyclerView |
|---|---|---|
| 视图创建与绑定 | 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包中的组件。
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的三个核心方法:
onCreateViewHolder(ViewGroup parent, int viewType):创建ViewHolder。当RecyclerView需要新的ViewHolder时调用这个方法。这里通过LayoutInflater加载recycler_item.xml布局,然后创建MyViewHolder实例。注意第三个参数false表示不立即将View添加到parent中,因为RecyclerView会在合适的时机自动添加。onBindViewHolder(MyViewHolder holder, int position):绑定数据。当ViewHolder需要显示数据时调用。这里直接通过holder中的控件引用设置数据,不需要再findViewById。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条不同类型的新闻,包括:
- 置顶无图新闻(第一条)
- 单图新闻(第二、四、六条)
- 三图新闻(第三、五条)
这个项目的核心价值在于:
- 多类型Item实现:同一个列表中混合展示不同布局的Item
- 面向对象数据封装:使用NewsBean实体类替代零散数组
- 真实业务场景模拟:模拟了新闻APP的常见数据结构和展示逻辑
4.2 三个项目的递进关系
理解HeadLine项目的最佳方式是将其与前两个项目对比:
| 维度 | ListView项目 | RecyclerView项目 | HeadLine项目 |
|---|---|---|---|
| 列表组件 | ListView | RecyclerView | RecyclerView |
| 数据类型 | 平行数组 | 平行数组 | NewsBean实体类 |
| Item类型 | 单一类型 | 单一类型 | 多类型(3种) |
| 布局文件 | 1个Item布局 | 1个Item布局 | 2个Item布局 |
| ViewHolder | 手动实现 | 1个ViewHolder | 2个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项目后,用户将看到一个仿今日头条的新闻列表页面:
- 顶部标题栏:红色背景,左侧显示"仿今日头条"文字,右侧有一个灰色圆角搜索框
- 分类标签栏:白色背景,水平排列多个分类标签(推荐、抗疫、小视频、北京、视频、热点、娱乐),其中"推荐"标签为红色高亮
- 新闻列表:浅灰色背景,包含6条新闻:
- 第一条:置顶新闻,无图片,左侧显示标题和来源信息,左上角有"置顶"标签
- 第二条:单图新闻,左侧文字+右侧一张图片
- 第三条:三图新闻,标题在上方,下方三张等宽图片,底部显示来源信息
- 第四条:单图新闻(同第二条样式)
- 第五条:三图新闻(同第三条样式)
- 第六条:单图新闻(同第二条样式)
第五章 项目工程结构与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 1和versionName "1.0"
- versionCode:内部版本号,整数,每次发布新版本必须递增
- versionName:展示给用户的版本号,字符串,如"1.0"、"2.1.0"
dependencies依赖项详解:
| 依赖 | 版本 | 作用 |
|---|---|---|
fileTree | - | 引入libs目录下的所有.jar文件 |
appcompat | 1.6.1 | AndroidX兼容库,提供AppCompatActivity等组件 |
constraintlayout | 2.1.4 | 约束布局库(虽然本项目未直接使用) |
junit | 4.13.2 | 单元测试框架 |
runner | 1.5.2 | Android测试运行器 |
espresso-core | 3.5.1 | UI自动化测试框架 |
recyclerview | 1.3.2 | RecyclerView组件库,本项目核心依赖 |
其中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_nameandroid:supportsRtl="true":支持从右到左的布局(如阿拉伯语)android:theme:应用主题,使用NoActionBar隐藏系统默认ActionBar,因为项目自定义了标题栏
Activity声明:
android:name:Activity的完整类名android:exported="true":允许其他应用启动此Activity(因为它是LAUNCHER)intent-filter:声明此Activity为应用入口MAINaction:主入口LAUNCHERcategory:显示在桌面应用列表中
第六章 布局资源深度解析
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,包含两个子元素:
- APP名称TextView:
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: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, 右侧新闻图片)
关键设计点:
- 固定高度90dp:单图Item的高度固定为90dp,保证列表的整齐统一
- 底部间距8dp:
android:layout_marginBottom="8dp"让每条新闻之间有适当的间隔 - 白色背景:与页面的浅灰色背景形成对比,产生卡片效果
- 标题宽度280dp:限制标题宽度,为右侧图片留出空间
- 标题最多2行:
android:maxLines="2"防止标题过长影响布局 - 置顶标签:
iv_top默认显示,在Java代码中根据position控制显示/隐藏 - 图片在右侧:
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布局设计原则:
- 选择合适的布局类型:
- 简单线性排列 → LinearLayout
- 相对定位 → RelativeLayout
- 复杂约束 → ConstraintLayout
- 减少布局嵌套层级:嵌套越深,测量和布局耗时越长。HeadLine项目中最多嵌套3层,是一个合理的深度。
- 使用样式复用:通过styles.xml定义公共样式,避免在每个控件中重复设置相同的属性。
- 使用布局复用:通过
<include>标签复用标题栏布局。 - 合理使用margin和padding:
- margin:控件外边距,控制控件之间的距离
- padding:控件内边距,控制内容与控件边缘的距离
- 使用资源引用:颜色、字符串等使用资源引用而非硬编码,便于统一管理和多语言支持。
第七章 控件使用详解
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行及以后的文字会被截断。这个属性对于新闻标题非常重要,因为:
- 保证列表Item的高度可控
- 避免超长标题导致的布局错乱
- 提升视觉一致性
7.3 ImageView——图片显示的核心控件
ImageView用于在界面上显示图片资源。HeadLine项目中使用了多个ImageView来展示新闻图片、置顶标签等。
7.3.1 ImageView在HeadLine中的使用场景
| 位置 | ID | 用途 | 图片来源 |
|---|---|---|---|
| 单图Item | iv_top | 置顶标签图标 | @drawable/top |
| 单图Item | iv_img | 新闻配图 | 动态设置(Java代码) |
| 三图Item | iv_img1 | 新闻图片1 | 动态设置(Java代码) |
| 三图Item | iv_img2 | 新闻图片2 | 动态设置(Java代码) |
| 三图Item | iv_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的配置分为三步:
- 找到控件实例
- 设置LayoutManager(布局管理器)
- 设置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的核心组件之一,负责:
- 测量和布局Item
- 管理Item的滚动
- 处理Item的可见性
- 决定何时回收不再可见的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 BaseAdapter | RecyclerView.Adapter | 说明 |
|---|---|---|
| getCount() | getItemCount() | 返回Item总数 |
| getView() | onCreateViewHolder() + onBindViewHolder() | 创建View并绑定数据 |
| - | getItemViewType() | 返回Item类型(多类型时重写) |
| - | onCreateViewHolder(parent, viewType) | 根据类型创建ViewHolder |
RecyclerView.Adapter的生命周期:
- RecyclerView首次展示时,调用
getItemCount()获取总数 - 对于每个可见Item,调用
getItemViewType()获取类型 - 根据类型调用
onCreateViewHolder()创建ViewHolder - 调用
onBindViewHolder()绑定数据到ViewHolder - 滚动时,复用已有的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的工作流程:
- RecyclerView需要显示一个Item时,先检查回收池中是否有可用的ViewHolder
- 如果有,直接复用;如果没有,调用
onCreateViewHolder()创建新的ViewHolder - 拿到ViewHolder后,调用
onBindViewHolder()将数据绑定到ViewHolder中的控件上 - 当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更加精细和高效。它使用了多级缓存策略:
四级缓存体系:
- mAttachedScrap:屏幕内的ViewHolder缓存,用于数据集不变时的快速复用(如notifyItemChanged)
- mCachedViews:屏幕外的ViewHolder缓存,默认2个,保存完整的数据状态
- ViewCacheExtension:开发者自定义的缓存层,一般不使用
- 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布局的标准方式。它有三个参数:
resource:布局资源ID(如R.layout.list_item_one)root:父容器(parent),用于生成正确的LayoutParamsattachToRoot:是否立即添加到父容器中。这里传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:置顶标签ImageViewiv_img:新闻图片ImageViewtitle:标题TextViewname:来源TextViewcomment:评论数TextViewtime:发布时间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:三张新闻图片ImageViewtitle/name/comment/time:与MyViewHolder1相同的文本控件
9.8 多类型Item的完整数据流
理解NewsAdapter的工作流程,最好的方式是追踪一条新闻从数据到展示的完整路径:
以第三条新闻(三图新闻)为例:
1. RecyclerView需要显示第3个Item(position=2)
↓
2. 调用getItemViewType(2) → 返回NewsList.get(2).getType() → 返回2
↓
3. 调用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() → 返回1
↓
3. 调用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中可以工作,但在实际项目中存在严重问题:
- 数据分散:一条新闻的信息分散在多个数组中,难以管理
- 容易出错:数组长度不一致会导致数据错位
- 难以扩展:新增字段需要新增数组,修改逻辑复杂
- 不符合面向对象思想:数据没有封装,缺乏类型安全
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,负责:
- 加载页面布局
- 组装新闻数据
- 初始化RecyclerView
- 设置适配器
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);
}
执行流程:
super.onCreate(savedInstanceState):调用父类的onCreate方法,完成Activity的基础初始化setContentView(R.layout.activity_main):加载主页面布局setData():组装新闻数据,将零散的数组合并为NewsBean对象列表findViewById(R.id.rv_list):找到RecyclerView控件setLayoutManager(new LinearLayoutManager(this)):设置线性布局管理器new NewsAdapter(MainActivity.this, NewsList):创建适配器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)。在完整的项目中,应该为不同密度的屏幕提供不同尺寸的图片:
| 密度 | 目录 | 缩放比例 | 适用屏幕 |
|---|---|---|---|
| ldpi | drawable-ldpi | 0.75x | 低密度屏幕 |
| mdpi | drawable-mdpi | 1.0x | 中密度屏幕 |
| hdpi | drawable-hdpi | 1.5x | 高密度屏幕 |
| xhdpi | drawable-xhdpi | 2.0x | 超高密度屏幕 |
| xxhdpi | drawable-xxhdpi | 3.0x | 超超高密度屏幕 |
| xxxhdpi | drawable-xxxhdpi | 4.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,这时需要使用专业的图片加载库:
| 图片加载库 | 特点 | 适用场景 |
|---|---|---|
| Glide | Google推荐,API简洁,自动缓存 | 大多数场景 |
| Picasso | Square出品,简单易用 | 简单图片加载 |
| Fresco | Facebook出品,内存优化最好 | 大量图片场景 |
| Coil | Kotlin协程优先,轻量级 | 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。
官方推荐的原因:
- 性能优势:RecyclerView内置了更高效的回收机制,四级缓存体系比ListView的两级缓存更加精细
- 灵活性:通过LayoutManager可以轻松切换不同的布局方式,而ListView只能实现垂直列表
- 动画支持:RecyclerView内置ItemAnimator,支持增删动画,ListView完全没有内置动画
- 局部刷新:RecyclerView支持
notifyItemChanged()、notifyItemInserted()等局部刷新方法,ListView只能全局刷新 - 扩展性: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 迁移后的收益
完成迁移后,你将获得以下收益:
- 代码更简洁:不再需要手动处理convertView复用逻辑,代码量减少约30%
- 性能更稳定:RecyclerView的回收机制更加精细,滑动流畅度提升
- 功能更丰富:可以轻松实现网格布局、瀑布流、Item动画等高级功能
- 维护更方便:职责分离的设计让代码结构更清晰,便于后续扩展
- 面向未来: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绘制到屏幕上。绘制过程也是"自上而下"的,但绘制顺序是"自下而上"——先绘制子元素,再绘制父元素(这样父元素的背景才不会覆盖子元素)。
绘制流程:
- 绘制背景(drawBackground)
- 绘制内容(onDraw)
- 绘制子元素(dispatchDraw)
- 绘制装饰(如滚动条)(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的设计思路:
这个布局采用了经典的"顶部固定+内容填充"的设计模式:
- 顶部固定区域:标题栏(通过include引入)
- 中间固定区域:分类标签栏
- 分隔区域:1dp分割线
- 内容填充区域:RecyclerView
这种设计模式在新闻类APP中非常常见,它的优点是结构清晰、层次分明。
list_item_one.xml的设计思路:
单图Item采用了"左文右图"的经典新闻列表布局:
- 左侧:标题 + 底部信息(来源、评论、时间)
- 右侧:新闻配图
这种布局的优点是信息密度适中,用户可以快速浏览标题和缩略图,找到感兴趣的新闻。
list_item_two.xml的设计思路:
三图Item采用了"上图下文"的布局:
- 顶部:标题
- 中间:三张等宽图片
- 底部:来源、评论、时间
这种布局适合展示图片内容丰富的新闻,三张图片可以给用户更多的视觉信息,帮助判断是否感兴趣。
16.4 dp、sp、px的区别
在Android布局中,尺寸单位的选择非常重要:
| 单位 | 全称 | 特点 | 使用场景 |
|---|---|---|---|
| dp/dip | Density-independent Pixels | 与屏幕密度无关 | 控件宽高、边距 |
| sp | Scale-independent Pixels | 与屏幕密度和用户字体偏好有关 | 文字大小 |
| px | Pixels | 物理像素 | 一般不推荐使用 |
为什么使用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类型设置数据
架构对比分析:
| 维度 | BaseAdapter | RecyclerView.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的优势:
- 自动管理内存缓存和磁盘缓存
- 自动处理生命周期,Activity销毁时自动取消加载
- 支持GIF、WebP等多种格式
- API简洁易用
- 自动根据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项目的核心知识点回顾
- RecyclerView多类型Item:通过重写
getItemViewType()返回不同类型,在onCreateViewHolder()中根据viewType创建不同的ViewHolder,在onBindViewHolder()中根据ViewHolder类型绑定不同的数据。 - 布局资源设计:主页面布局(activity_main.xml)、单图Item布局(list_item_one.xml)、三图Item布局(list_item_two.xml)、公共标题栏(title_bar.xml),四个布局文件各司其职。
- 控件使用:TextView显示文本、ImageView显示图片、EditText实现搜索、View作为分割线、LinearLayout和RelativeLayout组织布局、RecyclerView展示列表。
- 样式复用:通过styles.xml定义tvStyle、tvInfo、ivImg三种样式,在多个控件中复用,减少重复代码。
- 数据封装:使用NewsBean实体类封装新闻数据,替代零散数组,提升代码可维护性。
19.3 项目可扩展方向
HeadLine项目虽然完整实现了新闻列表的基本功能,但还有很多可以扩展的方向:
1. 下拉刷新与上拉加载
- 使用SwipeRefreshLayout实现下拉刷新
- 使用RecyclerView的滚动监听实现上拉加载更多
2. 点击事件处理
- 为每个Item添加点击事件监听
- 点击后跳转到新闻详情页
3. 网络数据对接
- 使用Retrofit请求新闻API
- 使用Gson解析JSON数据
- 使用Glide加载网络图片
4. 分类标签交互
- 实现分类标签的点击切换
- 根据选中的分类加载对应的新闻
5. 搜索功能
- 实现搜索框的输入监听
- 根据关键词过滤新闻列表
6. 数据库缓存
- 使用Room数据库缓存新闻数据
- 实现离线阅读功能
19.4 从教学到实战的建议
HeadLine项目是一个优秀的教学案例,它展示了RecyclerView多类型Item的核心实现。但从教学项目到真实的生产项目,还需要注意以下几点:
- 数据源:教学项目使用硬编码数据,真实项目需要从网络API获取
- 图片加载:教学项目使用
setImageResource(),真实项目需要使用Glide等图片加载库 - 错误处理:教学项目不考虑异常情况,真实项目需要处理网络错误、数据异常等
- 分页加载:教学项目数据量小,真实项目需要实现分页加载避免一次性加载过多数据
- 空状态处理:当没有数据时,需要显示空状态提示页面
- 加载状态:数据加载中需要显示Loading动画
- 性能监控:真实项目需要监控列表的帧率、内存占用等性能指标
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初学者系统学习列表组件开发