編輯:關於Android編程
ListView作為android中最常使用的控件,可以以條目的形式顯示大量的數據,經常被用於顯示最近聯系人列表,對於每一個 Item,均要求adapter的getView方法返回一個View,因此ListView的實現是離不開Adapter的,如果以MVC的思想來看ListView的話,ListView的顯示相當於V,Adapter部分相當於C,而數據部分就相當於M了,接下來的幾篇博客計劃對ListView自己所了解的一些優化措施總結一下,希望能夠幫助到大家;
先來看看如果我們不使用任何優化措施的話,使用ListView的方法:
這裡先補充下獲得LayoutInflater的三種方法:
(1)如果是在Activity中的話,可以調用
LayoutInflater inflater = getLayoutInflater();
(2)如果不是在Activity中,則可以將context上下文作為參數,通過
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
(3)如果不是在Activity中,則可以將context上下文作為參數,通過
LayoutInflater inflater = LayoutInflater.from(context);
首先定義Activity界面布局listview.xml,很簡單,裡面就只有一個ListView
<linearlayout 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"> <listview android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"> </listview></linearlayout>再定義ListView的每個item的布局item.xml,也很簡單,只有一個TextView
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <textview android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="50dp"> </textview></linearlayout>
接下來定義一個Adapter,繼承自BaseAdapter,用來為ListView填充數據
public class ListViewAdapter extends BaseAdapter{ Listlist = new ArrayList (); LayoutInflater inflater = null; public ListViewAdapter(List list,Context context) { this.list = list; inflater = LayoutInflater.from(context); } @Override public int getCount() { return list.size(); } @Override public Object getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = null; System.out.println("before: "+list.get(position)+"------- "+convertView); view = inflater.inflate(R.layout.item, null); System.out.println("after: "+list.get(position)+"------- "+view); TextView textView = (TextView) view.findViewById(R.id.textView); textView.setText(list.get(position)); return view; } }
可以發現,我們在getView裡並沒有做任何的優化,待會我們看看這樣的方式會出什麼問題;
定義Activity,將Adapter綁定到ListView上面
public class MainActivity extends Activity { List很簡單,我們模擬了有50個條目的ListView,並且通過ListViewAdapter的構造函數將其傳遞給Adapter用於在界面展示這些數據;list = new ArrayList (); ListView listView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.listview); listView = (ListView) findViewById(R.id.listView); for(int i = 1;i < 50;i++) { list.add("item "+i); } ListViewAdapter adapter = new ListViewAdapter(list, this); listView.setAdapter(adapter); } }
運行效果圖:
對應的Logcat輸出:
可以發現初始狀態有10個item顯示在界面上,並且他們的before view均是null的,調用inflate方法之後生成了新的view,因此after view非空了;
接著我們向上拖動屏幕,可以在Logcat看到有如下輸出:
可以發現此時的item 19和item 20在before之前convertView得地址都是@4181a590,這個view是已經劃出屏幕的item 9的,這點可以從剛開始第一屏幕的輸出看出來,因為item 9已經被加入到了RecycleBin緩存中了,所以在調用getView方法的時候convertView獲取到的是緩存中的隨機一個view,item 19和item 20完全有可能獲取到同一個view,但是他們的after view是不可能出現相同的,也就是說我們這個例子中的after view的地址值是不可能相同的,因為我們模擬了50個item,所以會有50個view被放到RecycleBin緩存中,也許現在對於我們顯示來說沒什麼,那麼如果有大量的信息需要顯示的話,直接就會報內存的;
通過上面我們會發現一點,在調用getView方法的時候,他的第二個參數可能不會是null的,原因就是RecycleBin緩存幫我們暫存了那些劃出屏幕的view,所以我們在convertView非空的情況下我們也沒什麼必要重新調用inflate方法加載布局了,因為這個方法畢竟也是要解析xml文件的,至少是要花時間的,直接使用從緩存中取出的view即可啦,先來看看ListView提供的RecycleBin緩存圖解:
從上面的圖上可以看到當item 1被劃出屏幕之後,會被放到Recycle緩存中,當item 8要劃入屏幕的時候,如果他和item 1的類型相同的話,則直接從Recycle中獲得即可,即此時的convertView不再是null;如果他和item 1類型不一致的話,則會新建view視圖,即此時convertView等於null;
好的,那我們接下來就該充分使用android提供給我們的RecycleBin機制來優化ListView了;
只需要修改ListViewAdapter類的getView方法即可了:
@Override public View getView(int position, View convertView, ViewGroup parent) { View view = null; System.out.println("before: "+list.get(position)+"------- "+convertView); if(convertView == null) { view = inflater.inflate(R.layout.item, null); }else view = convertView; TextView textView = (TextView) view.findViewById(R.id.textView); textView.setText(list.get(position)); return view; }不滑動屏幕的時候,程序輸出:
接著我們滑動屏幕,查看輸出:
注意紅色部分,發現沒經過11個item,都會復用之前的view,這也就是說我們的RecycleBin緩存中將只有11個view了,不像前面那樣有50個view,這在很大程度上節約了內存,想想如果有上千萬條數據需要顯示,每個數據條目都有一個view在RecycleBin中是一件多麼可怕的事情,進行了convertView是否為null的判斷之後,將只會緩存一屏幕的view,當然有可能會多那麼幾個吧;
上面我們通過判斷convertView是否為空對ListView進行了優化,接下來我們看看getView方法,裡面在獲取TextView的時候,我們使用了findViewById方法,這個方法是與IO有關的操作,想必也會影響性能吧,他只要的目的是獲得某一個view的布局罷了,我們如果有了view的話,其實只需要第一次將該view和其布局綁定到一起就可以了,沒必要每次都為view設置布局了,這也就是使用setTag的目的了;
這裡也僅僅只是對ListViewAdapter的geyView方法進行修改即可:
@Override public View getView(int position, View convertView, ViewGroup parent) { View view = null; ViewHolder viewHolder = null; System.out.println("before: "+list.get(position)+"------- "+convertView); if(convertView == null) { view = inflater.inflate(R.layout.item, null); viewHolder = new ViewHolder(); viewHolder.textView = (TextView) view.findViewById(R.id.textView); view.setTag(viewHolder); }else { view = convertView; viewHolder = (ViewHolder) view.getTag(); } viewHolder.textView.setText("item "+list.get(position)); return view; } static class ViewHolder { TextView textView; }這裡采用靜態內部類的方式用於定義item 的各個控件 ,如果convertView非空的話,表示該view對應的布局已經存在了,只需要調用getTag獲取到即可了,如果convertView為空的話,則需要通過findViewById來獲取這個布局中的控件,並且最後將該布局通過setTag設置到view上面即可;
程序的輸出結果和上面是一樣的,這裡不再列出;
我們平常的實際應用中,每個條目有可能不都是一樣的,這種情況下會出現不同的項目布局,那麼這時候該怎麼辦呢?同樣我們通過實例來學習一下這時候的RecycleBin機制是怎麼實現緩存的;
在此,我們增加一個只顯示一個按鈕的條目,布局文件button.xml
修改ListViewAdapter類如下:
public class ListViewAdapter extends BaseAdapter{ List通過getViewTypeCount()返回的是你到底有多少種布局,我們這裡有兩種list = new ArrayList (); LayoutInflater inflater = null; public ListViewAdapter(List list,Context context) { this.list = list; inflater = LayoutInflater.from(context); } @Override public int getItemViewType(int position) { //0表示顯示的是TextView,1表示顯示的是Button if(position % 4 != 0) return 0; else return 1; } @Override public int getViewTypeCount() { //表示有兩種類型的item布局 return 2; } @Override public int getCount() { return list.size(); } @Override public Object getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = null; TextViewHolder textViewHolder = null; ButtonViewHolder buttonViewHolder = null; System.out.println("before: "+list.get(position)+"------- "+convertView); int type = getItemViewType(position); if(convertView == null) { switch (type) { case 0: view = inflater.inflate(R.layout.item, null); textViewHolder = new TextViewHolder(); textViewHolder.textView = (TextView) view.findViewById(R.id.textView); view.setTag(textViewHolder); textViewHolder.textView.setText(list.get(position)); break; case 1: view = inflater.inflate(R.layout.button, null); buttonViewHolder = new ButtonViewHolder(); buttonViewHolder.button = (Button) view.findViewById(R.id.button); view.setTag(buttonViewHolder); buttonViewHolder.button.setText("button "+list.get(position)); break; default: break; } }else { view = convertView; switch (type) { case 0: textViewHolder = (TextViewHolder) view.getTag(); textViewHolder.textView.setText(list.get(position)); break; case 1: buttonViewHolder = (ButtonViewHolder) view.getTag(); buttonViewHolder.button.setText("button "+list.get(position)); break; default: break; } } return view; } static class TextViewHolder { TextView textView; } static class ButtonViewHolder { Button button; } }
通過getItemViewType(int)返回的是根據你的position得到的對應布局的ID,當然這個ID是可以由你來定的;
修改Activity類
public class MainActivity extends Activity { List界面運行效果圖為:list = new ArrayList (); ListView listView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.listview); listView = (ListView) findViewById(R.id.listView); for(int i = 1;i < 50;i++) { if(i % 4 == 0) { list.add("button "+i); }else list.add("item "+i); } ListViewAdapter adapter = new ListViewAdapter(list, this); listView.setAdapter(adapter); } }
可以發現每隔3個條目我們都會加載另一個不同的條目,接著查看Logcat輸出:
在我們滑動屏幕之後,輸出結果為:
注意圖中突出顯示部分,可以發現對於button item,我們也得到了復用,當然這個復用順序並不一定是按順序來的,因為RecycleBin機制只會把你已經滑出屏幕的item緩存下來,但是在從緩存中取得時候,並不一定就是按你存進去的順序取出來的,這點要注意啦,到此一般的ListView優化測試結束了,我們來總結一下:
(1)通過復用view的方式來充分利用android系統本身自帶的RecycleBin緩存機制,能夠保證即使有再多的item實際中也僅僅會有有限多個item,大大節省內存;
(2)使用靜態內部類以及setTag方式,將view與其對應的控件綁定起來,避免了每次得到view以後都需要通過findViewById的方式來獲取控件;
(3)對於有多種布局的ListView來說,我們可以通過getViewTypeCount()獲得布局的種類,通過getItemViewType(int)獲得當前位置上的布局到底是屬於哪一類;
好了,這篇先介紹到這裡
WeTest導讀安卓開發者都知道,RecyclerView比ListView要靈活的多,但不可否認的裡面的坑也同樣埋了不少人。下面讓我們看看騰訊開發工程師用實例講解自己踩
上一篇說到一鍵切換應用的主題顏色,那麼今天就繼續來講一講如何實現應用的一些圖標也一樣能夠跟隨應用的主題顏色切換而改變圖標的顏色。比如應用首頁的一些固定的展示圖標,或者是單
在工作中又很多需求都不是android系統自帶的控件可以達到效果的,內置的TabHost就是,只能達到簡單的效果 ,所以這個時候就要自定義控件來達到效果:這個效果就是: