1. 程式人生 > >RecyclerView詳解(基本使用+解決複用+新增HEAD和FOOT+上拉載入更多+下拉重新整理)以及ExpandableListView的簡單使用

RecyclerView詳解(基本使用+解決複用+新增HEAD和FOOT+上拉載入更多+下拉重新整理)以及ExpandableListView的簡單使用

一、RecyclerView的簡單使用

先看效果圖

程式碼實現

1.引入recyclerview包

implementation 'com.android.support:design:27.1.0'

2.佈局中新增RecyclerView

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_apps" android:layout_width="match_parent" android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>

3.MainActivity中找到控制元件,設定LayoutManager

public class MainActivity extends AppCompatActivity {

    private RecyclerView rvApps;
@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main
); rvApps = findViewById(R.id.rv_apps); rvApps.setLayoutManager(new LinearLayoutManager(this)); } }

LayoutManager的作用是表明RecyclerView的展示方式,LinearLayoutManager表示是線性垂直垂直展示。

4.RecyclerView只需要設定兩個引數,一是LayoutManager,二是介面卡,介面卡是用來繫結資料的。RecyclerView最重要的就是設定介面卡。

先看一下最終的MainActivity程式碼:

public class MainActivity extends AppCompatActivity {

    private RecyclerView rvApps;
    private ArrayList<App> apps = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rvApps = findViewById(R.id.rv_apps);
rvApps.setLayoutManager(new LinearLayoutManager(this));
//初始化資料
for (int i = 0;i<50;i++){
            App app = new App();
app.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
app.setName(getString(R.string.app_name)+i);
apps.add(app);
}

        rvApps.setAdapter(new MyAdapter(apps));
}
}

再看一下最終的Adapter程式碼:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.mViewHolder>{

    private ArrayList<App> apps;
//構造方法,傳入資料
public MyAdapter(ArrayList<App> apps) {
        this.apps = apps;
}

    //初始化控制元件
public class mViewHolder extends RecyclerView.ViewHolder {
        private ImageView ivIcon;
        private TextView tvName;
        public mViewHolder(View itemView) {
            super(itemView);
ivIcon = itemView.findViewById(R.id.iv_icon);
tvName = itemView.findViewById(R.id.tv_name);
}
    }

    //繫結佈局
@NonNull
    @Override
public mViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.app_item,parent,false);
mViewHolder holder = new mViewHolder(view);
        return holder;
}

    //對應位置繫結資料,這裡可以新增點選事件
@Override
public void onBindViewHolder(@NonNull mViewHolder holder, int position) {
        holder.tvName.setText(apps.get(position).getName());
holder.ivIcon.setImageDrawable(apps.get(position).getIcon());
holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
public void onClick(View v) {
                //這裡新增點選事件
}
        });
}

    //獲取列表長度
@Override
public int getItemCount() {
        return apps.size();
}
}

裡面用到了App類,貼出App類的程式碼:

public class App {
    private String name;//app名稱
private Drawable icon;//app圖示
public String getName() {
        return name;
}

    public void setName(String name) {
        this.name = name;
}

    public Drawable getIcon() {
        return icon;
}

    public void setIcon(Drawable icon) {
        this.icon = icon;
}
}

用到的app_item佈局:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
    <ImageView
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:src="@mipmap/ic_launcher"/>
    <TextView
android:id="@+id/tv_name"
app:layout_constraintLeft_toRightOf="@+id/iv_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"/>
</android.support.constraint.ConstraintLayout>

到這裡就完成了RecyclerView的功能。需要修改什麼直接 複製+修改對應的函式 即可。如果不滿足於 複製+修改,後面講解一下從無到有的建立介面卡。

介面卡建立過程

定義自己的介面卡繼承RecyclerView的Adapter,並實現Adapter中相應的方法。


視訊中彈出程式碼提示選單的快捷鍵是:Alt+Enter。解釋一下這裡面的函式作用:

mViewHolder 用來初始化控制元件

onCreateViewHolder 用來繫結佈局

onBindViewHolder 用來對應位置繫結資料

getItemCount 用來獲取列表長度

5.本例中,列表中每一項表示一個app,那麼我們先宣告展示的app物件

public class App {
    private String name;//app名稱
private Drawable icon;//app圖示
public String getName() {
        return name;
}

    public void setName(String name) {
        this.name = name;
}

    public Drawable getIcon() {
        return icon;
}

    public void setIcon(Drawable icon) {
        this.icon = icon;
}
}
6.在MainActivity中生成一些初始的列表資料
//初始化資料
for (int i = 0;i<50;i++){
    App app = new App();
app.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
app.setName(getString(R.string.app_name)+i);
apps.add(app);
}

7.通過建構函式將資料傳入Adapter

private ArrayList<App> apps;
//構造方法,傳入資料
public MyAdapter(ArrayList<App> apps) {
    this.apps = apps;
}

MainActivity中,給RecyclerView新增Adapter:

rvApps.setAdapter(new MyAdapter(apps));

 8.設定列表中每項資料的佈局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
    <ImageView
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:src="@mipmap/ic_launcher"/>
    <TextView
android:id="@+id/tv_name"
app:layout_constraintLeft_toRightOf="@+id/iv_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"/>
</android.support.constraint.ConstraintLayout>

9.實現Adapter對應的方法,Adapter最終的程式碼為:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.mViewHolder>{

    private ArrayList<App> apps;
//構造方法,傳入資料
public MyAdapter(ArrayList<App> apps) {
        this.apps = apps;
}

    //初始化控制元件
public class mViewHolder extends RecyclerView.ViewHolder {
        private ImageView ivIcon;
        private TextView tvName;
        public mViewHolder(View itemView) {
            super(itemView);
ivIcon = itemView.findViewById(R.id.iv_icon);
tvName = itemView.findViewById(R.id.tv_name);
}
    }

    //繫結佈局
@NonNull
    @Override
public mViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.app_item,parent,false);
mViewHolder holder = new mViewHolder(view);
        return holder;
}

    //對應位置繫結資料,這裡可以新增點選事件
@Override
public void onBindViewHolder(@NonNull mViewHolder holder, int position) {
        holder.tvName.setText(apps.get(position).getName());
holder.ivIcon.setImageDrawable(apps.get(position).getIcon());
holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
public void onClick(View v) {
                //這裡新增點選事件
}
        });
}

    //獲取列表長度
@Override
public int getItemCount() {
        return apps.size();
}
}

MainActivity中最終的程式碼為:

public class MainActivity extends AppCompatActivity {

    private RecyclerView rvApps;
    private ArrayList<App> apps = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rvApps = findViewById(R.id.rv_apps);
rvApps.setLayoutManager(new LinearLayoutManager(this));
//初始化資料
for (int i = 0;i<50;i++){
            App app = new App();
app.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
app.setName(getString(R.string.app_name)+i);
apps.add(app);
}

        rvApps.setAdapter(new MyAdapter(apps));
}
}

以上,便是RecyclerView的基本使用。

原始碼已上傳:

二、優雅的解決RecyclerView的Item複用問題

如果RecyclerView的Item中有Checkbox,或者其他的具有記錄狀態功能的控制元件。那麼很容易引發Item的複用問題


如上圖,本來一個很正常的列表,每個item都沒被選中,當我選中第一個Checkbox的時候,後面item的Checkbox也變成了選中狀態。這就是item的複用導致的錯誤。

這個問題是由於RecyclerView會自動回收移出螢幕的item,然後把回收回來的item讓新進入螢幕的item使用。所以導致了新進入的item保留了之前的選中狀態。

現在的程式碼如下:

佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
    <android.support.v7.widget.RecyclerView
android:id="@+id/rv_test"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</android.support.constraint.ConstraintLayout>

MainActivity:

public class MainActivity extends AppCompatActivity{
    private RecyclerView rvTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rvTest = (RecyclerView) findViewById(R.id.rv_test);
List<String> datas = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            datas.add(String.valueOf(i));
}
        rvTest.setLayoutManager(new LinearLayoutManager(this));
rvTest.setAdapter(new MyAdapter(datas));
}
}

MyAdapter:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.mViewHolder>{

    private List<String> DataList;
//構造方法,傳入資料
public MyAdapter(List<String> DataList) {
        this.DataList = DataList;
}

    //初始化控制元件
public class mViewHolder extends RecyclerView.ViewHolder {
        private TextView text;
        private CheckBox checkbox;
        public mViewHolder(View itemView) {
            super(itemView);
text = itemView.findViewById(R.id.text);
checkbox = itemView.findViewById(R.id.checkbox);
}
    }

    //繫結佈局
@NonNull
    @Override
public mViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item,parent,false);
mViewHolder holder = new mViewHolder(view);
        return holder;
}

    //對應位置繫結資料,這裡可以新增點選事件
@Override
public void onBindViewHolder(@NonNull final mViewHolder holder, int position) {
        holder.text.setText(DataList.get(position));
holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
public void onClick(View v) {
                holder.checkbox.setChecked(!holder.checkbox.isChecked());
}
        });
}

    //獲取列表長度
@Override
public int getItemCount() {
        return null == DataList ? 0:DataList.size();
}
}

item佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
    <CheckBox
android:id="@+id/checkbox"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
    <TextView
android:id="@+id/text"
tools:text="123"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintVertical_bias="0.5"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>

解決item複用問題的思路是:在滑動時雖然checkbox的狀態被複用了,但是資料卻沒有被複用,仍然是1,2,3,4......,所以我們可以通過一個checkStatus列表來儲存checkbox的狀態,每次選中checkbox時,把該位置加入checkStatus中,每次取消選中checkbox時,把該位置從checkStatus中移除。然後在每次設定checkbox狀態的時候根據列表來判斷是否選中。

將MyAdapter修改如下:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.mViewHolder>{

    private List<String> DataList;
//記錄checkbox狀態的列表
private List<Integer> checkStatus;
//構造方法,傳入資料
public MyAdapter(List<String> DataList) {
        this.DataList = DataList;
}

    //初始化控制元件
public class mViewHolder extends RecyclerView.ViewHolder {
        private TextView text;
        private CheckBox checkbox;
        public mViewHolder(View itemView) {
            super(itemView);
text = itemView.findViewById(R.id.text);
checkbox = itemView.findViewById(R.id.checkbox);
}
    }

    //繫結佈局
@NonNull
    @Override
public mViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item,parent,false);
mViewHolder holder = new mViewHolder(view);
        return holder;
}

    //對應位置繫結資料,這裡可以新增點選事件
@Override
public void onBindViewHolder(@NonNull final mViewHolder holder, int position) {
        if(checkStatus == null){
            checkStatus = new ArrayList<>();
}
        holder.text.setText(DataList.get(position));
holder.checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                Integer position = holder.getLayoutPosition();
                if(isChecked&&!checkStatus.contains(position)){
                    //如果checkbox選中了,而且checkStatus中不包含此位置,將該位置加入checkStatus中
checkStatus.add(position);
}else if(!isChecked&&checkStatus.contains(position)){
                    //如果checkbox未選中,而且checkStatus中包含此位置,將該位置從checkStatus移除
checkStatus.remove(position);
}
            }
        });
holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
public void onClick(View v) {
                holder.checkbox.setChecked(!holder.checkbox.isChecked());
}
        });
//根據checkStatus中是否包含此位置來設定checkbox狀態
holder.checkbox.setChecked(checkStatus.contains(position));
}

    //獲取列表長度
@Override
public int getItemCount() {
        return null == DataList ? 0:DataList.size();
}
}

效果圖如下:


三、給RecyclerView新增Head和Foot

先看效果圖


看一個普通的RecyclerView的Adapter:

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
    private List<String> DataList;
//構造方法,傳入資料
public MyAdapter(List<String> DataList) {
        this.DataList = DataList;
}

    //初始化ItemView
public class ItemViewHolder extends RecyclerView.ViewHolder {
        private TextView text;
        public ItemViewHolder(View itemView) {
            super(itemView);
text = itemView.findViewById(R.id.text);
}
    }

    //繫結佈局
@NonNull
    @Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;
RecyclerView.ViewHolder holder;
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item,parent,false);
holder = new ItemViewHolder(view);
        return holder;
}

    //對應位置繫結資料,這裡可以新增點選事件
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
itemViewHolder.text.setText(DataList.get(position-1));
}

    //獲取列表長度
@Override
public int getItemCount() {
        return null == DataList ? 0:DataList.size();
}
}

看一下添加了Head和Foot的RecyclerView的Adapter:

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
    private static final int ITEM = 0;
    private static final int HEAD = 1;
    private static final int FOOT = 2;
    private List<String> DataList;
//構造方法,傳入資料
public MyAdapter(List<String> DataList) {
        this.DataList = DataList;
}

    //初始化ItemView
public class ItemViewHolder extends RecyclerView.ViewHolder {
        private TextView text;
        public ItemViewHolder(View itemView) {
            super(itemView);
text = itemView.findViewById(R.id.text);
}
    }
    //初始化HeadView
public class HeadViewHolder extends RecyclerView.ViewHolder {
        private TextView text;
        public HeadViewHolder(View headView) {
            super(headView);
text = headView.findViewById(R.id.text);
}
    }
    //初始化FootView
public class FootViewHolder extends RecyclerView.ViewHolder {
        private TextView text;
        public FootViewHolder(View footView) {
            super(footView);
text = footView.findViewById(R.id.text);
}
    }

    //繫結佈局
@NonNull
    @Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;
RecyclerView.ViewHolder holder;
        switch (viewType){
            case HEAD:
                view = LayoutInflater.from(parent.getContext()).inflate(R.layout.head,parent,false);
holder = new HeadViewHolder(view);
                break;
            case FOOT:
                view = LayoutInflater.from(parent.getContext()).inflate(R.layout.foot,parent,false);
holder = new FootViewHolder(view);
                break;
            default:
                view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item,parent,false);
holder = new ItemViewHolder(view);
                break;
}
        return holder;
}

    //對應位置繫結資料,這裡可以新增點選事件
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if(holder instanceof HeadViewHolder){
            HeadViewHolder headViewHolder = (HeadViewHolder) holder;
headViewHolder.text.setText("HEAD");
}else if(holder instanceof FootViewHolder){
            FootViewHolder footViewHolder = (FootViewHolder) holder;
footViewHolder.text.setText("FOOT");
}else if(holder instanceof ItemViewHolder){
            ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
//比如有3條資料,0是head,1,2,3是item,4是foot。資料下標是0,1,2,所以item繫結position-1對應的資料
itemViewHolder.text.setText(DataList.get(position-1));
}
    }

    @Override
public int getItemViewType(int position) {
        if(position == 0){
            return HEAD;
} else if(position == getItemCount()-1){
            //比如有3條資料,0是head,1,2,3是item,4是foot。資料條數為3+2=5,所以用getItemCount-1
return FOOT;
}else{
            return ITEM;
}
    }

    //獲取列表長度
@Override
public int getItemCount() {
        return (null == DataList ? 0:DataList.size())+2;
}
}
                                

新增HEAD和FOOT的原理是:在onCreateViewHolder的引數中,第二個引數ViewType代表View的型別。我們重寫Adapter的getViewType()方法,根據位置返回對應的ViewType。如果位置是0,返回H