HeadLine仿今日头条项目:RecyclerView深度解析与布局全解

3 阅读25分钟

前言

在Android开发的学习过程中,掌握RecyclerView的使用是每个开发者必须跨越的门槛。HeadLine仿今日头条项目虽然看起来是一个简单的新闻列表页面,但它几乎涵盖了Android初级开发中最重要的知识点:列表控件使用、多布局实现、数据绑定、控件复用、布局编写等。本文将从项目架构、RecyclerView实现原理、布局资源分析、控件使用详解等多个维度,对这个项目进行全面深入的剖析。

一、项目整体架构与技术选型

1.1 项目概述

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

  • 置顶新闻条目(无图+置顶标签)
  • 单张图片新闻条目
  • 三张图片新闻条目
  • 横向滑动的频道标签栏
  • 流畅的列表滚动体验

1.2 技术选型分析

为什么选择RecyclerView而不是ListView?

这是很多初学者会问的问题。RecyclerView作为Android 5.0之后推出的高级列表控件,相较于ListView具有显著优势:

表格

对比维度ListViewRecyclerView
性能表现基础性能良好,但优化空间有限内置ViewHolder强制使用,性能优化更彻底
布局灵活性仅支持列表布局支持垂直、水平、网格、瀑布流等多种布局
动画效果动画支持有限内置ItemAnimator,支持丰富的添加、删除、移动动画
扩展性扩展能力有限高度模块化,可自定义LayoutManager、ItemDecoration等
多布局实现需要自己处理 getViewType原生支持多布局,实现更加简洁优雅

RecyclerView的核心优势:

  1. 强制使用ViewHolder模式:ListView虽然也支持ViewHolder,但不是强制的,很多开发者会忽略这个优化点。RecyclerView强制使用ViewHolder,从根本上减少了findViewById的调用次数。
  2. 内置的布局管理器:通过LayoutManager的抽象,RecyclerView将布局逻辑与数据展示逻辑完全分离,使得实现网格、瀑布流等复杂布局变得非常简单。
  3. 四级缓存机制:RecyclerView采用了比ListView更复杂的缓存策略,包括mAttachedScrap、mChangedScrap、mCachedViews、ViewPool四级缓存,大幅提升了滚动性能。
  4. 完善的API设计:通知数据变更的API更加精细,支持notifyItemInserted、notifyItemRemoved、notifyItemChanged等局部刷新,避免了全局刷新带来的性能损耗。

二、RecyclerView核心机制深度解析

2.1 RecyclerView的基本使用流程

在HeadLine项目中,RecyclerView的使用遵循以下标准流程:

// 1. 在布局文件中引入RecyclerView
<android.support.v7.widget.RecyclerView
    android:id="@+id/rv_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

// 2. 在Activity中找到这个控件
RecyclerView rv_list = findViewById(R.id.rv_list);

// 3. 设置布局管理器
rv_list.setLayoutManager(new LinearLayoutManager(this));

// 4. 准备数据集合
List<NewsBean> newsList = initData();

// 5. 写适配器Adapter
NewsAdapter adapter = new NewsAdapter(this, newsList);

// 6. 创建适配器并设置给RecyclerView
rv_list.setAdapter(adapter);

这个看似简单的过程,背后蕴含着Android View体系的深刻原理。接下来我们逐一分析每个步骤的作用。

2.2 LinearLayoutManager的作用与原理

LinearLayoutManager是RecyclerView最常用的布局管理器,它负责决定Item在屏幕上的排列方式和滚动方向。

LinearLayoutManager的核心职责:

  1. 测量与布局:计算每个Item的位置和大小
  2. 滚动处理:处理用户的滑动事件,计算滚动距离
  3. 复用策略:在滚动过程中决定哪些Item需要回收,哪些需要重新绑定

为什么必须设置LayoutManager?

RecyclerView本身并不知道如何排列Item,这个职责完全由LayoutManager承担。这种设计遵循了单一职责原则,使得RecyclerView可以专注于Item的复用和数据绑定,而将布局逻辑委托给专门的组件。

2.3 适配器模式的完美体现

RecyclerView.Adapter是适配器模式在Android开发中的典型应用。它充当了"数据"与"视图"之间的桥梁:

Adapter的核心作用:

  1. 创建ViewHolder:根据position创建或复用ViewHolder
  2. 绑定数据:将数据模型绑定到ViewHolder中的各个控件
  3. 确定类型:判断当前position应该显示哪种类型的布局

在HeadLine项目中,NewsAdapter类的定义如下:

public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private Context mContext;
    private List<NewsBean> NewsList;
    
    // 两个内部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);
        }
    }
    
    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);
        }
    }
}

2.4 多布局实现的深度剖析

多布局是HeadLine项目的核心技术亮点,它实现在同一个RecyclerView中展示不同样式的条目。这涉及到RecyclerView.Adapter中的几个关键方法:

2.4.1 getItemViewType方法的作用

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

这个方法是多布局实现的核心。它的职责是告诉RecyclerView,在position位置应该显示哪种类型的布局。

在HeadLine项目中,NewsBean类有一个type字段:

  • type = 1:表示单图布局(包括置顶新闻和普通单图新闻)
  • type = 2:表示三图布局

为什么需要这个方法?

RecyclerView内部通过viewType来区分不同类型的ViewHolder。相同viewType的ViewHolder可以相互复用,不同viewType的ViewHolder不能复用。这个设计确保了布局的正确性,避免将单图布局的数据绑定到三图布局的ViewHolder上。

2.4.2 onCreateViewHolder方法的多分支处理

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView;
    RecyclerView.ViewHolder holder;
    
    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;
}

这个方法的执行时机是:当RecyclerView需要一个新的ViewHolder时(即缓存中没有可用的ViewHolder时)。

LayoutInflater的工作原理:

LayoutInflater.from(mContext).inflate()这个调用完成了XML布局文件到View对象的转换过程:

  1. 解析XML文件,构建DOM树
  2. 根据XML标签创建对应的View对象
  3. 设置View的各种属性(宽度、高度、ID等)
  4. 建立View的父子关系

第三个参数parent的作用:

第三个参数false的含义是不将新创建的View直接添加到parent中,但仍使用parent的LayoutParams。这样做的好处是:

  • View的测量可以正确进行(因为有parent作为参考)
  • RecyclerView可以完全控制View的添加时机
  • 避免了重复添加导致的异常

2.4.3 onBindViewHolder方法的数据绑定

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    NewsBean bean = NewsList.get(position);
    
    if (holder instanceof MyViewHolder1) {
        MyViewHolder1 holder1 = (MyViewHolder1) holder;
        
        // 设置标题
        holder1.title.setText(bean.getTitle());
        
        // 设置作者、评论、时间
        holder1.name.setText(bean.getName());
        holder1.comment.setText(bean.getComment());
        holder1.time.setText(bean.getTime());
        
        // 处理置顶逻辑
        if (position == 0) {
            holder1.iv_top.setVisibility(View.VISIBLE);
            holder1.iv_img.setVisibility(View.GONE);
        } else {
            holder1.iv_top.setVisibility(View.GONE);
            holder1.iv_img.setVisibility(View.VISIBLE);
            holder1.iv_img.setImageResource(bean.getImList().get(0));
        }
        
    } else if (holder instanceof MyViewHolder2) {
        MyViewHolder2 holder2 = (MyViewHolder2) holder;
        
        // 设置标题
        holder2.title.setText(bean.getTitle());
        
        // 设置作者、评论、时间
        holder2.name.setText(bean.getName());
        holder2.comment.setText(bean.getComment());
        holder2.time.setText(bean.getTime());
        
        // 设置三张图片
        holder2.iv_img1.setImageResource(bean.getImList().get(0));
        holder2.iv_img2.setImageResource(bean.getImList().get(1));
        holder2.iv_img3.setImageResource(bean.getImList().get(2));
    }
}

数据绑定的核心原则:

  1. 完整状态设置:无论当前数据是否要求显示某个控件,都要明确设置其状态。比如即使不需要显示置顶图标,也要调用setVisibility(View.GONE)。
  2. 类型安全转换:使用instanceof判断Holder类型,确保类型转换的安全性。
  3. 复用状态重置:因为ViewHolder会被复用,所以每次绑定都要设置所有可能变化的属性,避免显示错误的数据。

2.5 ViewHolder复用机制的深度分析

RecyclerView的ViewHolder复用机制是其高性能的核心,这个机制通过四级缓存实现:

2.5.1 四级缓存详解

表格

缓存层级名称用途是否需要重新绑定
一级mAttachedScrap布局时从屏幕分离但仍attached的ViewHolder若position/itemId匹配则不需要
一级mChangedScrap被标记为"已变化"的ViewHolder需要重新绑定
二级mCachedViews滑动时刚移出屏幕的ViewHolder若position/itemId匹配则不需要
三级mViewCacheExtension开发者自定义缓存由实现决定
四级RecycledViewPool按viewType存储的缓存池需要重新绑定

缓存查找顺序:

RecyclerView通过tryGetViewHolderForPositionByDeadline方法按以下顺序查找ViewHolder:

  1. 先从mAttachedScrap中查找(按position或itemId)
  2. 再从mChangedScrap中查找
  3. 然后从mCachedViews中查找
  4. 接着查找mViewCacheExtension(如果开发者设置了)
  5. 最后从RecycledViewPool中查找
  6. 如果都没找到,则创建新的ViewHolder

2.5.2 复用过程中的常见问题

问题1:图片错位显示

原因:ViewHolder被复用时,如果图片加载是异步的,可能在回调时ViewHolder已经被复用到其他position,导致图片显示到错误的Item上。

解决方案:

// 在onBindViewHolder中,先取消之前的图片加载请求
Glide.with(mContext).clear(holder.iv_img);

// 然后加载新的图片
Glide.with(mContext)
     .load(bean.getImageUrl())
     .into(holder.iv_img);

问题2:状态显示错误

原因:ViewHolder被复用时,保留了上一个Item的状态,而新数据没有完全重置所有状态。

解决方案:在onBindViewHolder中必须设置所有可能变化的控件状态,包括visibility、enabled、checked等属性。

问题3:数组越界异常

原因:在复用过程中,数据集合可能发生了变化,导致position对应的索引越界。

解决方案:在onBindViewHolder中添加边界检查:

if (position >= 0 && position < NewsList.size()) {
    NewsBean bean = NewsList.get(position);
    // 绑定数据
}

2.6 性能优化策略

2.6.1 setHasFixedSize的使用

rv_list.setHasFixedSize(true);

这个方法的作用是告诉RecyclerView:每个Item的大小都是固定的。这样RecyclerView在计算布局时就不需要重新测量每个Item的大小,可以显著提升性能。

使用场景:

  • 当你的Item高度固定时,应该设置这个属性为true
  • 如果Item高度不固定(比如根据内容动态变化),则应该设置为false

2.6.2 ItemDecoration的使用

ItemDecoration用于给RecyclerView的Item添加装饰,比如分割线:

rv_list.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

自定义ItemDecoration可以实现更复杂的装饰效果,比如分组标题、吸顶效果等。

2.6.3 局部刷新代替全局刷新

当数据发生变化时,应该尽量使用局部刷新:

// 全局刷新(性能较差)
adapter.notifyDataSetChanged();

// 局部刷新(性能较好)
adapter.notifyItemChanged(position);
adapter.notifyItemInserted(position);
adapter.notifyItemRemoved(position);
adapter.notifyItemMoved(fromPosition, toPosition);

DiffUtil的使用:

DiffUtil是Android提供的一个工具类,用于计算两个数据集之间的差异,并生成相应的更新操作:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
    @Override
    public int getOldListSize() {
        return oldList.size();
    }
    
    @Override
    public int getNewListSize() {
        return newList.size();
    }
    
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return oldList.get(oldItemPosition).getId() == 
               newList.get(newItemPosition).getId();
    }
    
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
    }
});

diffResult.dispatchUpdatesTo(adapter);

使用DiffUtil可以自动计算出最小化的更新操作,避免不必要的Item重新绑定。

三、项目布局资源深度解析

HeadLine项目的所有布局文件都位于HeadLine\app\src\main\res\layout路径下,主要包括:

  • activity_main.xml:主页面布局
  • title_bar.xml:标题栏布局
  • list_item_one.xml:单图条目布局
  • list_item_two.xml:三图条目布局

3.1 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:orientation="vertical"
    android:background="@color/light_gray_color">
    
    <!-- 标题栏 -->
    <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" />
    
    <!-- 新闻列表 -->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

布局结构分析:

  1. 根布局LinearLayout:采用垂直方向排列,从上到下依次为标题栏、频道栏、RecyclerView。
  2. 标签的使用:通过将标题栏布局引入,这是Android推荐的代码复用方式。
  3. 频道栏LinearLayout:水平方向排列,包含多个TextView作为频道标签。
  4. RecyclerView控件:占据剩余的所有空间,负责展示新闻列表。

布局优化建议:

  1. 减少布局层级:当前的布局层级合理,没有不必要的嵌套。
  2. 使用merge标签:title_bar.xml的根布局可以使用merge标签,进一步减少层级。
  3. 考虑使用ConstraintLayout:如果布局更复杂,ConstraintLayout可以进一步减少布局嵌套。

3.2 title_bar.xml布局分析

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="50dp"
    android:layout_width="match_parent"
    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>

布局特点分析:

  1. 红色背景:使用#d33d3c这个红色背景,完全还原了今日头条的品牌色调。
  2. 水平排列:左侧是标题TextView,右侧是搜索框EditText,采用LinearLayout的水平排列。
  3. 居中对齐:通过layout_gravity="center"和layout_gravity="center_vertical"确保标题和搜索框垂直居中。
  4. 搜索框样式:使用了自定义背景@drawable/search_bg,实现了圆角矩形的搜索框效果。

控件属性详解:

  • layout_gravity vs gravity

    • layout_gravity:控制控件在父容器中的对齐方式
    • gravity:控制控件内部内容的对齐方式
  • margin vs padding

    • margin:控件与外界其他控件的间距
    • padding:控件内部内容与控件边界的间距

3.3 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>

布局结构深度解析:

这个布局实现了两种显示效果:置顶新闻和单图新闻,通过在Adapter中动态控制控件的visibility来实现。

RelativeLayout的使用技巧:

  1. 相对定位

    • android:layout_alignParentBottom="true":控件底部与父容器底部对齐
    • android:layout_toRightOf="@+id/iv_top":控件位于指定控件的右侧
  2. ID的引用:@+id/ll_info表示定义ID,@id/ll_info表示引用已存在的ID。

样式的统一管理:

<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:gravity">center_vertical</item>
    <item name="android:textSize">14sp</item>
    <item name="android:textColor">@color/gray_color</item>
</style>

通过style标签统一管理公共样式,可以:

  • 减少代码重复
  • 方便统一修改样式
  • 提高代码的可维护性

3.4 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"
    android:padding="8dp">
    
    <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>

布局特点分析:

  1. 三张图片的等宽布局:通过layout_weight="1"和layout_width="0dp"的组合,实现了三张图片等宽排列:
<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>

布局权重原理:

在LinearLayout中,当子View设置了layout_weight属性时,剩余空间的分配按照权重比例进行:

  • layout_width="0dp":初始宽度设为0
  • layout_weight="1":参与剩余空间的分配
  • 三个ImageView都设置相同的权重,所以它们平分水平空间

这种技巧在实现等宽、等高布局时非常有用。

  1. 动态高度:list_item_two的根布局高度设置为wrap_content,这样可以适应不同内容的Item高度。

与list_item_one的对比:

表格

特性list_item_onelist_item_two
根布局高度固定90dpwrap_content
图片数量1张3张
图片布局右侧大图底部三张小图并排
标题位置左侧顶部
置顶图标支持不支持

3.5 样式资源的统一管理

项目通过res/values/styles.xml文件统一管理样式:

<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:gravity">center_vertical</item>
    <item name="android:textSize">14sp</item>
    <item name="android:textColor">@color/gray_color</item>
</style>

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

样式继承机制:

Android支持样式继承,可以创建基础样式,然后继承并修改:

<style name="BaseTextStyle" parent="TextAppearance.AppCompat">
    <item name="android:textSize">14sp</item>
    <item name="android:textColor">#333333</item>
</style>

<style name="TitleTextStyle" parent="BaseTextStyle">
    <item name="android:textSize">18sp</item>
    <item name="android:textStyle">bold</item>
</style>

四、核心控件使用详解

4.1 RecyclerView:项目最核心的控件

作用与定位:

RecyclerView是HeadLine项目的核心控件,负责展示所有新闻条目。它位于activity_main.xml布局文件中。

初始化流程:

// 1. 获取RecyclerView实例
RecyclerView rv_list = findViewById(R.id.rv_list);

// 2. 设置布局管理器
rv_list.setLayoutManager(new LinearLayoutManager(this));

// 3. 设置适配器
rv_list.setAdapter(adapter);

常用配置选项:

  1. 固定大小优化
rv_list.setHasFixedSize(true);
  1. 添加分割线
rv_list.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
  1. 设置Item动画
rv_list.setItemAnimator(new DefaultItemAnimator());

4.2 TextView:文本显示控件

在项目中的使用位置:

TextView出现在所有的布局文件中,用于显示:

  • 标题文字(新闻标题)
  • 作者名称
  • 评论数量
  • 发布时间
  • 频道名称
  • 搜索提示文字

常用属性详解:

  1. 文本内容
android:text="仿今日头条"
  1. 文本颜色
android:textColor="@android:color/white"
  1. 文本大小
android:textSize="22sp"
  1. 最大行数
android:maxLines="2"

这个属性确保标题最多显示两行,超出部分用省略号表示。

  1. 文本对齐
android:gravity="center_vertical"

gravity控制控件内部内容的对齐方式。

常用方法:

// 设置文本内容
textView.setText("新闻标题");

// 设置文本颜色
textView.setTextColor(Color.BLACK);

// 设置文本大小
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);

// 控制可见性
textView.setVisibility(View.VISIBLE); // 显示
textView.setVisibility(View.GONE);    // 隐藏(不占用空间)
textView.setVisibility(View.INVISIBLE); // 隐藏(占用空间)

4.3 ImageView:图片显示控件

在项目中的使用位置:

ImageView出现在list_item_one.xml和list_item_two.xml中,用于显示:

  • 新闻封面图
  • 置顶图标
  • 三张新闻图片

常用属性详解:

  1. 图片来源
android:src="@drawable/top"
  1. 缩放类型
android:scaleType="centerCrop"

scaleType的可选值:

  • center:保持原图大小,居中显示
  • centerCrop:保持宽高比,填充整个ImageView
  • fitCenter:保持宽高比,完整显示图片
  • fitXY:拉伸图片以填满ImageView(不保持宽高比)
  1. 调整视图边界
android:adjustViewBounds="true"

这个属性让ImageView根据自己的内容调整边界。

常用方法:

// 设置图片资源
imageView.setImageResource(R.drawable.image);

// 设置图片URI
imageView.setImageURI(uri);

// 设置图片位图
imageView.setImageBitmap(bitmap);

// 控制可见性
imageView.setVisibility(View.VISIBLE);
imageView.setVisibility(View.GONE);

图片优化策略:

  1. 使用Glide加载网络图片
Glide.with(context)
     .load(url)
     .placeholder(R.drawable.placeholder)
     .error(R.drawable.error)
     .into(imageView);
  1. 图片压缩与缓存
  • 使用合适的图片格式(WebP格式比JPEG小30%)
  • 根据设备密度提供不同分辨率的图片
  • 启用磁盘缓存和内存缓存
  1. 异步加载避免卡顿
  • 使用图片加载库(Glide、Picasso、Coil)
  • 避免在主线程进行图片解码

4.4 LinearLayout:线性布局容器

在项目中的使用位置:

LinearLayout出现在所有布局文件中,是最基础的容器控件。

核心属性详解:

  1. 排列方向
android:orientation="vertical"

可选值:

  • vertical:垂直排列
  • horizontal:水平排列
  1. 权重分配
android:layout_weight="1"
android:layout_width="0dp"

权重的使用规则:

  • 在水平排列的LinearLayout中,设置android:layout_width="0dp"
  • 在垂直排列的LinearLayout中,设置android:layout_height="0dp"
  • 权重越大,分配的空间越多

实际应用示例:

在list_item_two.xml中,三张图片使用LinearLayout + layout_weight实现等宽排列:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    
    <ImageView
        android:layout_width="0dp"
        android:layout_height="90dp"
        android:layout_weight="1"/>
    
    <ImageView
        android:layout_width="0dp"
        android:layout_height="90dp"
        android:layout_weight="1"/>
    
    <ImageView
        android:layout_width="0dp"
        android:layout_height="90dp"
        android:layout_weight="1"/>
</LinearLayout>

4.5 RelativeLayout:相对布局容器

在项目中的使用位置:

RelativeLayout出现在list_item_one.xml和list_item_two.xml中,用于实现相对定位。

核心属性详解:

  1. 相对于父容器定位
android:layout_alignParentBottom="true"
android:layout_centerInParent="true"
  1. 相对于其他控件定位
android:layout_toRightOf="@+id/iv_top"
android:layout_below="@+id/tv_title"

使用技巧:

  1. ID的定义与引用
<!-- 定义ID -->
android:id="@+id/tv_title"

<!-- 引用ID -->
android:layout_below="@id/tv_title"
  1. 避免循环依赖:不要让A在B右边,B又在A右边,这会导致布局无法计算。

4.6 HorizontalScrollView:水平滚动容器

在项目中的使用位置:

虽然当前的HeadLine项目中频道栏是使用LinearLayout实现的,但在实际开发中,当频道数量较多时,会使用HorizontalScrollView来实现横向滑动。

核心特点:

  1. 只能包含一个直接子布局
<HorizontalScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        
        <!-- 频道标签 -->
    </LinearLayout>
</HorizontalScrollView>
  1. 支持横向滚动:当内容宽度超过容器宽度时,可以横向滚动查看。

4.7 View:基础视图控件

在项目中的使用位置:

View出现在activity_main.xml中,用于创建分割线:

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

核心作用:

  1. 占位与分割:通过设置背景颜色和尺寸,实现分割线效果。
  2. 布局间隔:在控件之间添加间距。

五、数据模型与数据绑定

5.1 NewsBean实体类设计

public class NewsBean {
    private String title;      // 新闻标题
    private String name;       // 作者名称
    private String comment;    // 评论数量
    private String time;       // 发布时间
    private List<Integer> imList;  // 图片资源列表
    private int type;          // 新闻类型
    
    // Getter和Setter方法
    public String getTitle() {
        return title;
    }
    
    public void setTitle(String title) {
        this.title = title;
    }
    
    // 其他Getter和Setter方法...
    
    public int getType() {
        return type;
    }
    
    public void setType(int type) {
        this.type = type;
    }
    
    public List<Integer> getImList() {
        return imList;
    }
    
    public void setImList(List<Integer> imList) {
        this.imList = imList;
    }
}

实体类设计原则:

  1. 封装性:所有字段都是private,通过public的getter和setter方法访问。
  2. 符合JavaBean规范:拥有无参构造函数和getter/setter方法。
  3. 类型适配性:字段类型与UI控件的需求相匹配。
  4. 扩展性:type字段支持多种新闻类型,便于扩展。

5.2 数据初始化与适配

在MainActivity中,通过initData方法模拟新闻数据:

private void initData() {
    newsList = new ArrayList<>();
    
    String[] titles = {
        "各地餐企齐行动,杜绝餐饮浪费",
        "花菜有人焯水,有人直接炒,都错了,看饭店大厨如何做",
        "睡觉时,双脚突然蹬一下,有踩空感,像从高楼坠落,是咋回事?"
        // 更多标题...
    };
    
    String[] names = {
        "央视新闻客户端",
        "味美食记",
        "民富康健康"
        // 更多作者...
    };
    
    String[] comments = {
        "9884评",
        "18评",
        "78评"
        // 更多评论...
    };
    
    String[] times = {
        "6小时前",
        "刚刚",
        "1小时前"
        // 更多时间...
    };
    
    int[][] images1 = {
        {R.drawable.image1_1},
        {R.drawable.image2_1},
        {R.drawable.image3_1}
        // 更多单图...
    };
    
    int[][] images3 = {
        {R.drawable.image1_1, R.drawable.image1_2, R.drawable.image1_3},
        {R.drawable.image2_1, R.drawable.image2_2, R.drawable.image2_3},
        {R.drawable.image3_1, R.drawable.image3_2, R.drawable.image3_3}
        // 更多三图...
    };
    
    for (int i = 0; i < titles.length; i++) {
        NewsBean bean = new NewsBean();
        bean.setTitle(titles[i]);
        bean.setName(names[i]);
        bean.setComment(comments[i]);
        bean.setTime(times[i]);
        
        if (i == 0) {
            // 第一条:置顶新闻
            bean.setType(1);
            bean.setImList(new ArrayList<Integer>());
        } else if (i % 3 == 0) {
            // 三图新闻
            bean.setType(2);
            List<Integer> imgList = new ArrayList<>();
            imgList.add(images3[i/3][0]);
            imgList.add(images3[i/3][1]);
            imgList.add(images3[i/3][2]);
            bean.setImList(imgList);
        } else {
            // 单图新闻
            bean.setType(1);
            List<Integer> imgList = new ArrayList<>();
            imgList.add(images1[i][0]);
            bean.setImList(imgList);
        }
        
        newsList.add(bean);
    }
}

5.3 数据绑定的最佳实践

1. 完整状态设置原则:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    NewsBean bean = NewsList.get(position);
    
    if (holder instanceof MyViewHolder1) {
        MyViewHolder1 holder1 = (MyViewHolder1) holder;
        
        // ❌ 错误做法:只设置需要显示的内容
        if (bean.isShowImage()) {
            holder1.iv_img.setVisibility(View.VISIBLE);
        }
        
        // ✅ 正确做法:完整设置所有状态
        holder1.iv_img.setVisibility(bean.isShowImage() ? View.VISIBLE : View.GONE);
        holder1.iv_top.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
    }
}

2. 异步操作的正确处理:

// ❌ 错误做法:可能导致图片错位
loadImageAsync(bean.getImageUrl(), new Callback() {
    @Override
    public void onSuccess(Bitmap bitmap) {
        holder.imageView.setImageBitmap(bitmap);
    }
});

// ✅ 正确做法:取消之前的加载任务
Glide.with(mContext).clear(holder.imageView);
Glide.with(mContext)
     .load(bean.getImageUrl())
     .into(holder.imageView);

3. 避免在ViewHolder中存储position:

// ❌ 错误做法:position会变化
class MyViewHolder extends RecyclerView.ViewHolder {
    private int position;
    
    public void setPosition(int position) {
        this.position = position;
    }
}

// ✅ 正确做法:每次使用时重新获取
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    NewsBean bean = NewsList.get(position);
    // 使用bean而不是position
}

六、RecyclerView的高级特性与应用

6.1 ItemDecoration的使用

ItemDecoration用于给RecyclerView的Item添加装饰,包括分割线、分组标题等。

自定义分割线示例:

public class CustomDividerItemDecoration extends RecyclerView.ItemDecoration {
    private Drawable divider;
    private int dividerHeight;
    
    public CustomDividerItemDecoration(Context context, int resId) {
        divider = ContextCompat.getDrawable(context, resId);
        dividerHeight = divider.getIntrinsicHeight();
    }
    
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount - 1; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int top = child.getBottom() + params.bottomMargin;
            int bottom = top + dividerHeight;
            divider.setBounds(left, top, right, bottom);
            divider.draw(c);
        }
    }
    
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.set(0, 0, 0, dividerHeight);
    }
}

6.2 ItemAnimator的使用

ItemAnimator控制RecyclerView中Item添加、删除、移动时的动画效果。

自定义ItemAnimator示例:

public class CustomItemAnimator extends DefaultItemAnimator {
    @Override
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
        if (oldHolder == newHolder) {
            return animateMove(oldHolder, fromX, fromY, toX, toY);
        }
        return super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY);
    }
}

6.3 嵌套RecyclerView的处理

当RecyclerView嵌套时,需要注意滑动冲突和性能优化。

解决滑动冲突:

innerRecyclerView.setNestedScrollingEnabled(false);

共享RecycledViewPool:

RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
outerRecyclerView.setRecycledViewPool(pool);
innerRecyclerView.setRecycledViewPool(pool);

七、性能优化与最佳实践

7.1 布局优化

1. 减少布局嵌套:

使用Hierarchy Viewer或Android Studio的Layout Inspector分析布局层级,减少不必要的嵌套。

2. 使用ConstraintLayout:

ConstraintLayout可以通过约束关系实现复杂的布局,同时保持扁平的层级结构。

3. 使用ViewStub延迟加载:

对于不立即显示的布局,使用ViewStub实现延迟加载:

<ViewStub
    android:id="@+id/stub_import"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout="@layout/import_panel" />

7.2 RecyclerView优化

1. 使用DiffUtil进行增量更新:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback(oldList, newList));
diffResult.dispatchUpdatesTo(adapter);

2. 增加缓存容量:

recyclerView.setItemViewCacheSize(20);

3. 预加载:

recyclerView.setItemViewCacheSize(20);
recyclerView.setDrawingCacheEnabled(true);

7.3 内存优化

1. 及时释放资源:

@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
    super.onViewRecycled(holder);
    if (holder instanceof MyViewHolder1) {
        Glide.with(mContext).clear(((MyViewHolder1) holder).iv_img);
    }
}

2. 使用弱引用:

对于缓存的数据,使用WeakReference避免内存泄漏。

3. 避免内存泄漏:

及时取消网络请求和异步任务,避免Activity/Fragment的引用泄漏。

八、常见问题与解决方案

8.1 图片显示错乱

问题原因:

ViewHolder被复用时,异步加载的图片在回调时ViewHolder已经被复用到其他position。

解决方案:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    NewsBean bean = NewsList.get(position);
    
    // 先取消之前的加载请求
    Glide.with(mContext).clear(holder.imageView);
    
    // 加载新图片
    Glide.with(mContext)
         .load(bean.getImageUrl())
         .into(holder.imageView);
}

8.2 布局嵌套过深

问题原因:

过度使用布局嵌套会导致渲染性能下降。

解决方案:

  1. 使用ConstraintLayout替代多层嵌套
  2. 使用merge标签减少不必要的层级
  3. 使用ViewStub延迟加载不立即显示的布局

8.3 滑动卡顿

问题原因:

  1. 主线程进行了耗时操作
  2. 布局过于复杂
  3. 图片加载未优化

解决方案:

  1. 将耗时操作放到子线程
  2. 简化布局结构
  3. 使用图片加载库并开启缓存
  4. 开启RecyclerView的固定大小优化

8.4 数据不更新

问题原因:

调用了notifyDataSetChanged()但没有刷新数据,或者notifyItemChanged的position错误。

解决方案:

// 确保在主线程调用
runOnUiThread(new Runnable() {
    @Override
    public void run() {
        adapter.notifyDataSetChanged();
        // 或者使用局部刷新
        adapter.notifyItemChanged(position);
    }
});

九、项目扩展与进阶应用

9.1 添加点击事件

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    NewsBean bean = NewsList.get(position);
    
    if (holder instanceof MyViewHolder1) {
        MyViewHolder1 holder1 = (MyViewHolder1) holder;
        
        // 设置点击事件
        holder1.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(mContext, NewsDetailActivity.class);
                intent.putExtra("news_id", bean.getId());
                mContext.startActivity(intent);
            }
        });
        
        // 设置长按事件
        holder1.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                // 显示删除确认对话框
                return true;
            }
        });
    }
}

9.2 实现下拉刷新和上拉加载

// 使用SwipeRefreshLayout实现下拉刷新
SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swipe_refresh);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
        // 重新加载数据
        loadData();
    }
});

// 使用RecyclerView的OnScrollListener实现上拉加载
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        
        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
        int visibleItemCount = layoutManager.getChildCount();
        int totalItemCount = layoutManager.getItemCount();
        int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
        
        if (!isLoading && !isLastPage) {
            if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
                && firstVisibleItemPosition >= 0
                && totalItemCount >= PAGE_SIZE) {
                loadMoreData();
            }
        }
    }
});

9.3 实现多级缓存策略

public class NewsCacheManager {
    private static NewsCacheManager instance;
    private LruCache<String, NewsBean> memoryCache;
    private DiskLruCache diskCache;
    
    private NewsCacheManager(Context context) {
        // 内存缓存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        memoryCache = new LruCache<String, NewsBean>(cacheSize);
        
        // 磁盘缓存
        File cacheDir = context.getCacheDir();
        try {
            diskCache = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public NewsBean getNews(String newsId) {
        // 先查内存缓存
        NewsBean news = memoryCache.get(newsId);
        if (news != null) {
            return news;
        }
        
        // 再查磁盘缓存
        news = getNewsFromDisk(newsId);
        if (news != null) {
            memoryCache.put(newsId, news);
            return news;
        }
        
        // 最后从网络加载
        return loadNewsFromNetwork(newsId);
    }
}

附录:完整代码示例

A. NewsBean.java完整代码

package com.example.headline;

import java.util.List;

public class NewsBean {
    private String title;
    private String name;
    private String comment;
    private String time;
    private List<Integer> imList;
    private int type;
    
    public NewsBean() {
    }
    
    public NewsBean(String title, String name, String comment, String time, List<Integer> imList, int type) {
        this.title = title;
        this.name = name;
        this.comment = comment;
        this.time = time;
        this.imList = imList;
        this.type = type;
    }
    
    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> getImList() {
        return imList;
    }
    
    public void setImList(List<Integer> imList) {
        this.imList = imList;
    }
    
    public int getType() {
        return type;
    }
    
    public void setType(int type) {
        this.type = type;
    }
}

B. NewsAdapter.java完整代码

package com.example.headline;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;

public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private Context mContext;
    private List<NewsBean> NewsList;
    
    public NewsAdapter(Context context, List<NewsBean> newsList) {
        this.mContext = context;
        this.NewsList = newsList;
    }
    
    @Override
    public int getItemViewType(int position) {
        return NewsList.get(position).getType();
    }
    
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView;
        RecyclerView.ViewHolder holder;
        
        if (viewType == 1) {
            itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_one, parent, false);
            holder = new MyViewHolder1(itemView);
        } else {
            itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item_two, parent, false);
            holder = new MyViewHolder2(itemView);
        }
        
        return holder;
    }
    
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        NewsBean bean = NewsList.get(position);
        
        if (holder instanceof MyViewHolder1) {
            MyViewHolder1 holder1 = (MyViewHolder1) holder;
            
            holder1.title.setText(bean.getTitle());
            holder1.name.setText(bean.getName());
            holder1.comment.setText(bean.getComment());
            holder1.time.setText(bean.getTime());
            
            if (position == 0) {
                holder1.iv_top.setVisibility(View.VISIBLE);
                holder1.iv_img.setVisibility(View.GONE);
            } else {
                holder1.iv_top.setVisibility(View.GONE);
                holder1.iv_img.setVisibility(View.VISIBLE);
                holder1.iv_img.setImageResource(bean.getImList().get(0));
            }
            
        } else if (holder instanceof MyViewHolder2) {
            MyViewHolder2 holder2 = (MyViewHolder2) holder;
            
            holder2.title.setText(bean.getTitle());
            holder2.name.setText(bean.getName());
            holder2.comment.setText(bean.getComment());
            holder2.time.setText(bean.getTime());
            
            holder2.iv_img1.setImageResource(bean.getImList().get(0));
            holder2.iv_img2.setImageResource(bean.getImList().get(1));
            holder2.iv_img3.setImageResource(bean.getImList().get(2));
        }
    }
    
    @Override
    public int getItemCount() {
        return NewsList.size();
    }
    
    class MyViewHolder1 extends RecyclerView.ViewHolder {
        ImageView iv_top, iv_img;
        TextView title, name, comment, time;
        
        public MyViewHolder1(View view) {
            super(view);
            iv_top = view.findViewById(R.id.iv_top);
            iv_img = view.findViewById(R.id.iv_img);
            title = view.findViewById(R.id.tv_title);
            name = view.findViewById(R.id.tv_name);
            comment = view.findViewById(R.id.tv_comment);
            time = view.findViewById(R.id.tv_time);
        }
    }
    
    class MyViewHolder2 extends RecyclerView.ViewHolder {
        ImageView iv_img1, iv_img2, iv_img3;
        TextView title, name, comment, time;
        
        public MyViewHolder2(View view) {
            super(view);
            iv_img1 = view.findViewById(R.id.iv_img1);
            iv_img2 = view.findViewById(R.id.iv_img2);
            iv_img3 = view.findViewById(R.id.iv_img3);
            title = view.findViewById(R.id.tv_title);
            name = view.findViewById(R.id.tv_name);
            comment = view.findViewById(R.id.tv_comment);
            time = view.findViewById(R.id.tv_time);
        }
    }
}