Android5.0發布后為我們帶來了新的控件RecyclerView。RecyclerView被稱為ListView和GridView的繼任者,它使用一種統一的方式整合了這兩種視圖的使用并提供更多選擇。通過使用RecyclerView結合不同的LayoutManager我們可以一次性實現線性,網格和瀑布流三種布局,如果需要,線性列表不再一定是垂直的,還可以橫向滾動。此外,RecyclerView還封裝了更好的ViewHolder和Adapter類,將原來使用ListView時必須由開發者來實現的復用優化工作封裝好。開發者寫更少的代碼就能避免性能不佳的問題。這篇短文通過對比ListView和RecyclerView的不同使用方式,來說明為什么我們應該優先使用RecyclerView。
Android App中開發列表界面可以做的很簡單,每個列表項僅顯示一行文字;也可以做的很復雜,每個列表項包含標題,副標題,按鈕,復選框,還能響應點擊事件等等。這些視圖組件在加載時需要父視圖通過findViewById()一個一個創建。這個函數通過遍歷整個布局資源來查找目標視圖組件,如果用戶每滑動一次列表就遍歷一遍甚至多遍,就會影響App的性能,因此兩種列表布局都使用ViewHolder來解決這個問題。此外,因為可以針對不同的數據模型定制列表項,所以ListView或者RecyclerView需要有輔助類來負責將數據模型適配到視圖組件中,這就需要借助“適配器模式”來實現。ListView使用的是ArrayAdapter,而RecyclerView使用的則是Adapter。下面讓我們通過對比,來看看RecyclerView的實現方式為什么比ListView的實現方式要好。
一. 使用ListView創建列表界面
1. ListView,ArrayAdapter和ViewHolder
使用ListView來創建列表項主要依賴ArrayAdapter和ViewHolder這兩個輔助類。我們創建一個ArrayAdapter的子類,然后在子類中定義一個ViewHolder的輔助類,這個輔助類負責托管視圖組件。然后最重要的邏輯通過重載ArrayAdapter的getView()方法來實現。下面通過一個具體的代碼示例來看看。
2. 代碼示例
假設我們要實現一個功能:通過網絡請求獲取數據并更新列表。那么我們首先需要在Activity或者Fragment中創建好ArrayAdapter子類,傳入列表布局資源和數據模型;初始化ListView,并在ListView上調用setAdapter()設置適配器,別忘了,一定要記得在數據模型發生更新時調用ArrayAdapter的notifyDataSetChanged()函數通知列表更新。
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的子類中,其中要特別注意的就是判斷視圖是不是第一次創建。如果是第一次創建,那么就需要通過ViewHolder初始化視圖組件,并setTag()緩存到View中去。如果第一次(比如用戶滾動列表刷新界面),就通過getTag()獲取已經創建好的視圖組件,然后用對應數據模型進行更新,這幾乎就是重載ArrayAdapter的getView()方法的標準套路。
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創建列表界面
在使用了RecyclerView之后這些工作都不用開發者自己寫了,因為RecyclerView都會給你做好。使用RecyclerView最好的一點是,它減少了開發者不必要的工作量,使得代碼看上去更規范,工整。它將這些優化工作封裝到了庫中,因此開發者只需要在Adapter中重載幾個函數,就能做到高性能實現。這也從更大程度上防止初學者寫出不太好的代碼。
1. RecyclerView,Adapter和ViewHolder
RecyclerView結合Adapter和ViewHolder,圍繞“適配器模式”提供了一套滿足“單一職責原則”的解決方案。
“一個類應該只有一個發生變化的原因”
其中RecyclerView作為ViewGroup的子類,負責展示列表項,每個列表項都是View的子對象。RecyclerView并不是有多少項就創建多少項,這樣很容易搞垮應用。當用戶滑動屏幕時,滑出視圖的列表項會被回收用于顯示新的列表項,這就是“RecyclerView”這個名字的由來。ViewHolder不變,仍然負責托管和容納視圖組件,這個沒什么好說的。而Adapter的使用就規范多了,只需要重載三個函數即可:
- onCreateViewHolder(ViewGroup parent, int viewType)
- onBindViewHolder(VH holder, int position)
- getItemCount()
其中onCreateViewHolder()用于創建ViewHolder對象,onBindViewHolder()用于將數據模型綁定到ViewHolder中的視圖組件上,getItemCount()返回數據模型的數量。當RecyclerView需要顯示視圖時,就會通過它的Adapter來實現,大致流程如下:
- RecyclerView調用Adapter的getItemCount()方法詢問數組列表中有多少數據;
- RecyclerView調用Adapter的createViewHolder()創建ViewHolder及其要顯示的視圖;
- RecyclerView調用Adapter的onBindViewHolder()方法,傳入ViewHolder對象及位置,Adapter找到目標位置對應的數據,用它綁定到ViewHolder對象容納的視圖上。
其中createViewHolder()調用并不頻繁。一旦創建了足夠的ViewHolder,RecyclerView就不會再調用createViewHolder(),而是回收利用舊的ViewHolder來節約內存開銷。
2. 代碼示例
比如我們要實現如下所示的列表界面:
只需要在Fragment或Activity中編寫初始化代碼,并定義好ViewHolder和Adapter即可,注意RecyclerView多出來一步設置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更容易理解,所以在項目開發中強烈建議使用這個新的組件。