13.1. 深入了解AnsyncTask
13.1.1. publishProgress()方法傳遞對象參數
13.1.1.1.概述
AnsyncTask.publishProgress方法不僅可以傳遞整形實參,也可以傳遞任意類型的對象
13.1.1.2.操作步驟
步驟1、自定義類,示例代碼:
//自定義類,存放下載下傳進度值和下載下傳提示資訊
private class Info {
private int progress;//下載下傳進度值
private String info;//下載下傳提示資訊
public int getProgress() {
return progress;
}
public String getInfo() {
return info;
}
//帶參數的構造方法
public Info(int progress, String info) {
super();
this.progress = progress;
this.info = info;
}
}
步驟2、在自定義AsyncTask類時,将第二個泛型參數設定為步驟1定義的類,示例代碼:
private class MyAsyncTask extends AsyncTask<URL, Info, String>{
步驟3、在doInBackground方法中,将需要在UI中更新的資料存放在自定義的對象中,然後調用publishProgress方法,在參數中将info類的一個對象發送給onProgressUpdate()方法。示例代碼:
publishProgress(new Info(i+1,"progress="+(i+1)+"%"));
步驟4、在onProressUpdate方法中用獲得的對象更新UI,示例代碼:
//重寫本方法,在UI中顯示下載下傳進度和提示資訊,參數Info類的對象
@Override
protected void onProgressUpdate(Info... info) {
super.onProgressUpdate(info);
mProgressBar.setProgress(info[0].getProgress());
mtvProgress.setText(""+info[0].getInfo());
}
13.1.2. publishProgress()方法中可變參數的用法
13.1.2.1.概述
publishProgress方法可以向onProgressUpdate發送數量任意多的實參。但這些資料必須是同一類型的。
13.1.2.2.操作步驟
步驟1、在doInBackground方法中,定義需要傳遞的參數的數組,示例代碼:
Integer[] values=new Integer[2];
values[0]=i+1;//第一進度值
values[1]=i+1+10;//第二進度值
步驟2、用publishProgress方法發送步驟1定義的values數組,示例代碼:
publishProgress(values);
步驟3、在onProgressUpdate方法中接收,并使用傳遞過來的數組更新UI,示例代碼:
//重寫本方法,更新進度
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
mProgressBar.setProgress(values[0]);//更新進度條中的第一個進度值
mProgressBar.setSecondaryProgress(values[1]);//更新第二個進度值
}
13.2.深入了解Handler
13.2.1.sendEmptyMessage()方法
1、sendEmptyMessage(int what);
2、作用:向Handler對象所工作的線程的消息隊列發送空消息。
3、說明:參數what用于在消息隊列中辨別不同的消息。
4、示例:在視窗中單擊一個按鈕,然後啟動一個工作線程執行一段時間,執行完畢後,向主線程的消息隊列發送一個空消息,主線程在視窗顯示一段文字資訊。
關鍵步驟及代碼如下所示:
步驟1、在Activity.onCreate()方法中建立Handler對象,重寫handleMessage方法,代碼如下所示:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mtvResult=(TextView)findViewById(R.id.tvResult);
Button btnDownload=(Button)findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);//注冊單擊事件
//在主線程建立mHandler并接收工作線程發送的消息
mHandler=new Handler(){
public void handleMessage(android.os.Message msg) {
//判斷msg.what值
switch(msg.what){
case DOWNLOAD_FINISHED://若是該常量
//在标簽的标題上顯示donwload finished
mtvResult.setText("donwload finished");
break;
}
};
};
}
步驟2、在按鈕的單擊事件中建立并啟動線程,線上程中模拟耗時操作結束後,向主線程的消息隊列發送一個空消息,代碼如下所示:
//實作單擊事件
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnDownload:
//建立并啟動線程,模拟下載下傳
new Thread(){
public void run() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//循環結束後,向主線程發送一個空消息,并傳遞一個整數值
mHandler.sendEmptyMessage(DOWNLOAD_FINISHED);
};
}.start();
}
}
13.2.2. 巧用Handler.postDelayed()
13.2.2.1.概述
上一章介紹了Handler類中的post和postDelayed方法,這兩個方法通過發送一個消息至主線程的消息隊列(其中該消息的第二參數是一個實作了Runnable接口的對象),主線程執行該對象中的run方法中的代碼,達到讓主線程執行工作線程中發送的代碼片段的效果。
以下介紹不用new線程對象,隻用postDelayed方法,即可實作一個工作線程定時修改UI的效果。
13.2.2.2.示例
單擊圖-1中的run按鈕,将在視窗中以秒為機關顯示一個計器。
單擊stop按鈕,計時器停止計時。
圖-1
步驟1、在Activity.onCreate()方法中建立按鈕控件,建立mHandler對象。
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//建立控件
mtvResult=(TextView)findViewById(R.id.tvResult);
Button btnDownload=(Button)findViewById(R.id.btnDownload);
Button btnPause=(Button)findViewById(R.id.btnPause);
//注冊按鈕的單擊事件
btnDownload.setOnClickListener(this);
btnPause.setOnClickListener(this);
mHandler=new Handler();
}
步驟2、在按鈕單擊事件中編寫響應單擊run按鈕和stop按鈕的代碼:
//實作單擊事件
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnDownload://單擊下載下傳按鈕
mHandler.post(callback);
break;
case R.id.btnPause://若是暫停按鈕,将callback所在的消息從消息隊列中移除
mHandler.removeCallbacks(callback);
break;
}
}
說明:
第一個紅框中的代碼将callback對象中封裝的代碼發送至主線程的消息隊列,由主線程執行。
第二個紅框中的removeCallbacks方法的作用是将參數callbakc所在的消息從主線程的消息隊列中删除。
步驟3、建立一個實作了Runnable接口的對象-callback,該類實作了run方法,在run方法中以秒為機關,動态顯示時間。代碼如下所示:
//建立一個Runnable接口的實作對象
private Runnable callback=new Runnable(){
//實作run方法
@Override
public void run() {
//顯示時間(機關:秒)再遞增1
mtvResult.setText(mProgress+++"s");
//一秒後再次向消息隊列發送封裝在callback對象中的代碼片段
mHandler.postDelayed(callback, 1000);
}
};
說明:
紅框中代碼調用mHandler.postDelayed方法将callback對象再次發送至消息隊列由主線程執行。如此循環反複。這種做法部分代替了工作線程的作用,即不用再new一個工作線程來重新整理UI.
但要注意:不要在以上代碼的run方法中執行耗時操作,因為Runnable隻是一個接口,而不是線程,該處的代碼是被發送至主線程中執行,若出現耗時操作同樣要阻塞主線程中其它的使用者輸入操作,同樣可能出現ANR現象。
13.2.3.Looper類
13.2.3.1.概述
Android應用程式是消息驅動的,Android系統提供了消息循環機制。Android通過Looper、Handler來實作消息循環機制,Android消息循環是針對線程的(每個線程都可以有自己的消息隊列和消息循環)。
Android系統中Looper負責管理線程的消息隊列(message queue)和消息循環。
13.2.3.2.重要方法
1、Looper.prepare():
作用:建立Looper對象和MessageQueue(消息隊列)對象。
2、Looper.getMainLooper();
作用:獲得主線程的Looper對象。
3、Looper.myLooper();
作用:獲得目前工作線程的Looper對象。
4、Looper.loop();
作用:進入消息循環。
13.2.3.3.Looper運作機制
Looper負責管理線程的消息隊列和消息循環,通過Loop.myLooper()得到目前線程的Looper對象,通過Loop.getMainLooper()獲得主線程的Looper對象。Looper不能用new來執行個體化,而是要用Looper.prepare()執行個體化。
Android中每一個線程都可以(但不是必須)跟着一個Looper,Looper可以幫助線程維護一個消息隊列,每個線程可以存在一個消息隊列和一個消息循環(Looper),特定線程的消息隻能分發給本線程,不能進行跨線程,跨程序通訊。但是建立的工作線程預設是沒有消息循環和消息隊列的,如果想讓該線程具有消息隊列和消息循環,需要線上程中首先調用Looper.prepare()來建立消息隊列,然後調用Looper.loop()進入消息循環。如下例所示
Looper.prepare();//建立消息隊列
myLooper=Looper.myLooper();//獲得目前線程中的Looper對象
Looper.loop();//進入目前線程的消息循環
一個Activity中可以建立多個工作線程,如果這些線程把它們的消息放入主線程消息隊列,那麼該消息就會在主線程中處理了。因為主線程一般負責界面的更新操作,是以這種方式可以很好的實作Android界面更新。Activity、Looper、Handler的關系如下圖所示
其它線程通過Handle對象将消息放入主線程的消息隊列。隻要Handler對象以主線程的Looper建立,那麼調用Handler的sendMessage等方法,會把消息放入主線程的消息隊列。并且Handler将會在主線程中調用handleMessage方法來處理消息。
13.2.3.4.示例
建立一個Thread類線程,該線程具有消息隊列和處理消息隊列的Looper:
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
};
Looper.loop();
不管是主線程還是工作線程,隻要有Looper的線程,别的線程就可以向這個線程的消息隊列中發送消息和計劃任務,然後做相應的處理。
13.2.4. Handler、Message、MessageQueue和Looper四者關系
Looper對象由Looper.prepare()方法來建立,該方法同時會建立MessageQueue,該消息隊列中的Message由Handler發送至主線程的消息隊列,由主線程的MainLooper處理消息。
MainLooper從上至下依次從消息隊列中取出消息進行處理。如圖-2所示:
圖-2
Handler對象在建立時,可以在構造方法中帶參數,也可以不帶參數。若用無參構造方法建立,則Handler在所在的線程中工作。
若用帶參構造方法建立,則按參數在指定的線程中工作。
示例1:
//擷取主線程的Looper對象
Looper mainLooper=Looper.getMainLooper();
Handler handler=new Handler(mainLooper);
則handler對象工作在主線程。以上代碼的第二行也可以寫成:
Handler handler=new Handler(Looper.getMainLooper);
示例2:
//擷取目前線程的Looper對象
Looper myLooper=Looper.myLooper();
//handler工作在目前線程
Handler handler=new Handler(myLooper);
可以做以下比喻:
Message 集裝箱:消息中存放各種類型的資料
MessageQueue 碼頭:存放許多Message
Handler 船:将工作線程中的消息發送至主線程的消息隊列中
Looper 搬運工:處理目前線程的消息隊列中的消息
提示:
Handler不一定都在主線程中建立,也可以在工作線程中建立。如果構造參數為空,那麼在哪個線程中new的Handler,則handlerMessage()方法就在哪個線程中處理該線程中的消息隊列中的消息。
13.3.Handler.Callback接口
13.3.1.概述
Handler在建立時,通常用如下代碼:
mHandler = new Handler(Looper.getMainLooper()) {
public void handleMessage(Message msg) {
//編寫處理消息的代碼
}
};
以下介紹Handler的另一種建立方法,該方法需要使用Handler類的一個内部接口Callback,該接口源代碼如下所示:
public interface Callback {
public boolean handleMessage(Message msg);
}
通過建立一個實作了該接口的對象,然後将該對象作為建立Handler的參數,也能建立Handler對象。
13.3.2.建立步驟
步驟1、建立Handler.Callback對象:
Handler.Callback callback = new Handler.Callback() {
//實作handleMessage方法
@Override
public boolean handleMessage(Message msg) {
//編寫處理消息的代碼
return false;
}
};
步驟2、将callback作為參數,用來建立Handler對象:
Handler handler=new Handler(callback);
13.4. HandlerThread類
13.4.1.概述
上面所示建立能處理消息的線程類的代碼有些麻煩,Android提供了一個線程類HanderThread類,HanderThread類繼承了Thread類,它封裝了Looper對象,我們不用關心Looper的開啟和釋放的細節問題。HandlerThread對象中可以通過getLooper方法擷取一個Looper對象引用。
13.4.2.建立HandlerThread對象的步驟
步驟1、建立handlerThread類型的線程對象,如下代碼所示
mThread=new HandlerThread("MyThread");
步驟2、啟動線程,如下代碼所示:
mThread.start();
步驟3、建立與mThread線程相關聯的Handler對象,代碼如下所示:
//重新建立mHandler,此時的mHandler工作在work thread中
mHandler=new Handler(mThread.getLooper()){
public void handleMessage(Message msg) {
Log.i("looper",Thread.currentThread().getName());
};
};
說明:第一行的參數mThread.getLooper()方法獲得mThread線程對象中的Looper對象,通過該參數将mHandler與mThread關聯起來。
13.5.清單對話框
13.5.1.概述
AlertDialog提供了多種風格的對話框,本節介紹清單對話框,清單對話框如圖-3所示:
圖-3
13.5.2.重要方法
setItems((CharSequence[] items, final OnClickListener listener);
1、作用:建立并顯示清單對話框,并傳回AlertDialog.Builder類的對象。
2、參數說明:
(1)第一個參數:存放菜單中各菜單項标題的數組。
(2)第二個參數,菜單項單擊事件。
提示:OnClickListener接口在android.content.DialogInterface包下,與按鈕等控件的OnClickListener接口名相同,但所在包不同。
13.5.3.示例
以下代碼建立并顯示圖-3,并将被單擊的清單項顯示出來
//從數組資源中解析獲得存放星期的數組
weekDay=getResources().getStringArray(R.array.weekDay);
//建立對話框
Builder dialog=new AlertDialog.Builder(this);
dialog.setIcon(R.drawable.icon);
dialog.setTitle("清單對話框");//設定标題
dialog.setItems(weekDay, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(mContext, weekDay[which], 3000).show();
}
});
dialog.show();//顯示對話框
13.6. 單選清單對話框
13.6.1.概述
AlertDialog提供了單選清單風格的對話框,如圖-4所示:
圖-4
13.6.2.重要方法
Builder setSingleChoiceItems(CharSequence[] items, int checkedItem,
final OnClickListener listener)
1、作用:設定單選清單對話框,并傳回AlertDialog.Builder類的對象。
2、參數說明
(1)第一個參數:存放菜單項的字元串數組。
(2)第二個參數:設定預設被選中的菜單項的索引值。如圖-4中第一個菜單項預設被選中,該值設定為0.
3、第三個參數:菜單項單擊事件。
13.6.3.示例
以下代碼建立并顯示圖-4,并将被單擊的清單項顯示出來
weekDay=getResources().getStringArray(R.array.weekDay);
//建立對話框
Builder dialog=new AlertDialog.Builder(this);
dialog.setIcon(R.drawable.icon);
dialog.setTitle("單選清單對話框");
dialog.setSingleChoiceItems(weekDay, 0,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(mContext, weekDay[which], 3000).show();
}
});
dialog.show();//顯示對話框
13.7.複選清單對話框
13.7.1.概述
AlertDialog提供了複選清單風格的對話框,如圖-5所示:
圖-5
13.7.2.重要方法
Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems,
final OnMultiChoiceClickListener listener)
1、作用:建立并設定複選清單對話框。
2、參數說明:
(1)第一個參數:存放清單項标題的字元串數組。
(2)第二個參數:預設選中/未選中的布爾數組。例如圖-5預設第一、第三個清單項被選中,則checkedItems數組被定義為如下格式:
Boolean checkedItems={true,false,true,false,false,false,false};
(3)第三個參數:複選清單的單擊事件,實作OnMultiChoiceClickListener接口時,要實作接口中定義的OnClick方法。
(4) OnMultiChoiceClickListener.onClick()方法聲明為如下格式:
onClick(DialogInterface dialog, int which, boolean isChecked)
第一個參數:目前的對話框;
第二個參數:被單擊的清單項的索引值。
第三個參數:該清單項是否被選中,選中值為true,否則為false。
13.7.3.示例
以下代碼建立并顯示圖-5的對話框并響應單擊事件,将被單擊的清單項顯示出來:
weekDay=getResources().getStringArray(R.array.weekDay);
//建立對話框
Builder dialog=new AlertDialog.Builder(this);
dialog.setIcon(R.drawable.icon);
dialog.setTitle("複選清單對話框");
dialog.setMultiChoiceItems(weekDay, checkedItems,
new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which,
boolean isChecked) {
Toast.makeText(mContext, weekDay[which], 3000).show();
}
});
dialog.show();//顯示對話框
13.8. 常見問題
現象:在成員變量中初始化UI,得到的元件引用為空,運作時出現空指針異常的提示。
原因:成員變量初始化在構造方法之前,
例如:
Button button = (Button) findViewById(R.id.button1);
setContentView(R.layout.main);
這時還沒有執行setContentView()方法,是以得不到元件的引用,button 為空
解決方法:
findViewById()方法要放在setContentView()之後執行