Tap or drag on the screen to move the paddle. The ball will move by itself, bouncing off the paddle and the edges of the screen. The app launches and remains in landscape orientation. On Mac, type fn-control-F12 to rotate the Android emulator to landscape orientation.
The infinite loop
(the
for (;;) {
)
repositions the ball 60 time a second.
It is our first piece of code that is executed by a
thread
other than the main thread (a.k.a. the UI thread).
Thus far we have been doing everything in the UI thread,
which has always been our only thread.
But the infinite loop would prevent the UI thread from doing its other
jobs,
such as detecting touches.
(For starters,
an infinite loop in the
View
’s
constructor
would prevent us from getting to the end
of the constructor,
and would therefore prevent the other
lifecycle
methods
from executing.)
We therefore execute the infinite loop in a separate thread.
Code in the UI thread is allowed to call the
invalidate
method of a
View
.
Code in another thread must call the
postInvalidate
method of the
View
.
See
Communicating
with
the UI Thread.
Having more than one thread
increases the number of places where we can write code.
Until now, we have been able to write code in only one place:
in the lifecycle methods of the
Activity
object
(onCreate
,
onStart
,
etc.)
and in the methods called by them.
Now we can have code than runs as long as we
want—even forever—in the parallel universe of another thread.
MainActivity.java
:
onCreate
creates a
PongView
.
PongView.java
AndroidManifest.xml
.
The
<activity>
element has the attribute
android:screenOrientation="landscape"
.
build.gradle
(Module: app).
See
Threads.
In the
PongView
constructor,
change
Thread thread = new Thread(runnable); //a thread that is not the UI thread thread.start(); //The thread will execute the run method of the Runnable object.to
Thread thread = new Thread(runnable); Log.d("myTag", "id of thread = " + thread.getId()); Log.d("myTag", "name of thread = " + thread.getName()); Log.d("myTag", "state of thread = " + thread.getState()); thread.start(); Log.d("myTag", "state of thread = " + thread.getState());
id of thread = 81 name of thread = Thread-81 state of thread = NEW state of thread = WAITING
The
ps
command (“process status”)
lists all the processes and threads that exist.
adb devices List of devices attached ca1784a34445a8d0308 device 192.168.57.101:5555 device emulator-5554 device adb -s 192.168.57.101:5555 shell ps USER PID PPID VSIZE RSS WCHAN PC NAME root 1 0 720 440 c021a64f 0805d2c6 S /init root 272 1 552120 57524 ffffffff b7653770 S zygote u0_a67 1354 272 595168 46708 ffffffff b76549f3 S edu.nyu.scps.pong root 87 1 28424 456 ffffffff 0806c3a0 S /sbin/adbd root 16841 87 1868 504 00000000 b76a1146 R ps adb -s 192.168.57.101:5555 shell ps -t 1354 USER PID PPID VSIZE RSS WCHAN PC NAME u0_a67 1354 272 595108 45460 ffffffff b76549f3 S edu.nyu.scps.pong u0_a67 1358 1354 595108 45460 c0183a45 b7655399 S GC u0_a67 1359 1354 595108 45460 c014bfe7 b76547cb S Signal Catcher u0_a67 1360 1354 595108 45460 c06c29bd b7654b13 S JDWP u0_a67 1361 1354 595108 45460 c0183a45 b7655399 S Compiler u0_a67 1362 1354 595108 45460 c0183a45 b7655399 S ReferenceQueueD u0_a67 1363 1354 595108 45460 c0183a45 b7655399 S FinalizerDaemon u0_a67 1364 1354 595108 45460 c0183a45 b7655399 S FinalizerWatchd u0_a67 1365 1354 595108 45460 c05963ba b7653586 S Binder_1 u0_a67 1366 1354 595108 45460 c05963ba b7653586 S Binder_2 u0_a67 1369 1354 595108 45460 c0183a45 b7655399 S Thread-81 adb -s 192.168.57.101:5555 shell root@vbox86p:/ # ps -t 1354 root@vbox86p:/ # exit
Another way to see the threads:
Tools → Android → Android Device Monitor
In the Devices tab in the left panel of Android Device Monitor,
select your app
edu.nyu.scps.pong
.
In the toolbar below the tab,
click on the Update Threads icon.
It has three parallel arrows pointing to the right.
In the right panel, select the Threads tab.
If some of the panels seems to be missing,
click on the Maximize icon in the upper right.
If some of the monitor windows seem to be missing, pull down
Window → Reset Perspective…
Do you want to reset the current DDMS perspective to its defaults?
Yes
cd ~/.android pwd ls -ld monitor-workspace
Insert the following code at the start of the
PongView
’s
constructor
(immediately after the
super(context);
)
and at the start of the
run
method
(immediately before the infinite
for
loop).
Thread currentThread = Thread.currentThread(); Log.d("myTag", "id of current thread = " + currentThread.getId()); Log.d("myTag", "name of current thread = " + currentThread.getName());
id of current thread = 1 name of current thread = main id of current thread = 81 name of current thread = Thread-81
dx
and
dy
fields of the
PongView
,
or decrease or increase the number of milliseconds of
sleep
time.
run
method.
Context context = getContext(); WindowManager manager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); Display display = manager.getDefaultDisplay(); float hertz = display.getRefreshRate(); Log.d("myTag", "hertz = " + hertz); long milliseconds = (long)(1000f / hertz);and change the parameter of
sleep
to
milliseconds
.
invalidate
instead of
postInvalidate
,
Android says
“Unfortunately, Pong has stopped.”
What error message do you get in the
logcat
window of Android Studio?
07-08 14:42:34.985 31167-31180/? E/AndroidRuntime: FATAL EXCEPTION: Thread-144
Process: edu.nyu.scps.pong, PID: 1354
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
ToneGenerator
at the start of the
run
method.
ToneGenerator toneGenerator = new ToneGenerator(AudioManager.STREAM_ALARM, ToneGenerator.MAX_VOLUME / 2);After each statement that reverses the sign of
dx
or
dy
,
//proprietary beep, duration in milliseconds toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP, 100);
MainActivity
.
private boolean isResumed = false; //true between onResume and onPause private ToneGenerator toneGenerator = new ToneGenerator(AudioManager.STREAM_ALARM, ToneGenerator.MAX_VOLUME / 2); @Override protected void onResume() { super.onResume(); isResumed = true; } @Override protected void onPause() { super.onPause(); isResumed = false; } public void beep() { if (isResumed) { toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP, 100); } }After each statement that reverses the sign of
dx
or
dy
,
((MainActivity)getContext()).beep();
PongView
.
private RectF paddle2 = new RectF(); //the paddle at the right end of the PongView
//in onLayout method of class PongView paddle2.set(getWidth() - 3 * w, 5 * w, getWidth() - 2 * w, 10 * w);
//in onDraw method of class PongView canvas.drawRect(paddle2, paint);
onTouch
to the following.
Both players can touch simultaneously;
see
Track
Multiple Pointers.
(For some reason, Android calls fingers “pointers”.)
@Override public boolean onTouch(View v, MotionEvent motionEvent) { int n = motionEvent.getPointerCount(); //number of fingers touching right now for (int i = 0; i < n; ++i) { if (motionEvent.getX(i) < getWidth() / 2) { //Finger number i touched left side of PongView. paddle.offsetTo(paddle.left, motionEvent.getY(i) - paddle.height() / 2); } else { //Finger number i touched right side of PongView. paddle2.offsetTo(paddle2.left, motionEvent.getY(i) - paddle2.height() / 2); } } invalidate(); return true; }
run
method after the code for the existing
paddle
.
//If the ball is not currently embedded in paddle2, if (!RectF.intersects(ball, paddle2)) { //but will be embedded on its next move, if (RectF.intersects(horizontal, paddle2)) { dx = -dx; } if (RectF.intersects(vertical, paddle2)) { dy = -dy; } }Instead of having two separate
RectF
s,
paddle
and
paddle2
,
it would be more beautiful to have an array of two
RectF
s.