13.RecyclerView

从Android 5.0(API 21)开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活,可以理解RecyclerView是ListView的升级版。

RecyclerView除了像ListView一样可以上下滑动,还可以左右滑动,同样RecyclerView也拥有item回收复用的功能,这一点从它的名字RecyclerView也可以看出。 

1、RecyclerView的基本用法

新建RecyclerViewTest项目。

目前使用的Android Studio版本是Android Studio Dolphin | 2021.3.1,Gradle是7.3.0。直接可以使用RecyclerView控件。

如果我们创建的项目不能直接使用RecyclerView控件,我们就需要先引入依赖库,引入的方法比较多。

【方法1】点击控件右边的下载按钮即可。

【方法2】在app/build.gradle中通过代码添加依赖库,代码如下所示:

dependencies {
    ……
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
}

【方法3】使用图形化的向导,通过查找来添加。

Project视图下,项目名右键选择“Open Module Settings”,打开Project Structure对话框,左侧选择Dependencies。

Step1.搜索“androidx.recyclerview:recyclerview”,选择1.1.0版本。

Step2.选择默认“implementation”

这样也可以完成依赖库的添加。

现在就可以在布局文件中使用<androidx.recyclerview.widget.RecyclerView>标签了。

修改acticity_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">

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

在布局文件中加入RecyclerView控件非常简单,需要注意的是,由于RecyclerView并不是内置在系统SDK中的,所以需要完整的包路径都写出来。

我们为了想要使用RecyclerView来实现和ListView同样的效果,我们就需要准备一份同样的图片

为了简单起见,我们把ListViewTest项目中的图片复制过来。顺便将Animal.java类和item.xml也复制过来。

接下来,我们需要为RecyclerView准备一个适配器,新建AnimalAdapter类,让这个适配器类去继承RecyclerView.Adapter,并将泛型指定为AnimalAdapter.ViewHolder。

其中ViewHolder是我们马上要在AnimalAdapter中定义的一个内部类。代码如下所示:

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class AnimalAdapter extends RecyclerView.Adapter<AnimalAdapter.ViewHolder> {
    private List<Animal> lstAnimal;

    // 内部类ViewHolder
    static class ViewHolder extends RecyclerView.ViewHolder {
        ImageView ivAnimal;
        TextView tvName;

        public ViewHolder(View view) {
            super(view);
            ivAnimal = (ImageView) view.findViewById(R.id.ivAnimal);
            tvName = (TextView) view.findViewById(R.id.tvName);
        }
    }

    // 构造方法
    public AnimalAdapter(List<Animal> lstAnimal) {
        this.lstAnimal = lstAnimal;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Animal animal = lstAnimal.get(position);
        holder.ivAnimal.setImageResource(animal.getImageId());
        holder.tvName.setText(animal.getName());
    }

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

虽然这段代码看上去好像有点长,但其实它比ListView的适配器要更容易理解。

这里我们首先定义了一个内部类ViewHolder,ViewHolder要继承自RecyclerView.ViewHolder。

然后ViewHolder的构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局,那么我们就可以通过findViewById()方法来获取到布局中的ImageView和TextView的实例了。

接着往下看,AnimalAdapter中也有一个构造函数,这个方法用于把要展示的数据源传进来,并赋值给一个全局变量lstAnimal,我们后续的操作都将在这个数据源的基础上进行。

继续往下看,由于AnimalAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法:

  • onCreateViewHolder()方法是用于创建ViewHolder实例的,我们在这个方法中将item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入到构造函数当中,最后将ViewHolder的实例返回。
  • onBindViewHolder()方法是用于对RecyclerView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Animal实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可。
  • getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。

适配器准备好了之后,我们就可以开始使用RecyclerView了,修改MainActivity中的代码,如下所示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;

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

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private List<Animal> lstAnimal = new ArrayList<>();
    private int[] images = {R.drawable.rat, R.drawable.ox, R.drawable.tiger, R.drawable.rabbit, R.drawable.dragon, R.drawable.snake, R.drawable.horse, R.drawable.goat, R.drawable.monkey, R.drawable.rooster, R.drawable.dog, R.drawable.pig};
    private String[] animals = {"子鼠", "丑牛", "寅虎", "卯兔", "辰龙", "巳蛇", "午马", "未羊", "申猴", "酉鸡", "戌狗", "亥猪"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initAnimals();
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        AnimalAdapter adapter = new AnimalAdapter(lstAnimal);
        recyclerView.setAdapter(adapter);
    }

    private void initAnimals() {
        for (int i = 0; i < animals.length; i++) {
            Animal animal = new Animal(animals[i], images[i]);
            lstAnimal.add(animal);
        }
    }
}

可以看到,这里使用了一个同样的initAnimals()方法,用于初始化所有的生肖数据。接着在onCreate()方法中我们先获取到RecyclerView的实例,然后创建了一个LinearLayoutManager对象,并将它设置到RecyclerView当中。

LayoutManager用于指定RecyclerView的布局方式,这里使用的LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。

接下来我们创建了AnimalAdapter的实例,并将生肖数据传入到AnimalAdapter的构造函数中,最后调用RecyclerView的setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成了。

现在可以运行一下程序了,效果如图所示。

可以看到,我们使用RecyclerView实现了和ListView几乎一模一样的效果,虽说在代码量方面并没有明显地减少,但是逻辑变得更加清晰了。

当然这只是RecyclerView的基本用法而已,接下来我们就看一看RecyclerView还能实现哪些ListView实现不了的效果。

2、实现横向滚动

我们已经知道,ListView 的扩展性并不好,它只能实现纵向滚动的效果,如果想进行横向滚动的话,ListView就做不到了。

那么RecyclerView就能做得到吗?当然可以,不仅能做得到,还非常简单,那么接下来我们就尝试实现一下横向滚动的效果。

首先要对item.xml布局进行修改,因为目前这个布局里面的元素是水平排列的,适用于纵向滚动的场景,而如果我们要实现横向滚动的话,应该把item.xml里的元素改成垂直排列才比较合理。

修改item.xml 中的代码,如下所示:

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

    <ImageView
        android:id="@+id/ivAnimal"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_gravity="center_horizontal" />

    <TextView
        android:id="@+id/tvName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />
</LinearLayout>

可以看到,我们将LinearLayout改成垂直方向排列,并把宽度设为100dp。

这里将宽度指定为固定值是因为每种生肖的文字长度不一致,如果用wrap_content的话,RecyclerView的子项就会有长有短,非常不美观;

而如果用match_parent的话,就会导致宽度过长,一个子项占满整个屏幕。

然后我们将ImageView和TextView都设置成了在布局中水平居中,并且使用layout_marginTop属性让文字和图片之间保持一些距离。

接下来修改MainActivity中的代码,如下所示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;

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

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private List<Animal> lstAnimal = new ArrayList<>();
    private int[] images = {R.drawable.rat, R.drawable.ox, R.drawable.tiger, R.drawable.rabbit, R.drawable.dragon, R.drawable.snake, R.drawable.horse, R.drawable.goat, R.drawable.monkey, R.drawable.rooster, R.drawable.dog, R.drawable.pig};
    private String[] animals = {"子鼠", "丑牛", "寅虎", "卯兔", "辰龙", "巳蛇", "午马", "未羊", "申猴", "酉鸡", "戌狗", "亥猪"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initAnimals();
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        // 设置布局的排列方向为水平方向
        layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(layoutManager);
        AnimalAdapter adapter = new AnimalAdapter(lstAnimal);
        recyclerView.setAdapter(adapter);
    }

    private void initAnimals() {
        for (int i = 0; i < animals.length; i++) {
            Animal animal = new Animal(animals[i], images[i]);
            lstAnimal.add(animal);
        }
    }
}

MainActivity中只加入了一行代码,调用LinearLayoutManager的setOrientation()方法来设置布局的排列方向,默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL表示让布局横向排列,这样RecyclerView就可以横向滚动了。

重新运行一下程序,效果如图所示。

你可以用手指在水平方向上滑动来查看屏幕外的数据。

为什么ListView很难或者根本无法实现的效果在RecyclerView上这么轻松就能实现了呢?这主要得益于RecyclerView出色的设计。

ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManager,LayoutManager中制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。

3、实现瀑布流布局

除了LinearLayoutManager之外,RecyclerView还给我们提供了GridLayoutManager和StaggeredGridLayoutManager这两种内置的布局排列方式。

GridLayoutManager可以用于实现网格布局,StaggeredGridLayoutManager可以用于实现瀑布流布局(交错网格布局)

这里我们来实现一下效果更加炫酷的瀑布流布局,网格布局就作为课后习题,交给你们自己来研究吧。

首先还是来修改一下item.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="wrap_content"
    android:layout_margin="5dp"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/ivAnimal"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_gravity="center_horizontal" />

    <TextView
        android:id="@+id/tvName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp" />
</LinearLayout>

这里做了几处小的调整,首先将LinearLayout的宽度由100dp改成了match_parent,因为瀑布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。

另外我们使用了layout_margin属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一些。

还有就是将TextView的对齐属性改成了左对齐,因为待会我们会将文字的长度变长,如果还是居中显示就会感觉怪怪的。

接着修改MainActivity中的代码,如下所示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;

import android.os.Bundle;

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

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private List<Animal> lstAnimal = new ArrayList<>();
    private int[] images = {R.drawable.rat, R.drawable.ox, R.drawable.tiger, R.drawable.rabbit, R.drawable.dragon, R.drawable.snake, R.drawable.horse, R.drawable.goat, R.drawable.monkey, R.drawable.rooster, R.drawable.dog, R.drawable.pig};
    private String[] animals = {"子鼠", "丑牛", "寅虎", "卯兔", "辰龙", "巳蛇", "午马", "未羊", "申猴", "酉鸡", "戌狗", "亥猪"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initAnimals();
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        // 使用瀑布流网格布局管理器来布局,3列,垂直方向
        StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);
        AnimalAdapter adapter = new AnimalAdapter(lstAnimal);
        recyclerView.setAdapter(adapter);
    }

    private void initAnimals() {
        for (int i = 0; i < animals.length; i++) {
            Animal animal = new Animal(getRandomLengthName(animals[i]), images[i]); // 调用随机长度方法
            lstAnimal.add(animal);
        }
    }
    // 将名字变成随机长度,复制最少10次
    private String getRandomLengthName(String name) {
        Random random = new Random();
        int length = random.nextInt(20) + 10;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(name);
        }
        return builder.toString();
    }

}

首先,在onCreate()方法中,我们创建了一个StaggeredGridLayoutManager的实例。StaggeredGridLayoutManager的构造函数接收两个参数:

第一个参数用于指定布局的列数,传入3表示会把布局分为3列;

第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列,最后再把创建好的实例设置到RecyclerView当中就可以了,就是这么简单!

没错,仅仅修改了一行代码,我们就已经成功实现瀑布流布局的效果了。

不过由于瀑布流布局需要各个子项的高度不一致才能看出明显的效果,为此我又使用了一个小技巧。

这里我们把眼光聚焦在getRandomLengthName()这个方法上,这个方法使用了Random对象来创造一个10 到30之间的随机数,然后将参数中传入的字符串重复随机遍。

在initAnimals()方法中,每个生肖的名字都改成调用getRandomLengthName()这个方法来生成,这样就能保证各生肖名字的长短差距都比较大,子项的高度也就各不相同了。

现在重新运行一下程序,效果如图所示。

 

当然,由于生肖名字的长度每次都是随机生成的,所以大家运行的效果肯定和图中是不一样的。

网格布局

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
// 使用网格布局管理器来布局,3列,垂直方向,倒序
GridLayoutManager layoutManager = new GridLayoutManager(this,3,GridLayoutManager.VERTICAL,true);
recyclerView.setLayoutManager(layoutManager);
AnimalAdapter adapter = new AnimalAdapter(lstAnimal);
recyclerView.setAdapter(adapter);

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
GridLayoutManager layoutManager = new GridLayoutManager(this, 3);
layoutManager.setOrientation(GridLayoutManager.VERTICAL); // 垂直方向
recyclerView.setLayoutManager(layoutManager);
AnimalAdapter adapter = new AnimalAdapter(lstAnimal);
recyclerView.setAdapter(adapter);

4、RecyclerView的点击事件

和ListView一样,RecyclerView也必须要能响应点击事件才可以,不然的话就没什么实际用途了。

不过不同于ListView的是, RecyclerView并没有提供类似于setOnItemClickListener()这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件,相比于ListView来说,实现起来要复杂一些。

那么你可能就有疑问了,为什么RecyclerView在各方面的设计都要优于ListView,偏偏在点击事件上却没有处理得非常好呢?

其实不是这样的,ListView在点击事件上的处理并不人性化,setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具体的某一个按钮呢?

虽然ListView也是能做到的,但是实现起来就相对比较麻烦了。

为此,RecyclerView干脆直接摒弃了子项点击事件的监听器,所有的点击事件都由具体的View去注册,就再没有这个困扰了。

下面我们来具体学习一下如何在RecyclerView中注册点击事件,修改AnimalAdapter中的代码,如下所示:

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class AnimalAdapter extends RecyclerView.Adapter<AnimalAdapter.ViewHolder> {
    private List<Animal> lstAnimal;

    // 内部类ViewHolder
    static class ViewHolder extends RecyclerView.ViewHolder {
        View animalView;
        ImageView ivAnimal;
        TextView tvName;

        public ViewHolder(View view) {
            super(view);
            animalView = view;
            ivAnimal = (ImageView) view.findViewById(R.id.ivAnimal);
            tvName = (TextView) view.findViewById(R.id.tvName);
        }
    }

    // 构造方法
    public AnimalAdapter(List<Animal> lstAnimal) {
        this.lstAnimal = lstAnimal;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        // 给子项目注册点击监听器
        viewHolder.animalView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = viewHolder.getAdapterPosition();
                Animal animal = lstAnimal.get(position);
                Toast.makeText(v.getContext(), "点击的是子项目:" + animal.getName(), Toast.LENGTH_SHORT).show();
            }
        });
        // 给图片控件注册点击监听器
        viewHolder.ivAnimal.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = viewHolder.getAdapterPosition();
                Animal animal = lstAnimal.get(position);
                Toast.makeText(v.getContext(), "点击的是图片:" + animal.getName(), Toast.LENGTH_SHORT).show();
            }
        });
        // 给文本控件注册点击监听器
        viewHolder.tvName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = viewHolder.getAdapterPosition();
                Animal animal = lstAnimal.get(position);
                Toast.makeText(v.getContext(), "点击的是文本:" + animal.getName(), Toast.LENGTH_SHORT).show();
            }
        });
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Animal animal = lstAnimal.get(position);
        holder.ivAnimal.setImageResource(animal.getImageId());
        holder.tvName.setText(animal.getName());
    }

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

我们先是修改了ViewHolder,在ViewHolder中添加了animalView变量来保存子项最外层布局的实例,然后在onCreateViewHolder()方法中注册点击事件就可以了。

这里分别为最外层子项目布局和ImageView、TextView都注册了点击事件, RecyclerView的强大之处也在这里,它可以轻松实现子项中任意控件或布局的点击事件。

我们在三个点击事件中先获取了用户点击的position, 然后通过position拿到相应的Animal实例,再使用Toast分别弹出三种不同的内容以示区别。

现在重新运行代码,并点击子项目的图片部分,这时触发了ImageView的点击事件;然后再点击子项目的文字部分,这时触发了TextView的点击事件;再点击图片的空白部分,这个事件会被子项的最外层布局捕获到,效果如图所示。

             

5、实战编写精美的聊天界面

我们要综合运用前面的知识来编写出一个较为复杂的精美聊天界面。

先创建UIBestTest项目。

(1)制作Nine-Patch图片(9点图片)

我们还需要先学习一下如何制作Nine-Patch图片(9点图片)。

我们之前还没有听说过这个名词,它是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域不可以。

那么Nine-Patch图片到底有什么实际作用呢?

我们还是通过一个例子来看一下。 

比如说,项目中有一张气泡样式的图片message_left.png,如图所示。

  我们将这张图片设置为LinearLayout 的背景图片,修改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="wrap_content"
    android:background="@drawable/message_left">
</LinearLayout>

将LinearLayout的宽度指定为match_parent, 然后将它的背景图设置为message_left,现在运行程序,效果如图所示。

可以看到,由于message_left的宽度不足以填满整个屏幕的宽度,整张图片被均匀地拉伸了。

这种效果非常差,用户肯定是不能容忍的,这时我们就可以使用Nine-Patch图片来进行改善。

在Android Studio中集成了制作Nine-Patch图片的功能。

在要制作的图片上,右键选择“Create 9-Patch file”,然后直接回车确认就可以在同路径下生成一张后缀.9.png的九点图,然后双击打开进行制作就可以了,原图要删除掉。文件的后缀名必须是.9.png。

    

绘制就比较简单了,在图片的四个边框,直接用鼠标选中边上的像素点拖动涂黑即可,按住Shift键拖动可以进行擦除。

其中在上边框左边框绘制的部分表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框右边框绘制的部分表示内容会被放置的区域。

绘制完成后效果如图所示。

这样当图片需要拉伸的时候,就可以只拉伸指定的区域。

我们再来加上文本,代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:background="@drawable/message_left">

    <TextView

        android:text="这是一条消息。这是一条消息。这是一条消息。这是一条消息。"

        android:layout_gravity="center_vertical"

        android:textSize="25sp"

        android:maxLines="2"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

</LinearLayout>

运行程序,如图所示。

(2)编写精美的聊天界面

既然是要编写一个聊天界面,那就肯定要有收到的消息和发出的消息。

我们刚制作了message_left.9.png可以作为收到消息的背景图,那么还需要再制作一张message_right.9.png作为发出消息的背景图。

   

图片都提供好了之后就可以开始编码了。

接下来开始编写主界面,修改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="#d8e0e8"
    android:orientation="vertical">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvMessage"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

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

        <EditText
            android:id="@+id/edtInput"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="请输入内容"
            android:maxLines="2" />

        <Button
            android:id="@+id/btnSend"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送" />
    </LinearLayout>
</LinearLayout>

在主界面放置了一个RecyclerView用于显示聊天内容,放置一个EditText用于输入消息,放置一个Button用于发送消息。

然后定义消息的实体类,新建Msg类,代码如下所示:

public class Msg {
    public static final int TYPE_RECEIVED = 0;
    public static final int TYPE_SENT = 1;
    private String content;
    private int type;

    public Msg(String content, int type) {
        this.content = content;
        this.type = type;
    }

    public String getContent() {
        return content;
    }

    public int getType() {
        return type;
    }

}

Msg类中只有两个字段,content表示消息的内容,type表示消息的类型。

其中消息类型有两个值可选,TYPE_RECEIVED表示一条收到的消息,TYPE_SENT表示一条发出的消息。

接下来编写RecyclerView子项的布局,新建msg_item.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="wrap_content"
    android:orientation="vertical"
    android:padding="10dp">

    <LinearLayout
        android:id="@+id/layoutLeft"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:background="@drawable/message_left">

        <TextView
            android:id="@+id/tvLeftMsg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:textColor="#fff" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/layoutRight"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:background="@drawable/message_right">

        <TextView
            android:id="@+id/tvRightMsg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp" />
    </LinearLayout>
</LinearLayout>

这里让收到的消息居左对齐,发出的消息居右对齐,并且分别使用message_left.9.png和message_right.9.png作为背景图。

运用可见属性,稍后在代码中根据消息的类型来决定隐藏和显示哪种信息,就可以让收到的消息和发出的消息都放在同一个布局里。

接下来需要创建RecyclerView的适配器类,新建类MsgAdapter,代码如下所示:

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {
    private List<Msg> lstMsgs;

    class ViewHolder extends RecyclerView.ViewHolder {
        LinearLayout leftLayout;
        LinearLayout rightLayout;
        TextView tvLeftMsg;
        TextView tvRightMsg;

        public ViewHolder(View view) {
            super(view);
            leftLayout = (LinearLayout) view.findViewById(R.id.layoutLeft);
            rightLayout = (LinearLayout) view.findViewById(R.id.layoutRight);
            tvLeftMsg = (TextView) view.findViewById(R.id.tvLeftMsg);
            tvRightMsg = (TextView) view.findViewById(R.id.tvRightMsg);
        }
    }

    // 适配器构造方法
    public MsgAdapter(List<Msg> msgList) {
        lstMsgs = msgList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Msg msg = lstMsgs.get(position);
        if (msg.getType() == Msg.TYPE_RECEIVED) {
            //如果是收到的信息,则显示左边的消息布局,将右边的消息布局隐藏
            holder.leftLayout.setVisibility(View.VISIBLE);
            holder.rightLayout.setVisibility(View.GONE);
            holder.tvLeftMsg.setText(msg.getContent());
        } else if (msg.getType() == Msg.TYPE_SENT) {
            //如果是发出的信息,则显示右边的消息布局,将左边的消息布局隐藏
            holder.rightLayout.setVisibility(View.VISIBLE);
            holder.leftLayout.setVisibility(View.GONE);
            holder.tvRightMsg.setText(msg.getContent());
        }
    }

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

在onBindViewHolder()方法中增加了对消息类型的判断。如果消息是收到的,则显示左边的消息布局,如果消息是发出的,则显示右边的消息布局。

最后修改MainActivity中的代码,来为RecyclerView初始化一些数据,并给发送按钮加入事件响应,代码如下所示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

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

public class MainActivity extends AppCompatActivity {
    private List<Msg> msgList = new ArrayList<>();
    private EditText edtInput;
    private Button btnSend;
    private RecyclerView rvMessage;
    private MsgAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initMsgs();  // 初始化消息数据

        edtInput = (EditText) findViewById(R.id.edtInput);
        btnSend = (Button) findViewById(R.id.btnSend);
        rvMessage = (RecyclerView) findViewById(R.id.rvMessage);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        rvMessage.setLayoutManager(layoutManager);
        adapter = new MsgAdapter(msgList);
        rvMessage.setAdapter(adapter);
        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String content = edtInput.getText().toString();
                if (!"".equals(content)) {
                    Msg msg = new Msg(content, Msg.TYPE_SENT);
                    msgList.add(msg);
                    adapter.notifyItemInserted(msgList.size() - 1);  // 当有新消息时,刷新RecyclerView中的显示
                    rvMessage.scrollToPosition(msgList.size() - 1); // 将RecyclerView定位到最后一行
                    edtInput.setText("");  // 清空输入框中的内容
                } else {
                    Toast.makeText(MainActivity.this, "消息不能为空", Toast.LENGTH_LONG).show();
                }
            }
        });
    }

    // 自定义一个用于初始化消息的方法
    private void initMsgs() {
        Msg msg1 = new Msg("你好", Msg.TYPE_RECEIVED);
        msgList.add(msg1);
        Msg msg2 = new Msg("你好,你是谁?", Msg.TYPE_SENT);
        msgList.add(msg2);
        Msg msg3 = new Msg("我是张三,很高兴认识你", Msg.TYPE_RECEIVED);
        msgList.add(msg3);
    }
}

在initMsgs()方法中初始化了几条数据用于在RecyclerView中显示。

在发送按钮的点击事件里获取EditText中的内容,如果内容不为空字符串则创建出一个新的Msg对象,并把它添加到msgList列表中去。

调用RecyclerView.Adapter适配器的noyifyItemInserted()方法,用于通知列表有新的数据插入,这样新增的一条消息才能够在RecyclerView中显示。

调用RecyclerView的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看到最后一条消息。

最后调用EditText的setText()方法将输入的内容清空。

RecyclerView.Adapter的方法:

  • noifyItemInserted(int position):列表position位置添加一条数据时调用,伴有动画效果
  • notifyItemRemoved(int position):列表position位置移除一条数据时调用,伴有动画效果
  • notifyItemMoved(int fromPosition, int toPosition) :列表fromPosition位置的数据移到toPosition位置时调用,伴有动画效果
  • notifyItemRangeChanged(int positionStart, int itemCount) :列表从positionStart之后的itemCount数量的列表项进行数据刷新,伴有动画效果
  • notifyItemRangeInserted(int positionStart, int itemCount) :列表从positionStart位置之后的itemCount数量的列表项批量添加数据时调用,伴有动画效果
  • notifyItemRangeRemoved(int positionStart, int itemCount) :列表从positionStart位置到itemCount数量的列表项批量删除数据时调用,伴有动画效果

运行程序,效果如图所示:

             

 

posted @ 2022-10-02 12:18  熊猫Panda先生  阅读(491)  评论(0编辑  收藏  举报