Android: 如何利用Handler、Thread更新視圖

原文地址

轉載請註明原文出處

===============================================================================

總是感覺android中UI更新很讓人糾結!自己小結一下,算是拋磚引玉。讀這篇文章之前,假設你已經明白線程、handler的使用。


1. 在onCreate()方法中開啓線程更新UI

  1. public class MasterActivity extends Activity {  
  2.     TextView tv = null;  
  3.     Button btn = null;  
  4.       
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.main);  
  9.         System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  10.         tv = (TextView)findViewById(R.id.text);  
  11.         btn = (Button)findViewById(R.id.btn);  
  12.           
  13.         /*onCreate中開啓新線程,更新UI。沒有報錯或者異常信息!*/  
  14.        Thread thread = new Thread(new Runnable() {  
  15.               
  16.             @Override  
  17.             public void run() {  
  18.                 System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  19.                 tv.setText("update UI is success!");  
  20.                 btn.setText("update UI is success!");  
  21.             }});  
  22.         thread.start();  
  23.     }  
 

隨便折騰,不會報錯或者異常!以爲開啓的線程和UI線程(主線程)是同一個線程,但是很不幸,他們的線程id根本是風牛馬不相及!

不知道爲什麼在這裏開啓子線程更新UI就沒有問題!真的想不明白????


2. 在activity如onResume、onStart、反正是以on開頭的回調方法

  1. @Override  
  2.     protected void onRestart() {  
  3.         super.onRestart();  
  4.          /*onRestart中開啓新線程,更新UI*/  
  5.         Thread thread = new Thread(new Runnable() {  
  6.               
  7.             @Override  
  8.             public void run() {  
  9.                 System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  10.                 tv.setText("update UI is success!");  
  11.                 btn.setText("update UI is success!");  
  12.             }});  
  13.         thread.start();  
  14.     }  
  

不好意思,按下返回按鈕在啓動程序,或者按Home健再啓動程序,就這麼折騰幾下,就會包異常!信息如下:

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

意思是:只有主線程纔可以更新UI。

解決辦法:加上postInvalidate()方法。

  1. @Override  
  2.     protected void onRestart() {  
  3.         super.onRestart();  
  4.          /*onRestart中開啓新線程,更新UI*/  
  5.         Thread thread = new Thread(new Runnable() {  
  6.               
  7.             @Override  
  8.             public void run() {  
  9.                 System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  10.                 tv.postInvalidate();  
  11.                 btn.postInvalidate();  
  12.                 tv.setText("update UI is success!");  
  13.                 btn.setText("update UI is success!");  
  14.             }});  
  15.         thread.start();  
  16.     }  
 

postInvalidate()方法,源碼:

  1. public void postInvalidate() {  
  2.         postInvalidateDelayed(0);  
  3.     }  
  4. public void postInvalidateDelayed(long delayMilliseconds) {  
  5.         // We try only with the AttachInfo because there's no point in invalidating  
  6.         // if we are not attached to our window  
  7.         if (mAttachInfo != null) {  
  8.             Message msg = Message.obtain();  
  9.             msg.what = AttachInfo.INVALIDATE_MSG;  
  10.             msg.obj = this;  
  11.             mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);  
  12.         }  
  13.     }  
 

其實,是調用了Handler的處理消息的機制!該方法可以在子線程中直接用來更新UI。還有一個方法invalidate (),稍候再說!


 3.  在Button的事件中開啓線程,更新UI

  1. public class MasterActivity extends Activity {  
  2.     TextView tv = null;  
  3.     Button btn = null;  
  4.       
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.main);  
  9.         System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  10.         tv = (TextView)findViewById(R.id.text);  
  11.         btn = (Button)findViewById(R.id.btn);  
  12.         btn.setOnClickListener(new OnClickListener() {  
  13.               
  14.             @Override  
  15.             public void onClick(View v) {  
  16.                 Thread thread = new Thread(new Runnable() {  
  17.                       
  18.                     @Override  
  19.                     public void run() {  
  20.                         System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  21.                         tv.setText("update UI is success!");  
  22.                         btn.setText("update UI is success!");  
  23.                     }});  
  24.                 thread.start();  
  25.             }  
  26.         });  
  27. }  
 

 Sorry,報錯!即使你加上postInvalidate()方法,也會報這個錯誤。

 android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

 


4. 使用Handler結合多線程更新UI

a. 開啓一個線程,在run方法中通知Handler

b. Handler中使用handleMessage方法更新UI。

  1. public class MasterActivity extends Activity {  
  2.     TextView tv = null;  
  3.     Button btn = null;  
  4.       
  5.     Handler mHandler = new Handler() {  
  6.         @Override  
  7.         public void handleMessage(Message msg) {  
  8.             if(msg.what == 1) {  
  9.                 tv.setText("update UI is success!");  
  10.                 btn.setText("update UI is success!");  
  11.             }  
  12.             super.handleMessage(msg);  
  13.         }  
  14.     };  
  15.       
  16.     @Override  
  17.     public void onCreate(Bundle savedInstanceState) {  
  18.         super.onCreate(savedInstanceState);  
  19.         setContentView(R.layout.main);  
  20.         System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  21.         tv = (TextView)findViewById(R.id.text);  
  22.         btn = (Button)findViewById(R.id.btn);  
  23.         btn.setOnClickListener(new OnClickListener() {  
  24.               
  25.             @Override  
  26.             public void onClick(View v) {  
  27.                 Thread thread = new Thread(new Runnable() {  
  28.                       
  29.                     @Override  
  30.                     public void run() {  
  31.                         System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getId());  
  32.                         Message msg = mHandler.obtainMessage();  
  33.                         msg.what = 1;  
  34.                         msg.sendToTarget();  
  35.                     }});  
  36.                 thread.start();  
  37.             }  
  38.         });  
  39. }  
 
5. Handler和invalidate 方法結合多線程更新UI

方法invalidate主要用在主線程中(即UI線程中),不可以用於子線程。如果在子線程中需要使用postInvalidate方法。
sdk的api有說明:
  1. public void invalidate ()  
  2. Since: API Level 1  
  3. Invalidate the whole view. If the view is visible, onDraw(Canvas) will be called at some point in the future. This must be called from a UI thread. To call from a non-UI thread, call postInvalidate().  
 
看看該方法源碼:
  1. public void invalidate() {  
  2.         if (ViewDebug.TRACE_HIERARCHY) {  
  3.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);  
  4.         }  
  5.         if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {  
  6.             mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;  
  7.             final ViewParent p = mParent;  
  8.             final AttachInfo ai = mAttachInfo;  
  9.             if (p != null && ai != null) {  
  10.                 final Rect r = ai.mTmpInvalRect;  
  11.                 r.set(00, mRight - mLeft, mBottom - mTop);  
  12.                 // Don't call invalidate -- we don't want to internally scroll  
  13.                 // our own bounds  
  14.                 p.invalidateChild(this, r);  
  15.             }  
  16.         }  
  17.     }  
 
invalidate 方法如果你直接在主線程中調用,是看不到任何更新的。需要與Handler結合!
感謝這位“雷鋒”,一個不錯的例子:http://disanji.net/2010/12/12/android-invalidate-ondraw/
只是被我修改了一點,加入times,看看onDraw到底運行多少次。

Android在 onDraw 事件處理繪圖,

而 invalidate() 函數可以再一次觸發onDraw事件,然後再一次進行繪圖動作。

  1. public class MasterActivity extends Activity {  
  2.     static int times = 1;  
  3.    
  4.     /** Called when the activity is first created. */  
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.    
  9.         setContentView( new View(null){  
  10.    
  11.             Paint vPaint = new Paint();  //繪製樣式物件  
  12.             private int i = 0;           //弧形角度  
  13.    
  14.             @Override  
  15.             protected void onDraw (Canvas canvas) {  
  16.                 super.onDraw(canvas);  
  17.                 System.out.println("this run " + (times++) +" times!");  
  18.    
  19.                 // 設定繪圖樣式  
  20.                 vPaint.setColor( 0xff00ffff ); //畫筆顏色  
  21.                 vPaint.setAntiAlias( true );   //反鋸齒  
  22.                 vPaint.setStyle( Paint.Style.STROKE );  
  23.    
  24.                 // 繪製一個弧形  
  25.                 canvas.drawArc(new RectF(60120260320), 0, i, true, vPaint );  
  26.    
  27.                 // 弧形角度  
  28.                 if( (i+=10) > 360 )  
  29.                     i = 0;  
  30.    
  31.                 // 重繪, 再一次執行onDraw 程序  
  32.                 invalidate();  
  33.             }  
  34.         });  
  35.     }  
  36. }  
 

經過測試,發現times一直在++,說明onDraw被多次調用,並且一致在畫圖!

sdk的api有時候讓人很鬱悶,無語.....關於invalidate的使用,還待探索。革命尚未成功,同志仍需努力!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章