/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* AndroidWorld Library, Copyright 2011 Bryan Chadwick *
* *
* FILE: ./android/world/BigBang.java *
* *
* This file is part of AndroidWorld. *
* *
* AndroidWorld is free software: you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation, either version *
* 3 of the License, or (at your option) any later version. *
* *
* AndroidWorld is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with AndroidWorld. If not, see
The initial value of the World assigns a (minimum) type, which is
* used to search/check all of the handlers. Functions that produce a world
* deserve special attention, since they may return a super-type of the
* initial World (e.g., initial EmptyScene, with an tick handler that
* returns a Scene). The name and types of handlers are given in the
* table below:
*
*
*
Event Name | BigBang Method | Handler Signature | Required? |
---|---|---|---|
OnDraw | onDraw(handler) | Scene apply(World w) | yes |
OnTick | onTick(handler) or ontick(handler, double) | World apply(World w) | no |
OnMouse | onMouse(handler) | World apply(World w, int x, int y, String what) | no |
OnKey | onKey(handler) | World apply(World w, String key) | no |
OnRelease | onRelease(handler) | World apply(World w, String key) | no |
StopWhen | stopWhen(handler) | boolean apply(World w) | no |
LastScene | lastScene(handler) | Scene apply(World w) | no |
Event Name | BigBang Method | Handler Signature | Required? |
---|---|---|---|
Orientation | orientation(handler) | World apply(World w, float x, float y, float z) | no |
The three dimensional vector represents the direction of gravitational force (i.e., the ground) * as compared to the device at rest (e.g., flat on a level table) where Z points directly at the * ground. The X and Y vectors are in the device's screen coordinates, and Z typically points out * the back of the device.
*/ public BigBang orientation(Object orientation){ Method orientationM = checkTypes(orientation, new Class[]{worldType,float.class,float.class,float.class}, worldType, "Orientation", true, true); return new BigBang(this.initial, this.worldType, this.time, this.ondraw, this.ondrawM, this.ontick, this.ontickM, this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM, this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM, orientation, orientationM); } // Private constructor... private BigBang(Object init, Class> worldT, double time, Object ondraw, Method ondrawM, Object ontick, Method ontickM, Object onmouse, Method onmouseM, Object onkey, Method onkeyM, Object onrelease, Method onreleaseM, Object stopwhen, Method stopwhenM, Object lastscene, Method lastsceneM, Object orientation, Method orientationM){ this.initial = init; this.worldType = worldT; this.time = time; this.ondraw = ondraw; this.ondrawM = ondrawM; this.ontick = ontick; this.ontickM = ontickM; this.onmouse = onmouse; this.onmouseM = onmouseM; this.onkey = onkey; this.onkeyM = onkeyM; this.onrelease = onrelease; this.onreleaseM = onreleaseM; this.stopwhen = stopwhen; this.stopwhenM = stopwhenM; this.lastscene = lastscene; this.lastsceneM = lastsceneM; this.orientation = orientation; this.orientationM = orientationM; } /** Check/find a method compatible with the given types in the * given function Object/Handler */ private Method checkTypes(Object f, Class>[] args, Class> ret, String what, boolean nullable, boolean wret){ Class> fClass = f.getClass(); Method[] possibles = fClass.getDeclaredMethods(); for(Method m : possibles){ if(m.getName().equals(Util.funcObjMethName) && Util.subtypes(args, m.getParameterTypes())){ if(Util.subtype(m.getReturnType(), ret)) return m; if(Util.subtype(ret, m.getReturnType()) && !m.getReturnType().equals(Object.class)){ this.worldType = m.getReturnType(); return m; } } } throw Util.exceptionDrop(2, "\n** Function Object ("+fClass.getSimpleName()+") used for "+ what.toLowerCase()+" does not contain a method sutable for:\n "+ ret.getSimpleName()+" "+Util.funcObjMethName+"("+Util.argsString(args,0)+")"); } /** Wrapper for the Tick Handler */ private Object doOnTick(Object w) { return Util.applyFunc(ontick, ontickM, new Object[]{w}); } /** Wrapper for the Mouse Handler */ private Object doOnMouseEvent(Object w, int x, int y, String me) { return Util.applyFunc(onmouse, onmouseM, new Object[]{w,x,y,me}); } /** Wrapper for the Key Handler */ private Object doOnKeyEvent(Object w, String ke){ if(ke.length() == 0)return w; return Util.applyFunc(onkey, onkeyM, new Object[]{w,ke}); } /** Wrapper for the Key Release Handler */ private Object doOnKeyRelease(Object w, String ke){ if(ke.length() == 0)return w; return Util.applyFunc(onrelease, onreleaseM, new Object[]{w,ke}); } /** Wrapper for the Draw Handler */ private Scene doOnDraw(Object w) { return (Scene)Util.applyFunc(ondraw, ondrawM, new Object[]{w}); } /** Wrapper for the StopWhen Handler */ private boolean doStopWhen(Object w) { if(stopwhen == null)return false; return (Boolean)Util.applyFunc(stopwhen, stopwhenM, new Object[]{w}); } /** Wrapper for the LastScene Handler */ private Scene doLastScene(Object w) { if(lastscene == null)return doOnDraw(w); return (Scene)Util.applyFunc(lastscene, lastsceneM, new Object[]{w}); } /** Wrapper for the Orientation Handler */ private Object doOnOrientationEvent(Object w, float x, float y, float z){ if(orientation == null)return w; return Util.applyFunc(orientation, orientationM, new Object[]{w,x,y,z}); } /** Construct and start the animation/interaction system. For the * Android version the client must pass the initiating Activity * in order to connect the handlers to the necessary events. * The method returns the initial World (for no good reason) * since the Activity.onCreate(...), must return * before the application may start. */ public Object bigBang(Activity act){ if(ondraw == null) throw new RuntimeException("No World Draw Handler"); Scene scn = doOnDraw(initial); if(this.handler == null){ this.handler = new Handler(act, this, initial, Bitmap.createBitmap(scn.width(), scn.height(), Bitmap.Config.ARGB_8888)); act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); act.addContentView(handler, new ViewGroup.MarginLayoutParams(scn.width(),scn.height())); if(orientation != null){ SensorManager s = (SensorManager)act.getSystemService(Context.SENSOR_SERVICE); s.registerListener(handler, s.getDefaultSensor(SensorManager.SENSOR_ORIENTATION), SensorManager.SENSOR_DELAY_GAME); } } handler.requestFocus(); return handler.w; } /** Construct and start the animation/interaction system * with the Android device in LANDSCAPE mode. */ public Object bigBangLandscape(Activity act){ Object res = this.bigBang(act); act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); return res; } /** Construct and start the animation/interaction system * with the Android device in FULLSCREEN mode. */ public Object bigBangFullscreen(Activity act){ act.requestWindowFeature(Window.FEATURE_NO_TITLE); act.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); Object res = this.bigBang(act); return res; } /** Construct and start the animation/interaction system * with the Android device in LANDSCAPE mode. */ public Object bigBangLandscapeFullscreen(Activity act){ act.requestWindowFeature(Window.FEATURE_NO_TITLE); act.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); Object res = this.bigBangLandscape(act); return res; } /** Handles the nitty-gritty of world updates and interfacing with Android APIs */ static class Handler extends EditText implements android.hardware.SensorEventListener, View.OnLongClickListener{ private static final long serialVersionUID = 1L; Activity act; BigBang world; Object w; Scene scnBuffer; Bitmap buffer; Timer run; TimerTask ticker; boolean isRunning = true; boolean isDone = false; boolean tap_down = false; int lastX = 0, lastY = 0; static class DoTick extends TimerTask{ Handler handler; DoTick(Handler handler){ this.handler = handler; } public void run(){ handler.tickAction(); } } /** Create a new Handler for all the World's events */ Handler(Activity act, BigBang world, Object w, Bitmap buff){ super(act); this.act = act; this.world = world; this.w = w; this.scnBuffer = null; this.buffer = buff; this.run = new Timer(); this.resetTimer(); this.setFocusable(true); act.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); //!! Make sure the keyboard doesn't open when we click this.setInputType(InputType.TYPE_NULL); if(world.onmouse != null){ this.setOnLongClickListener(this); } } /** Unpause this previously paused simulation/animation */ public void unpause(){ if(this.isDone)return; if(world.ontick != null && !this.isRunning){ this.resetTimer(); this.isRunning = true; } } /** Pause this simulation/animation */ public void pause(){ if(this.isDone)return; if(world.ontick != null && this.isRunning){ this.run.cancel(); this.isRunning = false; } } /** Setup a new Timer task for on-tick */ public void resetTimer(){ run.cancel(); run = new Timer(); run.schedule(this.ticker = new DoTick(this), 200, (int)(world.time*1000)); } /** Android uses onDraw(Canvas) instead of * paint(Graphics), though the two systems are * very similar. */ public void onDraw(Canvas c){ Scene curr; if(!this.isDone) curr = this.world.doOnDraw(this.w); else curr = this.world.doLastScene(this.w); if(curr != this.scnBuffer){ this.scnBuffer = curr; Canvas c2 = new Canvas(buffer); c2.drawRect(0,0, this.getWidth(), this.getHeight(), Image.WHITE); this.scnBuffer.paint(c2, 0, 0); } c.drawBitmap(buffer, 0, 0, Image.WHITE); } /** Android is not compatible with Swing timers, so we switch * everything over to to java.util.Timer */ public void tickAction(){ if(!this.isRunning || this.isDone)return; replace(this.world.doOnTick(this.w)); } static boolean screenShots = true; /** Keys in Android systems without a hardware keyboard are handled slightly different. On * the HTC Incredible holding the Menu button for an extended time will Show/Hid the * software keyboard. */ public boolean onKeyDown(int keyCode, KeyEvent e){ if(!this.isRunning || this.isDone)return super.onKeyDown(keyCode, e); if(world.onkey == null || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME)return super.onKeyDown(keyCode, e); if(Handler.screenShots && keyCode == KeyEvent.KEYCODE_MENU){ final boolean oldRun = this.isRunning; this.isRunning = false; this.run.cancel(); final String dir = "/screenshots"; new File(Environment.getExternalStorageDirectory()+dir).mkdirs(); final EditText et = new EditText(this.act); et.setText(Environment.getExternalStorageDirectory()+dir+"/screenshot.png"); new AlertDialog.Builder(this.act) .setMessage("Save Image As...") .setView(et) .setPositiveButton("Save", new Dialog.OnClickListener(){ public void onClick(DialogInterface d, int which){ String file = et.getText().toString(); try{ Handler.this.scnBuffer.toFile(file); }catch(RuntimeException ee){ Throwable e = ee.getCause(); new AlertDialog.Builder(Handler.this.act) .setMessage("Unable to save:\n '"+file+"'\n ("+ e.getClass().getSimpleName()+")\n "+e.getMessage()) .show(); } Handler.this.isRunning = oldRun; resetTimer(); }}) .setNegativeButton("Cancel", new Dialog.OnClickListener(){ public void onClick(DialogInterface d, int which){ Handler.this.isRunning = oldRun; resetTimer(); }}) .show(); } String key = convert(e.getKeyCode(), ""+((char)e.getUnicodeChar())); replace(world.doOnKeyEvent(w, key)); return true; } public boolean onKeyUp(int keyCode, KeyEvent e){ if(!this.isRunning || this.isDone)return super.onKeyDown(keyCode, e); if(world.onkey == null || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME)return super.onKeyUp(keyCode, e); String key = convert(e.getKeyCode(), ""+((char)e.getUnicodeChar())); replace(world.doOnKeyRelease(w, key)); return true; } /** Track-ball Events count for a mix of Mouse and Key events */ public boolean onTrackballEvent(MotionEvent e){ if(!this.isRunning || this.isDone)return super.onTrackballEvent(e); if(world.onkey == null)return super.onTrackballEvent(e); if(e.getAction() == MotionEvent.ACTION_DOWN) replace(world.doOnKeyEvent(w, "\n")); else if(e.getAction() == MotionEvent.ACTION_UP) replace(world.doOnKeyRelease(w, "\n")); if(e.getX() > 0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_RIGHT)); else if(e.getX() < -0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_LEFT)); if(e.getY() > 0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_DOWN)); else if(e.getY() < -0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_UP)); return true; } /** Android supports a long-press... so we capture that as a * separate event */ public boolean onLongClick(View v){ if(!this.isRunning || this.isDone)return false; if(world.onmouse == null)return false; replace(world.doOnMouseEvent(w, this.lastX, this.lastY, LONG_MOUSE_DOWN)); return true; } /** Touch events are like Mouse events, but there's no real * analog of "move" for touch screens */ public boolean onTouchEvent(MotionEvent e){ if(!this.isRunning || this.isDone)return super.onTouchEvent(e); if(world.onmouse == null)return super.onTouchEvent(e); this.lastX = (int)e.getX(); this.lastY = (int)e.getY(); String what = ""; if(e.getAction() == MotionEvent.ACTION_DOWN){ what = MOUSE_DOWN; tap_down = true; }else if(e.getAction() == MotionEvent.ACTION_UP){ what = MOUSE_UP; tap_down = false; }else if(e.getAction() == MotionEvent.ACTION_MOVE) what = tap_down?MOUSE_DRAG:MOUSE_MOVE; if(what.length() != 0){ replace(world.doOnMouseEvent(w, (int)e.getX(), (int)e.getY(), what)); } super.onTouchEvent(e); return true; } /** Required for sensor interface implementation */ public void onAccuracyChanged(Sensor sensor, int accuracy){} /** Orientation listener methods */ public void onSensorChanged(SensorEvent event){ if(!this.isRunning || this.isDone)return; if(event.values.length < 3)return; replace(world.doOnOrientationEvent(w, event.values[0], event.values[1], event.values[2])); } private void replace(Object w){ // This isn't enough when mutation is involved... if(!this.isRunning || this.isDone)return; if(this.isRunning && this.world.doStopWhen(w)){ this.isRunning = false; this.isDone = true; this.run.cancel(); } boolean change = !this.w.equals(w); this.w = w; if(change)this.postInvalidate(); } private String convert(int code, String ch){ switch(code){ case KeyEvent.KEYCODE_DPAD_UP: return KEY_ARROW_UP; case KeyEvent.KEYCODE_DPAD_DOWN: return KEY_ARROW_DOWN; case KeyEvent.KEYCODE_DPAD_LEFT: return KEY_ARROW_LEFT; case KeyEvent.KEYCODE_DPAD_RIGHT: return KEY_ARROW_RIGHT; case KeyEvent.KEYCODE_MENU: return KEY_MENU; case KeyEvent.KEYCODE_SEARCH: return KEY_SEARCH; case KeyEvent.KEYCODE_DEL: return "\b"; default: return ch; } } } /** Unpause this previously paused BigBang simulation/animation */ public void unpause(){ this.handler.unpause(); } /** Pause this BigBang simulation/animation */ public void pause(){ this.handler.pause(); } }