Tillbaka till lektionslistan

Mobila applikationer med Android: Lektion 8

Idag:

Klicka på startknappen i den lilla mediaspelaren ovan för att lyssna på lektionen. (Man kan behöva vänta en stund på att ljudfilen laddas ner.) Om mediaspelaren inte syns, eller om det inte fungerar av något annat skäl, kan man klicka här för att ladda ner mp3-filen (ca 23 minuter, ca 11 megabyte). Beroende på hur webbläsaren är konfigurerad kan det kräva ett separat mp3-spelarprogram av något slag.

Bild 1: Satellite Rush

Ett Breakout-liknande spel.

Hur det ska se ut (i emulatorn)

Bild 2: På en riktig telefon

HTC Desire

Bild 3: Användning

Användning på riktigt

Bild 4: Meny

Vi låter vår Activity definiera metoderna onCreateOptionsMenu och onOptionsItemSelected.

Hur det ska se ut (i emulatorn)

Bild 5: Dialog

En enkel ja/nej-dialog med klassen AlertDialog.

Hur det ska se ut (i emulatorn)

Bild 6: Hjälp-skärmen

Hjälp-skärmen är en egen Activity som startas med hjälp av ett Intent.

Kom ihåg: "An activity is a single, focused thing that the user can do. Almost all activities interact with the user, so the Activity class takes care of creating a window for you in which you can place your UI [...]"

Nytt: "An intent is an abstract description of an operation to be performed. It can be used with startActivity to launch an Activity [...]"

Hur det ska se ut (i emulatorn)

Bild 7: Spelet startar

Startskärmen.

Hur det ska se ut (i emulatorn)

Bild(er) 8: Ena hörnet

Vi använder kommandot Left Corner för att sätta vänstra hörnet.

Hur det ska se ut (i emulatorn)       Hur det ska se ut (i emulatorn)

Bild(er) 9: Med GPS

GPS:en måste förstås vara igång.

Hur det ska se ut (i emulatorn)       Hur det ska se ut (i emulatorn)

Bild 10: Högra hörnet

Vi använder kommandot Right Corner för att sätta högra hörnet.

Hur det ska se ut (i emulatorn)

Bild(er) 11: GPS!

Hörnen måste ha ett visst avstånd.

Hur det ska se ut (i emulatorn)       Hur det ska se ut (i emulatorn)       Hur det ska se ut (i emulatorn)

Bild 12: Starta spelet

Vi använder kommandot Start Game för att starta spelet.

Hur det ska se ut (i emulatorn)

Bild 13: Spela!

Vi spelar.

Hur det ska se ut (i emulatorn)

Bild 14: Röda bollar om man missar

Om man missar den blåa bollen med racketen, ersätts en nerskjuten grön boll med en ny, röd boll.

Hur det ska se ut (i emulatorn)

Bild 15: You win!

Spelet är slut när inga gröna eller röda bollar finns kvar.

Hur det ska se ut (i emulatorn)

"Bild" 16: Rättigheter mm

Hela projektet kan laddas ner som en Zip-fil: SatelliteRush.zip

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="se.nekotronic.satelliterush"
      android:versionCode="1"
      android:versionName="0.1">

    <uses-sdk android:minSdkVersion="3"
        android:targetSdkVersion="11" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".RushActivity"
                  android:label="@string/app_name"
                  android:screenOrientation="nosensor">
                  
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity android:name=".HelpActivity"
                  android:label="Satellite Rush Help">
        </activity>
        
        <supports-screens
            android:smallScreens="true"
            android:normalScreens="true"
            android:largeScreens="true"
            android:xlargeScreens="true"
            android:anyDensity="true"></supports-screens>

    </application>

    <uses-permission android:name="android.permission.WAKE_LOCK"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    
</manifest>

"Bild" 17: Koden för hjälp-aktiviteten

HelpActivity.java

    1   package se.nekotronic.satelliterush;
    2   
    3   import android.app.Activity;
    4   import android.content.Context;
    5   import android.content.pm.PackageInfo;
    6   import android.content.pm.PackageManager;
    7   import android.content.pm.PackageManager.NameNotFoundException;
    8   import android.os.Bundle;
    9   import android.text.Html;
   10   import android.text.method.LinkMovementMethod;
   11   import android.view.View;
   12   import android.view.View.OnClickListener;
   13   import android.widget.Button;
   14   import android.widget.TextView;
   15   
   16   public class HelpActivity extends Activity implements OnClickListener {
   17       
   18       @Override
   19       protected void onCreate(Bundle savedInstanceState) {
   20           super.onCreate(savedInstanceState);
   21           setContentView(R.layout.help);
   22           final Button back = (Button)findViewById(R.id.back_button);
   23           back.setOnClickListener(this);
   24           TextView view = (TextView) findViewById(R.id.helpview);    
   25           String text = getString(R.string.help_text);
   26   
   27           try {
   28               PackageManager manager = getPackageManager();
   29               PackageInfo info = manager.getPackageInfo("se.nekotronic.satelliterush", 0);
   30               String version_name = info.versionName;
   31               // int version_code = info.versionCode;
   32               text += "This is Satellite Rush version " + version_name + ".<br></br>"; // Not &lt;br> here!
   33           }
   34           catch(NameNotFoundException nnf) {
   35               // Couldn't find the version number. Just ignore it.
   36           }
   37   
   38           view.setText(Html.fromHtml(text));
   39           view.setMovementMethod(LinkMovementMethod.getInstance());
   40       }
   41   
   42       @Override
   43       public void onClick(View v) {
   44           finish(); // Finish this activity
   45       }
   46   
   47   } // class HelpActivity

"Bild" 18: Koden för huvud-aktiviteten

RushActivity.java

    1   package se.nekotronic.satelliterush;
    2   
    3   import android.app.Activity;
    4   import android.app.AlertDialog;
    5   import android.content.Context;
    6   import android.content.DialogInterface;
    7   import android.content.Intent;
    8   import android.os.Bundle;
    9   import android.os.PowerManager;
   10   import android.os.PowerManager.WakeLock;
   11   import android.util.Log;
   12   import android.view.Menu;
   13   import android.view.MenuItem;
   14   
   15   public class RushActivity extends Activity {
   16       private RushView the_view;
   17       private PowerManager the_power_manager;
   18       private WakeLock the_wake_lock;
   19   
   20       /** Called when the activity is first created. */
   21       @Override
   22       public void onCreate(Bundle savedInstanceState) {
   23           super.onCreate(savedInstanceState);
   24           setContentView(R.layout.main);
   25           the_view = (RushView) findViewById(R.id.rushview);
   26   
   27           // Get an instance of the PowerManager
   28           the_power_manager = (PowerManager) getSystemService(POWER_SERVICE);
   29   
   30           // Create a bright wake lock
   31           the_wake_lock = the_power_manager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, getClass().getName());
   32       } // onCreate
   33   
   34       @Override
   35       protected void onResume() {
   36           super.onResume();
   37   
   38           /* When the activity is resumed, we acquire a wake-lock so that the
   39            * screen stays on, since the user will likely not be fiddling with the
   40            * screen or buttons.
   41            */
   42           the_wake_lock.acquire();
   43   
   44           // Start the simulation
   45           the_view.resume();
   46       }
   47   
   48       @Override
   49       protected void onPause() {
   50           super.onPause();
   51           the_view.pause();
   52           the_wake_lock.release();
   53       }
   54   
   55       static final private int MENU_LEFT = Menu.FIRST;
   56       static final private int MENU_RIGHT = Menu.FIRST + 1;
   57       static final private int MENU_START = Menu.FIRST + 2;
   58       static final private int MENU_PAUSE = Menu.FIRST + 3;
   59       static final private int MENU_QUIT = Menu.FIRST + 4;
   60       static final private int MENU_HELP = Menu.FIRST + 5;
   61   
   62       MenuItem item_left;
   63       MenuItem item_right;
   64       MenuItem item_start;
   65       MenuItem item_pause;
   66       MenuItem item_quit;
   67       MenuItem item_help;
   68   
   69       @Override
   70       public boolean onCreateOptionsMenu(Menu menu) {
   71           item_left = menu.add(Menu.NONE, MENU_LEFT, Menu.NONE, R.string.menutext_left);
   72           item_right = menu.add(Menu.NONE, MENU_RIGHT, Menu.NONE, R.string.menutext_right);
   73           item_start = menu.add(Menu.NONE, MENU_START, Menu.NONE, R.string.menutext_start);
   74           item_pause = menu.add(Menu.NONE, MENU_PAUSE, Menu.NONE, R.string.menutext_pause);
   75           item_quit = menu.add(Menu.NONE, MENU_QUIT, Menu.NONE, R.string.menutext_quit);
   76           item_help = menu.add(Menu.NONE, MENU_HELP, Menu.NONE, R.string.menutext_help);
   77           return true;
   78       }
   79   
   80       public void disable_corner_commands() {
   81           item_left.setEnabled(false);
   82           item_right.setEnabled(false);
   83       }
   84       
   85       @Override
   86       public boolean onOptionsItemSelected(MenuItem item) {
   87           // TODO Auto-generated method stub
   88           // return super.onOptionsItemSelected(item);
   89   
   90           int menyvalet = item.getItemId();
   91           switch (menyvalet) {
   92           case MENU_LEFT:
   93               // showWarning("Can't set left corner yet.", this);
   94               the_view.mark_left_corner();
   95               break;
   96           case MENU_RIGHT:
   97               // showWarning("Can't set right corner yet.", this);
   98               the_view.mark_right_corner();
   99               break;
  100           case MENU_START:
  101               the_view.start_moving();
  102               break;
  103           case MENU_PAUSE:
  104               the_view.stop_moving();
  105               break;
  106           case MENU_QUIT:
  107               askQuit();
  108               break;
  109           case MENU_HELP:
  110               Intent the_help_intent = new Intent(RushActivity.this, HelpActivity.class);
  111               startActivity(the_help_intent);
  112               break;
  113           default:
  114               showWarning("This can't happen. How strange.", this);
  115               break;
  116           } // switch
  117           return true;
  118       } // onOptionsItemSelected
  119       
  120       public static void showWarning(String message, Context c) {
  121           AlertDialog.Builder builder = new AlertDialog.Builder(c);
  122           builder.setTitle("Hi there!");
  123           builder.setMessage(message);
  124   
  125           builder.setNeutralButton("Ok", null);
  126           builder.show();
  127       } // showWarning
  128   
  129       private void askQuit() {
  130           final Activity the_activity = this;
  131           final AlertDialog.Builder builder = new AlertDialog.Builder(this);
  132           builder.setMessage("Really quit?");
  133   
  134           DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
  135               public void onClick(DialogInterface dialog, int which) {
  136                   switch (which){
  137                   case DialogInterface.BUTTON_POSITIVE:
  138                       // "Yes" button clicked
  139                       the_activity.finish();
  140                       break;
  141   
  142                   case DialogInterface.BUTTON_NEGATIVE:
  143                       // "No" button clicked
  144                       break;
  145                   }
  146               } // OnClickListener
  147           };
  148   
  149           builder.setPositiveButton("Yes, quit", listener);
  150           builder.setNegativeButton("No", listener);
  151           builder.show();
  152       } // askQuit
  153       
  154   } // class RushActivity

"Bild" 19: Koden för vyn

RushView.java

    1   package se.nekotronic.satelliterush;
    2   
    3   import java.util.ArrayList;
    4   import java.util.Random;
    5   import java.util.Timer;
    6   import java.util.TimerTask;
    7   
    8   import android.app.Activity;
    9   import android.content.Context;
   10   import android.graphics.Canvas;
   11   import android.graphics.Color;
   12   import android.graphics.Paint;
   13   import android.graphics.Paint.Style;
   14   import android.location.Location;
   15   import android.location.LocationListener;
   16   import android.location.LocationManager;
   17   import android.location.LocationProvider;
   18   import android.os.Bundle;
   19   import android.text.format.Time;
   20   import android.util.AttributeSet;
   21   import android.util.DisplayMetrics;
   22   import android.util.Log;
   23   import android.view.Display;
   24   import android.view.View;
   25   import android.view.WindowManager;
   26   
   27   public class RushView extends View {
   28   
   29       // Ok, these bricks are round, but you get what I mean. Positions are in
   30       // pixels.
   31       class Brick {
   32           public float x, y;
   33           public float radius;
   34           public int color;
   35           public boolean active = true;
   36   
   37           public Brick(float x, float y, float radius, int color) {
   38               this.x = x;
   39               this.y = y;
   40               this.radius = radius;
   41               this.color = color;
   42           }
   43       } // class Brick
   44   
   45       // A ball is a moving brick! Speeds are measured in pixels per second.
   46       class Ball extends Brick {
   47           public float dx, dy;
   48   
   49           public Ball(float x, float y, float dx, float dy, float radius,
   50                   int color) {
   51               super(x, y, radius, color);
   52               this.dx = dx;
   53               this.dy = dy;
   54           }
   55       } // class Ball
   56   
   57       class Paddle {
   58           // The "x_quotient" is the distance from the left corner to the current position,
   59           // when the current position is projected on the base line.
   60           // Measured in "baseline lengths". Not pixels. Not actual ground meters.
   61           public float x_quotient;
   62           public float width;
   63           public float thickness;
   64           public int color;
   65   
   66           public Paddle(float x_quotient, float width, float thickness, int color) {
   67               this.x_quotient = x_quotient;
   68               this.width = width;
   69               this.thickness = thickness;
   70               this.color = color;
   71           }
   72       } // class Ball
   73   
   74       private ArrayList<Brick> bricks = new ArrayList<Brick>();
   75       private Ball ball;
   76       private Paddle paddle;
   77       private Timer ticker;
   78       private Random random;
   79       private LocationManager location_manager;
   80       private LocationListener location_listener;
   81       private boolean locations_enabled = true;
   82       private RushActivity context;
   83   
   84       private void init(final Context context) {
   85           if (context instanceof RushActivity)
   86               this.context = (RushActivity) context;
   87           random = new Random();
   88           location_manager = (LocationManager) context
   89                   .getSystemService(Context.LOCATION_SERVICE);
   90   
   91           // Define a listener that responds to location updates
   92           location_listener = new LocationListener() {
   93   
   94               @Override
   95               public void onLocationChanged(Location location) {
   96                   use_new_location(location);
   97               }
   98   
   99               @Override
  100               public void onProviderDisabled(String provider) {
  101                   if (locations_enabled) {
  102                       locations_enabled = false;
  103                       if (context != null)
  104                           RushActivity.showWarning("GPS has been disabled.",
  105                                   context);
  106                   }
  107               }
  108   
  109               @Override
  110               public void onProviderEnabled(String provider) {
  111                   if (locations_enabled == false) {
  112                       locations_enabled = true;
  113                       if (context != null)
  114                           RushActivity.showWarning(
  115                                   "GPS has been enabled again. Good!", context);
  116                   }
  117               }
  118   
  119               @Override
  120               public void onStatusChanged(String provider, int status,
  121                       Bundle extras) {
  122                   if (status == LocationProvider.OUT_OF_SERVICE) {
  123                       if (locations_enabled) {
  124                           locations_enabled = false;
  125                           if (context != null)
  126                               RushActivity.showWarning("GPS is out of service.",
  127                                       context);
  128                       }
  129                   } else if (status == LocationProvider.TEMPORARILY_UNAVAILABLE) {
  130                       if (locations_enabled) {
  131                           locations_enabled = false;
  132                           if (context != null)
  133                               RushActivity.showWarning(
  134                                       "GPS is temporarily unavailable.", context);
  135                       }
  136                   } else if (status == LocationProvider.AVAILABLE) {
  137                       if (locations_enabled == false) {
  138                           locations_enabled = true;
  139                           if (context != null)
  140                               RushActivity.showWarning(
  141                                       "GPS is available again. Good!", context);
  142                       }
  143                   }
  144   
  145               }
  146           };
  147           // Log.d("SatelliteRush", "init done");
  148       } // init
  149   
  150       private Location latest_location;
  151       private Time latest_location_time;
  152       private Location left_corner;
  153       private Location right_corner;
  154       private Time left_corner_time;
  155       private Time right_corner_time;
  156       private Time game_start_time;
  157       private Time game_win_time;
  158       private float game_win_seconds;
  159       private float base_line_length; // actual ground distance in meters
  160       private float base_speed = -1; // screen speed in pixels per second, calculated from an actual ground speed
  161   
  162       // This should be carefully adjusted 
  163       // A BASE_SPEED_FACTOR of 1 lets the ball move across a screen diagonal in the same time it took the player to walk the baseline
  164       public static float BASE_SPEED_FACTOR = 2.0f;
  165       // How much the ball speeds up after popping a brick
  166       public static float BALL_SPEED_UP_FACTOR = 1.03f;
  167       
  168       private void calculate_base_speed() {
  169           if (base_speed == -1) {
  170               // The base speed is only calculated once,
  171               // and depends on the player's speed when walking the base line
  172               // (actually: the time between the setting of the two corners)
  173               float walk_seconds = Math.abs(left_corner_time.toMillis(true) - right_corner_time.toMillis(true)) / 1000f;
  174               base_speed = (float)Math.sqrt(width_pixels * width_pixels + height_pixels * height_pixels) / walk_seconds * BASE_SPEED_FACTOR;
  175               ball.dx = base_speed / 1.4142f;
  176               ball.dy = - base_speed / 1.4142f;
  177           }
  178       }
  179       
  180       private void use_new_location(Location location) {
  181           latest_location = location;
  182           Time now = new Time();
  183           now.setToNow();
  184           latest_location_time = now;
  185   
  186           if (status == Status.WAITING_FOR_LEFT_CORNER) {
  187               left_corner = location;
  188               left_corner_time = now;
  189               if (location.getAccuracy() > 10) {
  190                   corner_wait_status = CornerWaitStatus.NOT_ENOUGH_ACCURACY;
  191               }
  192               else if (right_corner == null) {
  193                   status = Status.NOT_READY;
  194               }
  195               else {
  196                   base_line_length = left_corner.distanceTo(right_corner);
  197                   if (base_line_length < 50) {
  198                       corner_wait_status = CornerWaitStatus.BASE_LINE_TOO_SHORT;
  199                   }
  200                   else {
  201                       status = Status.READY;
  202                       calculate_base_speed();
  203                   }
  204               }
  205           }
  206   
  207           if (status == Status.WAITING_FOR_RIGHT_CORNER) {
  208               right_corner = location;
  209               right_corner_time = now;
  210               if (location.getAccuracy() > 10) {
  211                   corner_wait_status = CornerWaitStatus.NOT_ENOUGH_ACCURACY;
  212               }
  213               else if (left_corner == null) {
  214                   status = Status.NOT_READY;
  215               }
  216               else {
  217                   base_line_length = right_corner.distanceTo(left_corner);
  218                   if (base_line_length < 50) {
  219                       corner_wait_status = CornerWaitStatus.BASE_LINE_TOO_SHORT;
  220                   }
  221                   else {
  222                       status = Status.READY;
  223                       calculate_base_speed();
  224                   }
  225               }
  226           }
  227   
  228           if (left_corner != null && right_corner != null) {
  229               float distance_from_left = left_corner.distanceTo(latest_location);         
  230               float distance_from_right = right_corner.distanceTo(latest_location);
  231   
  232               // This assumes that we are on (or at least close to) the base line
  233               // paddle.x = distance_from_left / (distance_from_left + distance_from_right) * width_pixels;
  234   
  235               // This projects the player's position onto the base line
  236               float baseline_meters_from_left =
  237                   ((distance_from_left * distance_from_left) - (distance_from_right * distance_from_right) + (base_line_length * base_line_length))
  238                       / (2 * base_line_length);
  239               if (baseline_meters_from_left < 0)
  240                   baseline_meters_from_left = 0;
  241               else if (baseline_meters_from_left > base_line_length)
  242                   baseline_meters_from_left = base_line_length;
  243               paddle.x_quotient =  baseline_meters_from_left / base_line_length;
  244           }
  245   
  246           /*
  247            * updateText(); double longitude = location.getLongitude(); double
  248            * latitude = location.getLatitude(); double accuracy =
  249            * location.getAccuracy(); double altitude= location.getAltitude();
  250            * print("Long " + longitude + ", lat " + latitude + ", accuracy " +
  251            * accuracy + ", alt " + altitude);
  252            */
  253           
  254           // The things to show on the screen may have changed now
  255           invalidate_view();
  256       } // use_new_location
  257   
  258       public RushView(Context context) {
  259           super(context);
  260           init(context);
  261       }
  262   
  263       public RushView(Context context, AttributeSet attrs) {
  264           super(context, attrs);
  265           init(context);
  266       }
  267   
  268       public RushView(Context context, AttributeSet attrs, int defStyle) {
  269           super(context, attrs, defStyle);
  270           init(context);
  271       }
  272   
  273       private int width_pixels = -1;
  274       private int height_pixels = -1;
  275       private float screen_density = -1;
  276       private boolean bricks_initialized = false;
  277   
  278       // This can be called not just from Android, but from the Graphical Layout
  279       // in Eclipse
  280       @Override
  281       protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  282           width_pixels = w;
  283           height_pixels = h;
  284           // screen_density = 1f;
  285           Context context = getContext();
  286           if (context != null && context instanceof Activity) {
  287               Activity parent = (Activity) getContext();
  288               // parent.getWindowManager().getDefaultDisplay().getMetrics(dm);
  289               DisplayMetrics dm = new DisplayMetrics();
  290               WindowManager window_manager = parent.getWindowManager();
  291               if (dm != null && window_manager != null) {
  292                   // Crashed with NullPointerException when in the Graphical
  293                   // Layout in Eclipse
  294                   Display display = window_manager.getDefaultDisplay();
  295                   display.getMetrics(dm);
  296                   screen_density = dm.density;
  297               }
  298           }
  299   
  300           if (!bricks_initialized) {
  301               float d = screen_density;
  302               if (d == -1)
  303                   d = 1f;
  304               float standard_brick_size = Math.min(w, h) / 10f * d;
  305               int nr_columns = (int) (w / standard_brick_size);
  306               float column_width = w / nr_columns;
  307               int nr_rows = (int) (h / standard_brick_size); // Rows of the entire
  308                                                               // screen!
  309               float row_height = h / nr_rows;
  310               float current_y_center = row_height / 2;
  311               for (int y = 0; y < nr_rows / 2; ++y) { // Divide by 2: Only use the
  312                                                       // upper half of the screen
  313                   float current_x_center = column_width / 2;
  314                   for (int x = 0; x < nr_columns; ++x) {
  315                       Brick brick = new Brick(current_x_center, current_y_center,
  316                               standard_brick_size * 0.4f, 0xff00ff00);
  317                       bricks.add(brick);
  318                       current_x_center += column_width;
  319                   }
  320                   current_y_center += row_height;
  321               }
  322               // Log.d("SatelliteRush", "h = " + h);
  323               ball = new Ball(w * 0.5f, h * 0.60f, 1, 1,
  324                       standard_brick_size * 0.3f, 0xff0000ff);
  325               // paddle = new Paddle(0.5f, width_pixels / 1f, 10 * d, 0xff0000ff);
  326               paddle = new Paddle(0.5f, width_pixels / 5f, 10 * d, 0xff0000ff);
  327               bricks_initialized = true;
  328           }
  329       }
  330   
  331       // This can be called not just from Android, but from the Graphical Layout
  332       // in Eclipse
  333       // We need to synchronize since we read the same data that update_simulation
  334       // updates
  335       @Override
  336       synchronized protected void onDraw(Canvas canvas) {
  337           super.onDraw(canvas);
  338   
  339           int h = height_pixels;
  340           if (h == -1)
  341               h = canvas.getHeight();
  342           int w = width_pixels;
  343           if (w == -1)
  344               w = canvas.getWidth();
  345           float d = screen_density;
  346           if (d == -1)
  347               d = 1f;
  348           
  349           Paint p = new Paint();
  350   
  351           for (Brick b : bricks) {
  352               if (b.active) {
  353                   p.setColor(b.color);
  354                   canvas.drawCircle(b.x, b.y, b.radius, p);
  355               }
  356           }
  357   
  358           p.setStyle(Style.STROKE);
  359           for (Brick b : bricks) {
  360               if (b.active) {
  361                   p.setColor(Color.WHITE);
  362                   canvas.drawCircle(b.x, b.y, b.radius, p);
  363               }
  364           }
  365           p.setStyle(Style.FILL);
  366   
  367           if (status != Status.YOU_WIN) {
  368               // Draw a line that shows the ball's direction
  369               p.setColor(Color.RED);
  370               // I don't remember how I found, or invented, this calculation, and I am rather suspicious of it. It seems to work, though.
  371               float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
  372               float screen_diagonal = (float)Math.sqrt(width_pixels * width_pixels + height_pixels * height_pixels);
  373               float speed_scale = screen_diagonal / ball_speed;
  374               canvas.drawLine(ball.x, ball.y, ball.x + ball.dx * speed_scale, ball.y + ball.dy * speed_scale, p);
  375           }
  376           
  377           p.setColor(ball.color);
  378           canvas.drawCircle(ball.x, ball.y, ball.radius, p);
  379           p.setColor(Color.WHITE);
  380           p.setStyle(Style.STROKE);
  381           canvas.drawCircle(ball.x, ball.y, ball.radius, p);
  382           p.setStyle(Style.FILL);
  383           
  384           float usable_base_pixels = width_pixels - paddle.width;
  385           float paddle_left = paddle.x_quotient * usable_base_pixels;
  386           float paddle_right = paddle.x_quotient * usable_base_pixels + paddle.width;
  387   
  388           p.setColor(paddle.color);
  389           canvas.drawRect(paddle_left, h - paddle.thickness, paddle_right, h, p);
  390           p.setColor(Color.WHITE);
  391           p.setStyle(Style.STROKE);
  392           canvas.drawRect(paddle_left, h - paddle.thickness, paddle_right, h, p);
  393           p.setStyle(Style.FILL);
  394   
  395           /*
  396           p.setColor(Color.WHITE);
  397           canvas.drawText("Satellite Rush: status = " + status,
  398                   20f, h / 2 + 20f, p);               
  399           canvas.drawText("h = " + h + " (" + height_pixels + ")" +
  400                   ", w = " + w + " (" + width_pixels + ")" +
  401                   ", d = " + d + " (" + screen_density + ")",
  402                   20f, h / 2 + 40f, p);               
  403           canvas.drawText("Ball: x = " + String.format("%.2f", ball.x) +
  404                   ", y = " + String.format("%.2f", ball.y) +
  405                   ", dx = " + String.format("%.2f", ball.dx) +
  406                   ", dy = " + String.format("%.2f", ball.dy),
  407                   20f, h / 2 + 60f, p);
  408           canvas.drawText("Speed = " + String.format("%.2f", Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy)) +
  409                   ", base_speed = " + String.format("%.2f", base_speed) +
  410                   " (" + String.format("%.2f", Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy) / base_speed) + ")",
  411                   20f, h / 2 + 80f, p);
  412   
  413           final long now_nanos = java.lang.System.nanoTime(); 
  414           if (previous_draw_nanos != -1 && now_nanos > previous_draw_nanos) {
  415               long nanos = now_nanos - previous_draw_nanos; 
  416               float seconds = nanos / 1e9f;
  417               float frame_rate = 1 / seconds; 
  418               if (accumulated_frame_rate == -1)
  419                   accumulated_frame_rate = frame_rate;
  420               else
  421                   accumulated_frame_rate = (0.99f * accumulated_frame_rate + 0.01f * frame_rate);
  422               canvas.drawText("frame rate = " + (int)(accumulated_frame_rate + 0.5), 
  423                       20f, h / 2 + 100f, p);
  424           }
  425           previous_draw_nanos = now_nanos;
  426           
  427           canvas.drawText("paddle.x_quotient = " + String.format("%.2f", paddle.x_quotient),
  428                   20f, h / 2 + 120f, p);
  429           */
  430           
  431           if (status == Status.YOU_WIN) {
  432               p.setColor(Color.GREEN);
  433               p.setTextSize(40f * d);
  434               canvas.drawText("YOU WIN!", 0, height_pixels * 0.25f, p);
  435               p.setTextSize(40f * d);
  436               int hours = (int)(game_win_seconds / 3600);
  437               int minutes = (int)(game_win_seconds % 3600 / 60);
  438               float seconds = game_win_seconds % 60;
  439               canvas.drawText("Time: " + hours + ":" + String.format("%02d", minutes) + ":" + String.format("%05.2f", seconds),
  440                       0, height_pixels * 0.50f, p);
  441           }
  442           
  443           if (this.locations_enabled == false) {
  444               p.setColor(Color.RED);
  445               p.setTextSize(20f * d);
  446               canvas.drawText("You need to enable GPS.", 0, height_pixels * 0.75f, p);
  447           }
  448           else if (status == Status.WAITING_FOR_LEFT_CORNER) {
  449               p.setColor(Color.RED);
  450               p.setTextSize(20f * d);
  451               canvas.drawText("Setting left corner. Wait...", 0, height_pixels * 0.75f, p);
  452               if (corner_wait_status == CornerWaitStatus.NOT_ENOUGH_ACCURACY) {
  453                   canvas.drawText("(Waiting for better accuracy than " + String.format("%.2f", latest_location.getAccuracy()) + " m.)",
  454                                   0, height_pixels * 0.75f + 25f * d, p);
  455               }
  456               else if (corner_wait_status == CornerWaitStatus.BASE_LINE_TOO_SHORT) {
  457                   canvas.drawText("Base line too short (" + (int)(base_line_length + 0.5) + " m).", 0, height_pixels * 0.75f + 25f * d, p);
  458                   canvas.drawText("Keep walking!", 0, height_pixels * 0.75f + 50f * d, p);
  459                   }
  460           }
  461           else if (status == Status.WAITING_FOR_RIGHT_CORNER) {
  462               p.setColor(Color.RED);
  463               p.setTextSize(20f * d);
  464               canvas.drawText("Setting right corner. Wait...", 0, height_pixels * 0.75f, p);
  465               if (corner_wait_status == CornerWaitStatus.NOT_ENOUGH_ACCURACY) {
  466                   canvas.drawText("(Waiting for better accuracy than " + String.format("%.2f", latest_location.getAccuracy()) + " m.)",
  467                                   0, height_pixels * 0.75f + 25f * d, p);
  468               }
  469               else if (corner_wait_status == CornerWaitStatus.BASE_LINE_TOO_SHORT) {
  470                   canvas.drawText("Base line too short (" + (int)(base_line_length + 0.5) + " m).", 0, height_pixels * 0.75f + 25f * d, p);
  471                   canvas.drawText("Keep walking!", 0, height_pixels * 0.75f + 50f * d, p);
  472               }
  473           }
  474           else if (left_corner == null && right_corner == null) {
  475               p.setColor(Color.RED);
  476               p.setTextSize(20f * d);
  477               canvas.drawText("Welcome to Satellite Rush!", 0, height_pixels * 0.75f, p);
  478               canvas.drawText("First you must set the corners.", 0, height_pixels * 0.75f + 25f * d, p);
  479               canvas.drawText("Go there, and use the commands.", 0, height_pixels * 0.75f + 50f * d, p);
  480           }
  481           else if (left_corner == null) {
  482               p.setColor(Color.RED);
  483               p.setTextSize(20f * d);
  484               canvas.drawText("Now set the left corner.", 0, height_pixels * 0.75f, p);
  485           }
  486           else if (right_corner == null) {
  487               p.setColor(Color.RED);
  488               p.setTextSize(20f * d);
  489               canvas.drawText("Now set the right corner.", 0, height_pixels * 0.75f, p);
  490           }
  491           else if (status == status.READY) {
  492               p.setColor(Color.GREEN);
  493               p.setTextSize(20f * d);
  494               canvas.drawText("Ready to start the game!", 0, height_pixels * 0.75f, p);
  495           }
  496       } // onDraw
  497   
  498       private long previous_draw_nanos = -1;
  499       private float accumulated_frame_rate = -1;
  500   
  501       enum Status {
  502           NOT_READY, // Not started yet
  503           READY, // Will not continue until user commands it
  504           PAUSED, // Will continue as soon as visible
  505           RUNNING,
  506           WAITING_FOR_LEFT_CORNER,
  507           WAITING_FOR_RIGHT_CORNER,
  508           YOU_WIN
  509       }
  510       
  511       private Status status = Status.NOT_READY;
  512   
  513       enum CornerWaitStatus {
  514           NO_LOCATION_YET,
  515           NOT_ENOUGH_ACCURACY,
  516           BASE_LINE_TOO_SHORT
  517       }
  518   
  519       private CornerWaitStatus corner_wait_status = CornerWaitStatus.NO_LOCATION_YET;
  520   
  521       // Called when the player wants to begin (or continue) the game
  522       public void start_moving() {
  523           if (status == Status.READY || status == status.PAUSED) { // Should never be PAUSED
  524               if (game_start_time == null) {
  525                   Time now = new Time();
  526                   now.setToNow();
  527                   game_start_time = now;
  528               }
  529               status = Status.RUNNING;
  530               context.disable_corner_commands();
  531               start_ticker();
  532           }
  533           else if (status == Status.YOU_WIN) {
  534               RushActivity.showWarning("You already won.", context);
  535           }
  536           else {
  537               RushActivity.showWarning("No no no! Set the corners first.", context);
  538           }
  539           invalidate_view();
  540       }
  541   
  542       // Called when the player wants to pause the game
  543       public void stop_moving() {
  544           if (status == Status.RUNNING) {
  545               status = Status.READY;
  546               stop_ticker();
  547           }
  548           invalidate_view();
  549       }
  550   
  551       // Called when the system will show the screen (possibly for the first time)
  552       public void resume() {
  553           try {
  554               // Register the listener with the Location Manager to receive
  555               // location updates
  556               location_manager.requestLocationUpdates(
  557                       LocationManager.GPS_PROVIDER, 0, 0, location_listener);
  558           }
  559           catch (Exception e) {
  560               RushActivity.showWarning("Couldn't use the GPS: " + e.getMessage(), context);
  561           }
  562   
  563           if (status == Status.PAUSED) {
  564               status = Status.RUNNING;
  565               start_ticker();
  566           }
  567   
  568           invalidate_view();
  569       }
  570   
  571       // Called when the system (not the player) will hide the screen or otherwise pause the game
  572       public void pause() {
  573           stop_ticker();  
  574           if (status == Status.RUNNING) {
  575               status = Status.READY;
  576           }
  577           location_manager.removeUpdates(location_listener);
  578           // invalidate_view(); -- No!
  579       }
  580   
  581       private void start_ticker() {
  582           if (ticker != null)
  583               return;
  584           ticker = new Timer();
  585           TimerTask task = new TimerTask() {
  586               @Override
  587               public void run() {
  588                   update_simulation();
  589                   invalidate_view();
  590               }
  591           };
  592           ticker.schedule(task, 100, 50); // Needs to wait?
  593           // ticker.scheduleAtFixedRate(task, 100, 100);
  594       }
  595   
  596       private void invalidate_view() {
  597           Runnable r = new Runnable() {
  598               @Override
  599               public void run() {
  600                   invalidate();
  601               }
  602           };
  603           if (getHandler() != null)
  604               getHandler().post(r);
  605       }
  606   
  607       private void stop_ticker() {
  608           if (ticker != null) {
  609               ticker.cancel();
  610               ticker = null;
  611               nanos_when_paused = java.lang.System.nanoTime();
  612           }
  613       }
  614   
  615       // -1 is not guaranteed to never happen, but we ignore that
  616       private long previous_nanos = -1;
  617       private long nanos_when_paused = -1;
  618   
  619       // We need to synchronize since we update the same data that onDraw reads
  620       synchronized private void update_simulation() {
  621           long now_nanos = java.lang.System.nanoTime();
  622           if (previous_nanos == -1 || now_nanos < previous_nanos) {
  623               // First time, or overflow, so don't update the game
  624               previous_nanos = now_nanos;
  625               return;
  626           }
  627   
  628           long nanos = now_nanos - previous_nanos;
  629           if (nanos_when_paused != -1) {
  630               // We have been paused!
  631               nanos = nanos_when_paused - previous_nanos;
  632               nanos_when_paused = -1;
  633           }
  634   
  635           previous_nanos = now_nanos;
  636           double seconds = nanos / 1e9;
  637   
  638           ball.x += ball.dx * seconds;
  639           ball.y += ball.dy * seconds;
  640   
  641           // Check for collisions and handle them
  642           Brick tragic_victim = null;
  643           for (Brick brick : bricks) {
  644               // To avoid some floating-point calculations, check a square first.
  645               if (brick.active 
  646                       && Math.abs(brick.x - ball.x) <= brick.radius + ball.radius + 1
  647                       && Math.abs(brick.y - ball.y) <= brick.radius + ball.radius
  648                               + 1
  649                       && Math.sqrt((brick.x - ball.x) * (brick.x - ball.x)
  650                               + (brick.y - ball.y) * (brick.y - ball.y)) < brick.radius
  651                               + ball.radius) {
  652   
  653                   // Log.d("Satellite Rush", "Popped a brick!");
  654   
  655                   double current_heading = Math.atan(ball.dy / ball.dx);
  656                   if (ball.dx < 0)
  657                       current_heading += Math.PI;
  658   
  659                   double normal = Math.atan((ball.y - brick.y) / (ball.x - brick.x));
  660                   if ((ball.x - brick.x) < 0)
  661                       normal += Math.PI;
  662   
  663                   float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
  664                   // Bounce back!
  665                   double new_heading = 2 * normal - current_heading - Math.PI;
  666                   ball.dx = ball_speed * (float)Math.cos(new_heading);
  667                   ball.dy = ball_speed * (float)Math.sin(new_heading);
  668   
  669                   // The ball speeds up slightly after each popped brick
  670                   ball.dx *= BALL_SPEED_UP_FACTOR;
  671                   ball.dy *= BALL_SPEED_UP_FACTOR;
  672                   
  673                   // unbore(); -- Done at the end of this simulation step
  674   
  675                   /*
  676                   // If a speed component (x or y) is less than 1% of the screen
  677                   // size, increase it a bit
  678                   float dx_limit = width_pixels / 100f;
  679                   float dy_limit = height_pixels / 100f;
  680                   if (Math.abs(ball.dx) < dx_limit)
  681                       ball.dx = (dx_limit + random.nextFloat())
  682                               * Math.signum(ball.dx);
  683                   if (Math.abs(ball.dy) < dy_limit)
  684                       ball.dy = (dy_limit + random.nextFloat())
  685                               * Math.signum(ball.dy);
  686                   */
  687                   
  688                   tragic_victim = brick;
  689                   break;
  690               } // if the ball hit a brick
  691           } // for each brick
  692   
  693           if (tragic_victim != null) {
  694               tragic_victim.active = false;
  695               boolean any_bricks_left = false;
  696               for (Brick brick : bricks) {
  697                   if (brick.active) {
  698                       any_bricks_left = true;
  699                       break;
  700                   }
  701               }
  702               if (any_bricks_left == false) {
  703                   Time now = new Time();
  704                   now.setToNow();
  705                   game_win_time = now;
  706                   game_win_seconds = (game_win_time.toMillis(true) - game_start_time.toMillis(true)) / 1000.0f;
  707                   status = Status.YOU_WIN;
  708                   stop_ticker();
  709               }
  710           }
  711   
  712           // Bounce against the walls, with a small random adjustment to the speed
  713           if (ball.x - ball.radius < 0) {
  714               ball.x = ball.radius + (ball.radius - ball.x);
  715               ball.dx = -ball.dx * (99 + 2 * random.nextFloat()) / 100f;
  716           }
  717           if (ball.x + ball.radius > width_pixels) {
  718               // ball.x = width_pixels - (ball.radius + ball.x + ball.radius -
  719               // width_pixels);
  720               ball.x = 2 * width_pixels - 2 * ball.radius - ball.x;
  721               ball.dx = -ball.dx * (99 + 2 * random.nextFloat()) / 100f;
  722           }
  723           if (ball.y - ball.radius < 0) {
  724               ball.y = ball.radius + (ball.radius - ball.y);
  725               ball.dy = -ball.dy * (99 + 2 * random.nextFloat()) / 100f;
  726           }
  727   
  728           if (ball.dy > 0 && ball.y + ball.radius > height_pixels - paddle.thickness) {
  729               float usable_base_pixels = width_pixels - paddle.width;
  730               float paddle_left = paddle.x_quotient * usable_base_pixels;
  731               float paddle_right = paddle.x_quotient * usable_base_pixels + paddle.width;
  732               if (ball.x > paddle_left && ball.x < paddle_right) {
  733                   ball.y = 2 * height_pixels - 2 * paddle.thickness - 2 * ball.radius - ball.y;
  734                   ball.dy = -ball.dy * (99 + 2 * random.nextFloat()) / 100f;
  735               }
  736           }
  737           if (ball.y + ball.radius > height_pixels) {
  738               ball.y = 2 * height_pixels - 2 * ball.radius - ball.y;
  739               ball.dy = -ball.dy * (99 + 2 * random.nextFloat()) / 100f;
  740               add_a_ball();
  741               ball.dx /= (BALL_SPEED_UP_FACTOR * BALL_SPEED_UP_FACTOR);
  742               ball.dy /= (BALL_SPEED_UP_FACTOR * BALL_SPEED_UP_FACTOR);
  743           }
  744           
  745           unbore();
  746       } // update_simulation
  747       
  748       public float MIN_HORIZONTAL_FRACTION = 0.10f;
  749       public float MIN_VERTICAL_FRACTION = 0.20f;
  750   
  751       // If the ball movement is boring (almost entirely vertical or horizontal), then make it less boring
  752       void unbore() {
  753           if (ball.dx != 0 && Math.abs(ball.dy / ball.dx) < MIN_VERTICAL_FRACTION) {
  754               float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
  755               ball.dx = ball_speed * MIN_VERTICAL_FRACTION;
  756               ball.dy = (float)Math.sqrt(ball_speed * ball_speed - ball.dx * ball.dx) * Math.signum(ball.dy);
  757           }
  758           else if (ball.dy != 0 && Math.abs(ball.dx / ball.dy) < MIN_HORIZONTAL_FRACTION) {
  759               float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
  760               ball.dy = ball_speed * MIN_HORIZONTAL_FRACTION;
  761               ball.dx = (float)Math.sqrt(ball_speed * ball_speed - ball.dy * ball.dy) * Math.signum(ball.dx);
  762           }
  763       } // unbore
  764       
  765       void add_a_ball() {
  766           for (Brick brick : bricks) {
  767               if (!brick.active) {
  768                   brick.color = Color.RED;
  769                   brick.active = true;
  770                   break;
  771               }
  772           }
  773       }
  774       
  775       public void mark_left_corner() {
  776           if (left_corner != null)
  777               RushActivity.showWarning("Setting new left corner.", context);
  778           status = Status.WAITING_FOR_LEFT_CORNER;
  779           corner_wait_status = CornerWaitStatus.NO_LOCATION_YET;
  780           invalidate_view();
  781       }
  782   
  783       public void mark_right_corner() {
  784           if (right_corner != null)
  785               RushActivity.showWarning("Setting new right corner.", context);
  786           status = Status.WAITING_FOR_RIGHT_CORNER;
  787           corner_wait_status = CornerWaitStatus.NO_LOCATION_YET;
  788           invalidate_view();
  789       }
  790   
  791   } // class RushView

Övningar

Tillbaka till lektionslistan


Thomas Padron-McCarthy (thomas.padron-mccarthy@oru.se), 26 september 2011