Android5.0發(fā)布后為我們帶來了新的控件RecyclerView。RecyclerView被稱為L(zhǎng)istView和GridView的繼任者,它使用一種統(tǒng)一的方式整合了這兩種視圖的使用并提供更多選擇。通過使用RecyclerView結(jié)合不同的LayoutManager我們可以一次性實(shí)現(xiàn)線性,網(wǎng)格和瀑布流三種布局,如果需要,線性列表不再一定是垂直的,還可以橫向滾動(dòng)。此外,RecyclerView還封裝了更好的ViewHolder和Adapter類,將原來使用ListView時(shí)必須由開發(fā)者來實(shí)現(xiàn)的復(fù)用優(yōu)化工作封裝好。開發(fā)者寫更少的代碼就能避免性能不佳的問題。這篇短文通過對(duì)比ListView和RecyclerView的不同使用方式,來說明為什么我們應(yīng)該優(yōu)先使用RecyclerView。
Android App中開發(fā)列表界面可以做的很簡(jiǎn)單,每個(gè)列表項(xiàng)僅顯示一行文字;也可以做的很復(fù)雜,每個(gè)列表項(xiàng)包含標(biāo)題,副標(biāo)題,按鈕,復(fù)選框,還能響應(yīng)點(diǎn)擊事件等等。這些視圖組件在加載時(shí)需要父視圖通過findViewById()一個(gè)一個(gè)創(chuàng)建。這個(gè)函數(shù)通過遍歷整個(gè)布局資源來查找目標(biāo)視圖組件,如果用戶每滑動(dòng)一次列表就遍歷一遍甚至多遍,就會(huì)影響App的性能,因此兩種列表布局都使用ViewHolder來解決這個(gè)問題。此外,因?yàn)榭梢葬槍?duì)不同的數(shù)據(jù)模型定制列表項(xiàng),所以ListView或者RecyclerView需要有輔助類來負(fù)責(zé)將數(shù)據(jù)模型適配到視圖組件中,這就需要借助“適配器模式”來實(shí)現(xiàn)。ListView使用的是ArrayAdapter,而RecyclerView使用的則是Adapter。下面讓我們通過對(duì)比,來看看RecyclerView的實(shí)現(xiàn)方式為什么比ListView的實(shí)現(xiàn)方式要好。
一. 使用ListView創(chuàng)建列表界面
1. ListView,ArrayAdapter和ViewHolder
使用ListView來創(chuàng)建列表項(xiàng)主要依賴ArrayAdapter和ViewHolder這兩個(gè)輔助類。我們創(chuàng)建一個(gè)ArrayAdapter的子類,然后在子類中定義一個(gè)ViewHolder的輔助類,這個(gè)輔助類負(fù)責(zé)托管視圖組件。然后最重要的邏輯通過重載ArrayAdapter的getView()方法來實(shí)現(xiàn)。下面通過一個(gè)具體的代碼示例來看看。
2. 代碼示例
假設(shè)我們要實(shí)現(xiàn)一個(gè)功能:通過網(wǎng)絡(luò)請(qǐng)求獲取數(shù)據(jù)并更新列表。那么我們首先需要在Activity或者Fragment中創(chuàng)建好ArrayAdapter子類,傳入列表布局資源和數(shù)據(jù)模型;初始化ListView,并在ListView上調(diào)用setAdapter()設(shè)置適配器,別忘了,一定要記得在數(shù)據(jù)模型發(fā)生更新時(shí)調(diào)用ArrayAdapter的notifyDataSetChanged()函數(shù)通知列表更新。
public class NewTaskFragment extends Fragment {
private static String TAG = NewTaskFragment.class.getSimpleName();
private List<CheckItem> mNewTaskList = new ArrayList<>();
private TaskFragmentAdapter mAdapter;
...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Logger.t(TAG).d(TAG, "onCreateView");
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_new_task, container, false);
...
mAdapter = new TaskFragmentAdapter(getContext(), R.layout.task_item, mNewTaskList, true);
ListView newTaskView = (ListView) view.findViewById(R.id.new_task_list);
newTaskView.setAdapter(mAdapter);
return view;
}
private void refreshNewTask() {
...
mNewTaskList.add(taskItem);
mAdapter.notifyDataSetChanged();
}
}
最重要的工作都放到ArrayAdapter的子類中,其中要特別注意的就是判斷視圖是不是第一次創(chuàng)建。如果是第一次創(chuàng)建,那么就需要通過ViewHolder初始化視圖組件,并setTag()緩存到View中去。如果第一次(比如用戶滾動(dòng)列表刷新界面),就通過getTag()獲取已經(jīng)創(chuàng)建好的視圖組件,然后用對(duì)應(yīng)數(shù)據(jù)模型進(jìn)行更新,這幾乎就是重載ArrayAdapter的getView()方法的標(biāo)準(zhǔn)套路。
public class TaskFragmentAdapter extends ArrayAdapter<CheckItem> {
private int mResID;
...
public TaskFragmentAdapter(Context context, int resource, List<CheckItem> objects, boolean newTask) {
super(context, resource, objects);
mResID = resource;
...
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final CheckItem item = getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(mResID, null);
viewHolder = new ViewHolder();
viewHolder.image = (ImageView) view.findViewById(R.id.type_image);
viewHolder.addrView = (TextView) view.findViewById(R.id.task_name);
viewHolder.macView = (TextView) view.findViewById(R.id.mac);
viewHolder.checkButton = (Button) view.findViewById(R.id.check);
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.image.setImageResource(item.getTypeIndicatorID());
viewHolder.addrView.setText(item.getAddress());
viewHolder.macView.setText(item.getMAC());
viewHolder.checkButton.setOnClickListener(new OnClickListener() {
...
});
return view;
}
class ViewHolder {
ImageView image;
TextView addrView;
TextView macView;
Button checkButton;
}
}
二. 使用RecyclerView創(chuàng)建列表界面
在使用了RecyclerView之后這些工作都不用開發(fā)者自己寫了,因?yàn)镽ecyclerView都會(huì)給你做好。使用RecyclerView最好的一點(diǎn)是,它減少了開發(fā)者不必要的工作量,使得代碼看上去更規(guī)范,工整。它將這些優(yōu)化工作封裝到了庫(kù)中,因此開發(fā)者只需要在Adapter中重載幾個(gè)函數(shù),就能做到高性能實(shí)現(xiàn)。這也從更大程度上防止初學(xué)者寫出不太好的代碼。
1. RecyclerView,Adapter和ViewHolder
RecyclerView結(jié)合Adapter和ViewHolder,圍繞“適配器模式”提供了一套滿足“單一職責(zé)原則”的解決方案。
“一個(gè)類應(yīng)該只有一個(gè)發(fā)生變化的原因”
其中RecyclerView作為ViewGroup的子類,負(fù)責(zé)展示列表項(xiàng),每個(gè)列表項(xiàng)都是View的子對(duì)象。RecyclerView并不是有多少項(xiàng)就創(chuàng)建多少項(xiàng),這樣很容易搞垮應(yīng)用。當(dāng)用戶滑動(dòng)屏幕時(shí),滑出視圖的列表項(xiàng)會(huì)被回收用于顯示新的列表項(xiàng),這就是“RecyclerView”這個(gè)名字的由來。ViewHolder不變,仍然負(fù)責(zé)托管和容納視圖組件,這個(gè)沒什么好說的。而Adapter的使用就規(guī)范多了,只需要重載三個(gè)函數(shù)即可:
- onCreateViewHolder(ViewGroup parent, int viewType)
- onBindViewHolder(VH holder, int position)
- getItemCount()
其中onCreateViewHolder()用于創(chuàng)建ViewHolder對(duì)象,onBindViewHolder()用于將數(shù)據(jù)模型綁定到ViewHolder中的視圖組件上,getItemCount()返回?cái)?shù)據(jù)模型的數(shù)量。當(dāng)RecyclerView需要顯示視圖時(shí),就會(huì)通過它的Adapter來實(shí)現(xiàn),大致流程如下:
- RecyclerView調(diào)用Adapter的getItemCount()方法詢問數(shù)組列表中有多少數(shù)據(jù);
- RecyclerView調(diào)用Adapter的createViewHolder()創(chuàng)建ViewHolder及其要顯示的視圖;
- RecyclerView調(diào)用Adapter的onBindViewHolder()方法,傳入ViewHolder對(duì)象及位置,Adapter找到目標(biāo)位置對(duì)應(yīng)的數(shù)據(jù),用它綁定到ViewHolder對(duì)象容納的視圖上。
其中createViewHolder()調(diào)用并不頻繁。一旦創(chuàng)建了足夠的ViewHolder,RecyclerView就不會(huì)再調(diào)用createViewHolder(),而是回收利用舊的ViewHolder來節(jié)約內(nèi)存開銷。
2. 代碼示例
比如我們要實(shí)現(xiàn)如下所示的列表界面:

只需要在Fragment或Activity中編寫初始化代碼,并定義好ViewHolder和Adapter即可,注意RecyclerView多出來一步設(shè)置LayoutManager。
public class CrimeListFragment extends Fragment {
RecyclerView mCrimeRecyclerView;
CrimeAdapter mCrimeAdapter;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_crime_list, container, false);
mCrimeRecyclerView = (RecyclerView) view.findViewById(R.id.crime_recycler_view);
mCrimeRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
updateUI();
return view;
}
private class CrimeHolder extends RecyclerView.ViewHolder ... {
private TextView mTitleTextView;
private TextView mDateTextView;
private CheckBox mSolvedCheckBox;
public CrimeHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(this);
mTitleTextView = (TextView) itemView.findViewById(R.id.list_item_crime_title_text_view);
mDateTextView = (TextView) itemView.findViewById(R.id.list_item_crime_date_text_view);
mSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_crime_solved_check_box);
}
public void bindCrime(Crime crime) {
mTitleTextView.setText(crime.getTitle());
mDateTextView.setText(crime.getDate().toString());
mSolvedCheckBox.setChecked(crime.isSolved());
}
...
}
private class CrimeAdapter extends RecyclerView.Adapter<CrimeHolder> {
private List<Crime> mCrimes;
public CrimeAdapter(List<Crime> crimes) {
mCrimes = crimes;
}
@Override
public CrimeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
View view = inflater.inflate(R.layout.list_item_crime, parent, false);
return new CrimeHolder(view);
}
@Override
public void onBindViewHolder(CrimeHolder holder, int position) {
Crime crime = mCrimes.get(position);
holder.bindCrime(crime);
}
@Override
public int getItemCount() {
return mCrimes.size();
}
}
private void updateUI() {
mCrimeAdapter = new CrimeAdapter(CrimeLab.get(getActivity()).getCrimes());
mCrimeRecyclerView.setAdapter(mCrimeAdapter);
}
}
使用RecyclerView寫出來的代碼要比ListView更容易理解,所以在項(xiàng)目開發(fā)中強(qiáng)烈建議使用這個(gè)新的組件。