模擬一個 CandyBox (糖果盒),裡面放了 100 顆糖果,然後由 13 個 kid (小孩) 隨意抓取,每次抓取的糖果數量為 5 ~ 10 顆不等 (以亂數決定),每個小孩抓取的次數不限,直到糖果盒裡面的糖果被取完為止,最後再以文字的方式將各個數據結果呈現在螢幕上。
◎此範例主要是模擬多個執行緒共同取用一個固定值的程式架構示範,並沒有穿插精彩的圖片動畫作為輔助說明。
程式說明 :
程式主要分為四個部份 :
1. MainActivity : 主要的 Activity (Activity 類別)
2. CandyBox : 糖果盒類別 (自訂類別)
3. Kid : 小孩類別 (自訂類別並實作 Runnable 介面)
4. ExecResult : 負責處理整個事件運作的核心類別 (View類別)
在 MainActivity 中僅簡單地設定了全螢幕顯示模式,而負責呈現畫面內容的 setContentView() 函式中則將 ExecResult 類別物件導入。其他部份則維持原始設定。
程式碼如下 :
MainActivity.java
package a.b.c.testsync;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.Window;
import android.view.WindowManager;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設定全螢幕
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(new ExecResult(this));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
CandyBox 類別基本上算是一個靜態類別,此類別並不會被拿來生成物件 (object),所以不會在其他程序中看到類似 CandyBox candyBox = new CandyBox(); 的敘述發生。
此類別的構造非常簡單,兩個 static 資料欄位 (candy 與 catchCount) 和一個 static 函式 (函式名稱 : takeout),candy 與 catchCount 分別記錄糖果剩餘數量與抓取次數的總合。
注意到 static 函式 takeout 使用了 synchronized (同步) 修飾詞,當函式宣告為 synchronized 後,於程式執行期間,若有一執行緒呼叫此函式,在該函式未完成此執行緒交付的工作之前,其他執行緒無法再進入執行此函式。
您可以想成這個糖果盒設定了一道規則,就是一次只能允許一個小孩來抓取裡面的糖果;而這個 takeout 函式就是糖果盒( CandyBox 類別 )所設定的規則,同一時間只能允許一個 kid 執行緒來抓取糖果。
在現實的世界中即使所有的小孩在同一時間中搶成一團,不管各自搶到多少糖果,在糖果還未被吃掉之前,糖果的總合並不會有所改變。但反觀在多執行緒程式運作的世界裡,若沒有針對數值資料作保護限制,最後呈現的結果將會出乎意料、錯誤百出。
◎synchronized 同步敘述還有其他用法,這裡僅示範以函式為施行對象。
程式碼如下 :
CandyBox.java
package a.b.c.testsync;
public class CandyBox {
static int candy;
static int catchCount;
//靜態(類別)初始函式
static {
candy = 100; //預設有 100 顆糖果
catchCount = 0; //記錄糖果被抓取的次數總合
}
//糖果抓取事件
protected synchronized static int takeout(int count) {
//candy:糖果目前剩餘的數量 count:此次要取出的糖果數量
//當糖果的剩餘數量不足以應付要取出的數量時
if (candy <= count) {
//剩多少就給多少
count = candy;
candy = 0;
} else {
//扣除被取走的糖果
candy -= count;
}
catchCount++; //記錄糖果被抓取的次數總合
return count; //傳回該次糖果被抓取時所取出的數量
}
}
Kid 類別的結構也很簡單,由於我們只需要這個 kid (小孩) 去做抓取糖果的動作,沒有要他們互相打架,所以不會發武器給他們 XD。
此類別設置了一個口袋 pocket 用來放置糖果,count 是這個小孩一次想抓取的糖果數量,因為此類別必須 "動起來" 所以我實作了 Runnable 介面,只要一生成此類別物件,其執行緒也會跟著啟動。
執行緒的工作內容 : public void run() { //工作內容 ........ }
小孩 (kid) 會先去檢查糖果盒 (CandyBox) 裡面是否還有糖果,接著以亂數值來模擬每次抓取一把糖果的數量 count,再透過呼叫 CandyBox.takeout(int count) 函式確定實際取得的糖果數量,最後再放入口袋 (pocket) 中。
在糖果還未被搶光以前,每個小孩都可能還有第二次 (甚至第三次) 機會由糖果盒中抓取糖果,程式中雖然設定執行緒每次抓取糖果後會稍停 0.2 秒空檔 -- Thread.sleep(200) 以便讓其它小孩也有空檔可以抓取糖果,但仍無法保證每個小孩都可以搶到糖果。
簡略來說,小孩 (kid) 的工作內容就是去糖果盒 (CandyBox) 抓取 (takeout) 糖果 (candy) 並放入口袋 (pocket),當小孩發現 CandyBox 裡的糖果 (candy <= 0) 都已經沒有的時候,該小孩才會停止動作 (即執行緒停止)。
最後我們可以藉由 checkpocket 函式來檢查這個小孩共拿了多少糖果。
程式碼如下 :
Kid.java
package a.b.c.testsync;
public class Kid implements Runnable {
private int pocket = 0; //口袋
private boolean flag = true;
private int count = 0; //抓取一把糖果的數量
//建構函式
public Kid() {
new Thread(this).start();
}
//檢查口袋
protected int checkpocket() {
return pocket; //傳回口袋裡的糖果數量
}
@Override
public void run() {
// TODO Auto-generated method stub
while(flag){
if (CandyBox.candy <= 0) {
count = 0;
flag = false;
} else {
count = (int)(Math.random()*6)+5; //count 亂數值介於 5 ~ 10
}
int temp = 0;
temp = CandyBox.takeout(count); //從CandyBox中 隨機抓取 5 ~ 10 顆糖果
pocket += temp; //把抓取到的糖果放入口袋
try {
Thread.sleep(200); //延遲 0.2 秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
最後一個類別 ExecResult,它繼承了 View 類別,其主要工作內容如下 :
1. 生成 kid 物件
2. 隨時檢查 CandyBox 裡的 candy 是否已經歸零
3. 負責處理各項數據的畫面佈局
4. 加入螢幕觸碰函式 (這裡的作用是碰觸螢幕後就直接結束程式)
程式中宣告了一個物件陣列 ArrayList,用 ArrayList 來存放所產生的 kid 物件,以便後續的一些數據操作,程式中設定產生 13 個 kid 物件。
當這些 kid 物件被產生之後,每一個 kid 物件隨即啟動自身的執行緒進行指定的工作,kid 的工作細節請回顧上方 Kid 類別的內容。
在生成 13 個 kid 物件並放入 kid 物件陣列後,接著會執行 checkResult() 函式,這個函式裡面宣告了一個 check 執行緒,並在函式內啟動,check 執行緒會不斷地去檢查 CandyBox 裡的 candy 數量,一旦確定 candy 為零時,就會執行 invalidate() 函式,而這個 invalidate() 函式會自動去呼叫 onDraw() 函式,onDraw() 函式會依照我們所指定的內容將數據結果顯示到畫面上。
程式碼如下 :
ExecResult.java
package a.b.c.testsync;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.View;
public class ExecResult extends View {
ArrayList<Kid> kid;
Paint paint;
boolean flag; //執行緒 check 旗標
//建構函式
public ExecResult(Context context) {
super(context);
// TODO Auto-generated constructor stub
paint = new Paint(); //產生畫筆物件
kid = new ArrayList<Kid>(); //產生 Kid 物件陣列
initialSet(); //初始設定
}
//初始設定函式
private void initialSet() {
CandyBox.candy = 100; //設定糖果盒有 100 顆糖果
CandyBox.catchCount = 0; //清除抓取總合
//設定畫筆
paint.setColor(Color.WHITE);
paint.setTextSize(30);
paint.setStrokeWidth(2);
paint.setAntiAlias(true);
//checkResult 函式裡的 check 執行緒旗標
flag = true;
kid.clear(); //先清除 kid 物件陣列
//產生 13 個 Kid 執行緒
for (int i=0; i<13; i++) {
kid.add(new Kid());
}
//檢查結果函式(呼叫後將同時啟動其內部的 check 執行緒)
checkResult();
}
protected void checkResult() {
//宣告在函式內部的執行緒及工作內容
Runnable check = new Runnable() {
//執行緒 check 的工作內容
@Override
public void run() {
// TODO Auto-generated method stub
while(flag) {
//檢查 CandyBox 內的 candy 數量是否歸零
if(CandyBox.candy <= 0) {
flag = false; //設定 flag 為 false 準備結束執行緒
invalidate(); //invalidate()會執行 onDraw()函式,繪出結果
}
}
}
};
//上方寫好了執行緒的工作內容之後
//接著把 check 套入 Thread 類別並將它啟動
Thread p;
p = new Thread(check);
p.start(); //啟動 check 執行緒
//如果於執行期間造成緒程方面的錯誤而導致程式被迫中斷
//請使用 join(),等待此 check 執行緒結束,即可解決此情形
try {
p.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.BLACK); //清除畫面
canvas.drawText("糖果盒裡面共有 100 顆糖果", 10, 30, paint);
//當所有的糖果都被取光時 印出結果
if (done){
int num = 0;
int sum = 0;
for (Kid k : kid) {
num++;
sum += k.checkpocket();
canvas.drawText("第 " + num + " 號 kid 拿了 " + k.checkpocket() +
" 個糖果", 10, 60 + 30 * num, paint);
}
canvas.drawText("糖果盒剩 : " + CandyBox.candy + " 顆 ", 10, 30 + 30 * (num + 3, paint);
canvas.drawText("共發生 " + CandyBox.catchCount + " 次抓取事件", 10, 30 + 30 * (num + 4), paint);
canvas.drawText("所有 Kid 的糖果總合 : " + sum , 10, 30 + 30 * (num + 5), paint);
}
}
}
執行結果 :




好複雜的程式,看了眼花。這跟FB的糖果遊戲是一樣的嗎?我玩這個遊戲很遜,應該是所有遊戲是都一樣
回覆刪除希維亞
ㄟ嘿 ~
刪除妳怎跑到這裡留言了 ~.~
這只是在描述某個語法的實際運用 .... 與 FB 的 Candy Crush 完全無關 XD
最近 Candy Crush 很熱門 ! 不曉得妳到第幾關了 ? XDDD
達仔, 您好。難得看到台灣有人這麼熱心寫這麼詳細的文章介紹Android開發。感謝!
回覆刪除Android應該會是最普及的平台。加油!