本次會詳細講解將Android的Snake Sample移植到J2ME上,從而比較二者的區別和聯系。
在《1.Android SDK Sample-Snake詳解》中,我們已經詳細介紹了Android實現的Snake項目結構,現在我們要將這個項目用J2ME實現。
一、 J2ME vs. Android
Android的UI實用、方便,而且很美觀,基本無需改動且定制方便。而J2ME的高級用戶界面比較雞肋,在現在大多數的應用裡都看不到,多數稍微復雜點的界面都是手工畫,或是用一些開源的高級UI庫。接下來我們簡單比較下二者的區別,為Snake項目從Android到J2ME的移植做准備。
1. 平台
J2ME:
開發平台
Android:
操作系統
2. 工程結構
J2ME:
res:資源文件
src:源代碼
Android:
src:源代碼
res\drawable:圖片
res\raw:聲音
res\values:字符串
assets:數據文件
3. 安裝包
J2ME:
jad,jar
Android:
apk
4. 代碼結構
J2ME:
MIDlet,Canvas,采用繼承的方式,只有一個MIDlet,一般只有一個Canvas
Android:
Activity,View,采用繼承的方式,只有一個Activity,一般只有一個View
5. 入口程序
J2ME:
MIDlet類
Android:
Activity類
6. 主程序結構
J2ME:
package com.deaboway.j2me;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
public class MyMidlet extends MIDlet {
protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
// TODO Auto-generated method stub
}
protected void pauseApp() {
// TODO Auto-generated method stub
}
protected void startApp() throws MIDletStateChangeException {
// TODO Auto-generated method stub
}
}
Android:
package com.deaboway.android;
import android.app.Activity;
import android.os.Bundle;
public class myActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
}
}
7. 生命周期-開始
J2ME:
startApp(),活動狀態,啟動時調用,初始化。
Android:
onCreate(),返回時也會調用此方法。
onCreate()後調用onStart(),onStart()後調用onResume(),此時Activity進入運行狀態。
8. 生命周期-暫停
J2ME:
pauseApp(),暫停狀態,如來電時,調用該接口。
Android:
onPause()。
9. 生命周期-銷毀
J2ME:
destroyApp(),銷毀狀態,退出時調用。
Android:
onStop(),程序不可見時調用onDestroy(),程序銷毀時調用。
10. 刷新
J2ME:
高級UI組件由內部刷新實現。低級UI,canvas中通過調用線程結合repaint()來刷新,讓線程不斷循環。低級UI架構可以用MVC方式來實現,建議使用二級緩存。
Android:
高級UIHandler類通過消息的機制刷新。onDraw()刷新接口,低級UI開發者用線程控制更新,在lockCanvas()和unlockCanvasAndPost()方法之間繪制。
如果去讀API,我們可以發現J2ME中Canvas的repaint()與Android中View的invalidate()/postInvalidate()方法實現了相同的功能(連說明文字幾乎都一樣…),但是invalidate()/postInvalidate()兩者卻有著區別:invalidate() 只能在UI這個線程裡通過調用onDraw(Canvas canvas)來update屏幕顯示,而postInvalidate()是要在non-UI線程裡做同樣的事情的。這就要求我們做判斷,哪個調用是本 線程的,哪個不是,這在做多線程callback的時候尤為重要。而在J2ME中,不管怎樣直接調用repaint()就好了。
11. 繪畫
J2ME:
Displayable類。J2me中所有可顯示的組件都是直接或間接的繼承了Displayable,直接的是Canvas和Screen。不同的繼承導致了低級 UI和高級UI的區別。J2me中現成的UI組件都是直接或者間接繼承了Screen。只要調用Display.getDisplay(MIDLet instan).setCurrrent(Displayable disp),就可以把組件顯示到手機界面上。切換界面的時候也可以使用該接口。
Android:
View類。可見的組件直接或者間接繼承了android.view.View。通過 Activity.setContentView(View view)就可以顯示在android手機界面上,切換界面的時候也可以使用該接口。如果是直接繼承了View而不是Android自帶的UI組件,那麼 還要自己去實現它的刷新,類似J2me的低級UI組件。
12. 畫筆
J2ME:
高級UI組件由內部刷新實現。低級UI,canvas中通過調用線程結合repaint()來刷新,讓線程不斷循環。低級UI架構可以用MVC方式來實現,建議使用二級緩存。
Android:
Canvas類,Android繪 制的時候會傳入一個參數Paint。該對象表示繪制的風格,比如顏色,字體大小,字體格式等。Android的Canvas不同於J2ME的Canvas,它更像於J2ME的Graphics,用來繪制。
13. 全屏
J2ME:
Canvas中SetFullScreenMode()。
Android:
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);requestWindowFeature(Window.FEATURE_NO_TITLE);
14. 獲得屏幕尺寸
J2ME:
Canvas類的getHeight()和getWidth()
Android:
Display d = getWindowManager().getDefaultDisplay();
screenWidth = d.getWidth();
screenHeight = d.getHeight();
15. 可繪區域
J2ME:
int clipX = g.getClipX();
int clipY = g.getClipY();
int clipWidth = g.getClipWidth();
int clipHeight = g.getClipHeight();
g.clipRect(x, y, width, height);
g.setClip(clipX, clipY, clipWidth, clipHeight);//釋放當前狀態
Android:
canvas.save();//保存當前狀態
canvas.clipRect(x,y, x+width, y+height)
cavnas.resave();//釋放當前狀態
16. 清屏操作
J2ME:
g.setColor(Color.WHITE);
g.fillRect(0,0,getWidth(),getHeight());
Android:
// 首先定義paint
Paint paint = new Paint();
// 繪制矩形區域-實心矩形
// 設置顏色
paint.setColor(Color.WHITE);
// 設置樣式-填充
paint.setStyle(Style.FILL);
// 繪制一個矩形
canvas.drawRect(new Rect(0, 0, getWidth(), getHeight()), paint);
17. 雙緩沖
J2ME:
Image bufImage=Image.createImage(bufWidth, bufHeight);
Graphics bufGraphics=bufImage.getGraphics();
Android:
Bitmap carBuffer = Bitmap.createBitmap(bufWidth, bufHeight, Bitmap.Config.ARGB_4444);
Canvas carGp = new Canvas(carBuffer);
18. 圖片類
J2ME:
Image類,Image.createImage(path);
Android:
BitMap類,BitmapFactory.decodeResource(getResources(),R.drawable.map0);
19. 繪制矩形
J2ME:
drawRect的後兩個參數為寬度和高度
Android:
drawRect的後兩個參數為結束點的坐標
20. 按鍵
J2ME:
keyPressed()
keyRepeated()
keyReleased()
Android:
onKeyDown()
onKeyUp()
onTracKballEvent()
21. 鍵值
J2ME:
Canvas.LEFT…
Android:
KeyEvent.KEYCODE_DPAD_LEFT…
22. 觸筆
J2ME:
pointerPressed(),pointerReleased(),pointerDragged()
Android:
onTouchEvent()
23. 數據存儲
J2ME:
Record Management System (RMS)
Android:
SQLite數據庫,SharedPreferences類
24. 連接
J2ME:
從Connector打開,可以直接在Connector.Open時設置連接是否可讀寫,以及超時設置
Android:
從URL對象打開,必須使用setDoInput(boolean)和setDoOutput(boolean)方法設置,使用setConnectTimeout(int)不僅可以對連接超時進行設置,還能設置超時時間,參數為0時忽略連接超時
25. 游戲開發包
J2ME:
javax.microedition.lcdui.game.*;
GameCanvas/Layer/LayerManager/ Sprite/TiledLayer
Android:
無專門針對游戲的開發包。
26. 音效
J2ME:
Player s=Manager.createPlayer(InputStream);
s.prepare();//創建
s.start();//播放
s.stop();//暫停
s.stop();//關閉
s.release();//釋放
Android:
MediaPlayer類處理背景音樂
SoundPool類處理一些簡單的音效
27. 顯示文本
J2ME:
String
Android:
TextView類
28. 打印信息
J2ME:
System.out.println()
Android:
Log類
二、 遷移關鍵點
1. 基礎類和結構
J2ME程序的主體從Activity改變為MIDlet,TileView從View改變為Canvas,相關的方法也需要進行調整,但是主體結構和邏輯還是一致的。此外,有些J2ME不支持的類,需要做對應處理。
資源獲取,從xml改為自行獲取:
1 private void initSnakeView() {
2
3 // 獲取圖片資源
4
5 try {
6
7 imageRED_STAR = Image.createImage("/redstar.png");
8
9 imageRED_STAR = Utils.zoomImage(imageRED_STAR,mTileSize,mTileSize);
10
11 imageYELLOW_STAR = Image.createImage("/yellowstar.png");
12
13 imageYELLOW_STAR = Utils.zoomImage(imageYELLOW_STAR,mTileSize,mTileSize);
14
15 imageGREEN_STAR = Image.createImage("/greenstar.png");
16
17 imageGREEN_STAR = Utils.zoomImage(imageGREEN_STAR,mTileSize,mTileSize);
18
19 } catch(Exception e) {
20
21 Log.info("Create Images: "+e);
22
23 }
24
25 // 設置貼片圖片數組
26
27 resetTiles(4);
28
29 // 把三種圖片存到Bitmap對象數組
30
31 loadTile(RED_STAR, imageRED_STAR);
32
33 loadTile(YELLOW_STAR, imageYELLOW_STAR);
34
35 loadTile(GREEN_STAR, imageGREEN_STAR);
36
37 }
ArrayList,用Vector替換掉:
1 // 坐標數組轉整數數組,把Coordinate對象的x y放到一個int數組中——用來保存狀態
2
3 private String coordVectorToString(Vector cvec) {
4
5 int count = cvec.size();
6
7 StringBuffer rawArray = new StringBuffer();
8
9 for (int index = 0; index < count; index++) {
10
11 Coordinate c = (Coordinate) cvec.elementAt(index);
12
13 rawArray.append(c.x+",");
14
15 rawArray.append(c.y+",");
16
17 Log.info("coordVectorToString(), c.x="+c.x+",c.y="+c.y);
18
19 }
20
21 Log.info("coordVectorToString(), rawArray.toString="+rawArray);
22
23 return rawArray.toString();
24
25 }
26
27 // 整數數組轉坐標數組,把一個int數組中的x y放到Coordinate對象數組中——用來恢復狀態
28
29 // @J2ME 還是用Vector替換ArrayList
30
31 private Vector coordStringToVector(String raw) {
32
33 Vector coordArrayList = new Vector();
34
35 Log.info("coordStringToVector(), raw="+raw);
36
37 String[] rawArray = Utils.splitUtil(raw,",");
38
39 Log.info("coordStringToVector(), rawArray.length="+rawArray.length);
40
41 int coordCount = rawArray.length;
42
43 for (int index = 0; index < coordCount; index += 2) {
44
45 Coordinate c = new Coordinate(Integer.parseInt(rawArray[index]), Integer.parseInt(rawArray[index + 1]));
46
47 coordArrayList.addElement(c);
48
49 }
50
51 return coordArrayList;
52
53 }
Bundle,用RMS實現:
1 /**
2
3 * <p>Title: Snake</p>
4
5 * <p>Copyright: (C) 2011 Gavin's Snake project. Licensed under the Apache License, Version 2.0 (the "License")</p>
6
7 * @author Gavin
8
9 */
10
11 package com.deaboway.snake.util;
12
13 import java.io.ByteArrayInputStream;
14
15 import java.io.ByteArrayOutputStream;
16
17 import java.io.DataInputStream;
18
19 import java.io.DataOutputStream;
20
21 import javax.microedition.rms.RecordStore;
22
23 public class Bundle extends BaseRMS {
24
25 private static String[] SECTION = {
26
27 "\"AL\":","\"DT\":",
28
29 "\"ND\":","\"MD\":",
30
31 "\"SC\":","\"ST\":"};
32
33 private static int LEN = SECTION.length;
34
35 private static boolean inited = false;
36
37 private static Bundle INSTANCE;
38
39 public static void INIT(){
40
41 if(inited)return;
42
43 inited = true;
44
45 INSTANCE = new Bundle();
46
47 INSTANCE.loadBundles();
48
49 }
50
51 public static Bundle getInstance(){
52
53 return INSTANCE;
54
55 }
56
57 private String[] CONTENT = new String[LEN];
58
59 private Bundle() {
60
61 super("snake-view");
62
63 }
64
65 public void loadBundles() {
66
67 try {
68
69 this.open();
70
71 this.close();
72
73 } catch (Exception e) {
74
75 try {
76
77 this.close();
78
79 RecordStore.deleteRecordStore(this.getRMSName());
80
81 this.open();
82
83 this.close();
84
85 } catch (Exception ex) {
86
87 }
88
89 }
90
91 }
92
93 public void resetBundles() {
94
95 try {
96
97 this.close();
98
99 RecordStore.deleteRecordStore(this.getRMSName());
100
101 this.open();
102
103 this.close();
104
105 } catch (Exception ex) {
106
107 }
108
109 }
110
111 public void updateBundles() throws Exception {
112
113 try {
114
115 this.openonly();
116
117 updateData();
118
119 if (this.getRecordStore() != null)
120
121 this.close();
122
123 } catch (Exception e) {
124
125 throw new Exception(this.getRMSName() + "::updateBundles::" + e);
126
127 }
128
129 }
130
131 protected void loadData() throws Exception {
132
133 try {
134
135 byte[] record = this.getRecordStore().getRecord(1);
136
137 DataInputStream istream = new DataInputStream(
138
139 new ByteArrayInputStream(record, 0, record.length));
140
141 String content = istream.readUTF();
142
143 int[] start = new int[LEN+1];
144
145 for(int i =0;i<LEN;i++){
146
147 start[i] = content.indexOf(SECTION[i]);
148
149 }
150
151 start[LEN]=content.length();
152
153 for(int i =0;i<LEN;i++){
154
155 CONTENT[i] = content.substring(start[i]+5,start[i+1]);
156
157 Log.info("CONTENT["+i+"]="+CONTENT[i]);
158
159 }
160
161 } catch (Exception e) {
162
163 throw new Exception(this.getRMSName() + "::loadData::" + e);
164
165 }
166
167 }
168
169 protected void createDefaultData() throws Exception {
170
171 try {
172
173 ByteArrayOutputStream bstream = new ByteArrayOutputStream(12);
174
175 DataOutputStream ostream = new DataOutputStream(bstream);
176
177 CONTENT[0] = "9,20,9,7";
178
179 CONTENT[1] = "1";
180
181 CONTENT[2] = "1";
182
183 CONTENT[3] = "600";
184
185 CONTENT[4] = "0";
186
187 CONTENT[5] = "7,7,6,7,5,7,4,7,3,7,2,7";
188
189 StringBuffer sb = new StringBuffer();
190
191 for(int i=0;i<LEN;i++){
192
193 sb.append(SECTION[i]);
194
195 sb.append(CONTENT[i]);
196
197 }
198
199 ostream.writeUTF( sb.toString());
200
201 ostream.flush();
202
203 ostream.close();
204
205 byte[] record = bstream.toByteArray();
206
207 this.getRecordStore().addRecord(record, 0, record.length);
208
209 } catch (Exception e) {
210
211 throw new Exception(this.getRMSName() + "::createDefaultData::" + e);
212
213 }
214
215 }
216
217 protected void updateData() throws Exception {
218
219 try {
220
221 ByteArrayOutputStream bstream = new ByteArrayOutputStream(12);
222
223 DataOutputStream ostream = new DataOutputStream(bstream);
224
225 StringBuffer sb = new StringBuffer();
226
227 for(int i=0;i<LEN;i++){
228
229 sb.append(SECTION[i]);
230
231 sb.append(CONTENT[i]);
232
233 }
234
235 ostream.writeUTF(sb.toString());
236
237 ostream.flush();
238
239 ostream.close();
240
241 byte[] record = bstream.toByteArray();
242
243 this.getRecordStore().setRecord(1, record, 0, record.length);
244
245 } catch (Exception e) {
246
247 throw new Exception(this.getRMSName() + "::updateData::" + e);
248
249 }
250
251 }
252
253 public String getValue(int key) {
254
255 return CONTENT[key];
256
257 }
258
259 public void setValue(int key, String value) {
260
261 CONTENT[key] = value;
262
263 }
264
265 }
Log,自己實現Log系統:
1 /**
2
3 * <p>Title: Snake</p>
4
5 * <p>Copyright: (C) 2011 Gavin's Snake project. Licensed under the Apache License, Version 2.0 (the "License")</p>
6
7 * @author Gavin
8
9 */
10
11 package com.deaboway.snake.util;
12
13 public class Log{
14
15 private static final int FATAL = 0;
16
17 private static final int ERROR = 1;
18
19 private static final int WARN = 2;
20
21 private static final int INFO = 3;
22
23 private static final int DEBUG = 4;
24
25 private static int LOG_LEVEL = INFO;
26
27 public static void info(String string){
28
29 if(LOG_LEVEL >= INFO){
30
31 System.out.println("[Deaboway][ INFO] " + string);
32
33 }
34
35 }
36
37 public static void debug(String string){
38
39 if(LOG_LEVEL >= DEBUG){
40
41 System.out.println("[Deaboway][DEBUG] " + string);
42
43 }
44
45 }
46
47 public static void warn(String string){
48
49 if(LOG_LEVEL >= WARN){
50
51 System.out.println("[Deaboway][ WARN] " + string);
52
53 }
54
55 }
56
57 public static void error(String string){
58
59 if(LOG_LEVEL >= ERROR){
60
61 System.out.println("[Deaboway][ERROR] " + string);
62
63 }
64
65 }
66
67 public static void fatal(String string){
68
69 if(LOG_LEVEL >= FATAL){
70
71 System.out.println("[Deaboway][FATAL] " + string);
72
73 }
74
75 }
76
77 }
2. TileView類
(1)用Image替換BitMap,“private Image[] mTileArray;”
(2)private final Paint mPaint = new Paint();不再需要了。直接在Graphics中drawImage即可
(3)onSizeChanged() 不會被自動調用,需要在構造函數中主動調用,以實現對應功能。onSizeChanged(this.getWidth(),this.getHeight());
(4)最重要的,用paint替換onDraw,呵呵,Canvas的核心啊!失去它你傷不起!!!咱也試試咆哮體!!!!!!
3. SnakeView類
(1)J2ME 沒有Handler,直接用Thread定期repaint()就OK。這裡要啰嗦幾句。
熟悉Windows編程的朋友可能知道Windows程序是消息驅動的,並且有全局的消息循環系統。而Android應用程序也是消息驅動的,按道理來說也應該提供消息循環機制。實際上Android中也實現了類似Windows的消息循環機制,它通過Looper、Handler來實現消息循環機制,Android消息循環是針對線程的(每個線程都可以有自己的消息隊列和消息循環)。
Android系統中Looper負責管理線程的消息隊列和消息循環。Handler的作用是把消息加入特定的(Looper)消息隊列中,並分發和處理該消息隊列中的消息。構造Handler的時候可以指定一個Looper對象,如果不指定則利用當前線程的Looper創建。
一個Activity中可以創建多個工作線程或者其他的組件,如果這些線程或者組件把他們的消息放入Activity的主線程消息隊列,那麼該消息就會在主線程中處理了。因為主線程一般負責界面的更新操作,並且Android系統中的weget不是線程安全的,所以這種方式可以很好的實現Android界面更新。在Android系統中這種方式有著廣泛的運用。
如果另外一個線程要把消息放入主線程的消息隊列,就需要通過Handle對象,只要Handler對象以主線程的Looper創建,那麼調用Handler的sendMessage等接口,將會把消息放入隊列都將是放入主線程的消息隊列。並且將會在Handler主線程中調用該handler的handleMessage接口來處理消息。
之所以Android有這些處理,是因為Android平台來說UI控件都沒有設計成為線程安全類型,所以需要引入一些同步的機制來使其刷新。而對於J2ME來說,Thread比較簡單,直接匿名創建重寫run方法,調用start方法執行即可。或者,也可以從Runnable接口繼承。實現如下:
1 class RefreshHandler extends Thread {
2
3 public void run() {
4
5 while (true) {
6
7 try {
8
9 //delay一個延遲時間單位
10
11 Thread.sleep(mMoveDelay);
12
13 } catch (Exception e) {
14
15 e.printStackTrace();
16
17 }
18
19 // 更新View對象
20
21 SnakeView.this.update();
22
23 // 強制重繪
24
25 SnakeView.this.repaint();
26
27 }
28
29 }
30
31 };
(2)直接使用String代替TextView類,在Canvas的paint()中直接繪制各種提示信息。
(3)在一些地方需要主動調用repaint()進行強制重繪。
其它具體參考源代碼。
4. Snake類
本類就比較簡單了,直接把源代碼貼出來如下:
1 /**
2
3 * <p>Title: Snake</p>
4
5 * <p>Copyright: (C) 2011 Gavin's Snake project. Licensed under the Apache License, Version 2.0 (the "License")</p>
6
7 * @author Gavin
8
9 */
10
11 package com.deaboway.snake;
12
13 import javax.microedition.lcdui.Display;
14
15 import javax.microedition.midlet.MIDlet;
16
17 import javax.microedition.midlet.MIDletStateChangeException;
18
19 import com.deaboway.snake.util.BaseRMS;
20
21 import com.deaboway.snake.util.Bundle;
22
23 import com.deaboway.snake.util.Log;
24
25 /**
26
27 * Snake: a simple game that everyone can enjoy.
28
29 *
30
31 * 貪吃蛇: 經典游戲,在一個花園中找蘋果吃,吃了蘋果會變長,速度變快。碰到自己和牆就掛掉
32
33 *
34
35 */
36
37 public class Snake extends MIDlet {
38
39 public static Display display;
40
41 public static SnakeView mSnakeView;
42
43 public static MIDlet SNAKE;
44
45 public Snake() {
46
47 Bundle.INIT();
48
49 display=Display.getDisplay(this);
50
51 SNAKE = this;
52
53 mSnakeView = new SnakeView();
54
55 mSnakeView.setTextView("");
56
57 mSnakeView.setMode(SnakeView.READY);
58
59 display.setCurrent(mSnakeView);
60
61 }
62
63 protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
64
65 mSnakeView.saveState();
66
67 }
68
69 protected void pauseApp() {
70
71 mSnakeView.setMode(SnakeView.PAUSE);
72
73 }
74
75 protected void startApp() throws MIDletStateChangeException {
76
77 // 檢查存貯狀態以確定是重新開始還是恢復狀態
78
79 Log.debug("startApp(), BaseRMS.FIRST="+BaseRMS.FIRST);
80
81 if (BaseRMS.FIRST) {
82
83 // 存儲狀態為空,說明剛啟動可以切換到准備狀態
84
85 mSnakeView.setMode(SnakeView.READY);
86
87 } else {
88
89 // 已經保存過,那麼就去恢復原有狀態
90
91 // 恢復狀態
92
93 if (!mSnakeView.restoreState()) {
94
95 // 恢復狀態不成功,設置狀態為暫停
96
97 mSnakeView.setMode(SnakeView.PAUSE);
98
99 }
100
101 }
102
103 }
104
105 }
本次就大概介紹這麼多,源代碼將在下次放出。下次主要講解源代碼的存儲和維護,敬請期待。