development
Revision | 5cfbd84e5006b931ff5682a3d6505cbad7b27805 (tree) |
---|---|
Zeit | 2011-02-25 06:22:52 |
Autor | Jeff Brown <jeffbrown@goog...> |
Commiter | Android (Google) Code Review |
Merge "Add a sample to demonstrate game controller usage."
@@ -1961,6 +1961,13 @@ | ||
1961 | 1961 | </intent-filter> |
1962 | 1962 | </activity> |
1963 | 1963 | |
1964 | + <activity android:name=".view.GameControllerInput" android:label="Views/Game Controller Input"> | |
1965 | + <intent-filter> | |
1966 | + <action android:name="android.intent.action.MAIN" /> | |
1967 | + <category android:name="android.intent.category.SAMPLE_CODE" /> | |
1968 | + </intent-filter> | |
1969 | + </activity> | |
1970 | + | |
1964 | 1971 | <!-- ************************************* --> |
1965 | 1972 | <!-- GRAPHICS SAMPLES --> |
1966 | 1973 | <!-- ************************************* --> |
@@ -0,0 +1,52 @@ | ||
1 | +<?xml version="1.0" encoding="utf-8"?> | |
2 | +<!-- Copyright (C) 2011 The Android Open Source Project | |
3 | + | |
4 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + you may not use this file except in compliance with the License. | |
6 | + You may obtain a copy of the License at | |
7 | + | |
8 | + http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + | |
10 | + Unless required by applicable law or agreed to in writing, software | |
11 | + distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + See the License for the specific language governing permissions and | |
14 | + limitations under the License. | |
15 | +--> | |
16 | + | |
17 | +<!-- Game controller input demo. --> | |
18 | +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
19 | + android:orientation="vertical" | |
20 | + android:layout_width="match_parent" | |
21 | + android:layout_height="match_parent"> | |
22 | + | |
23 | + <TextView | |
24 | + android:id="@+id/description" | |
25 | + android:layout_width="match_parent" | |
26 | + android:layout_height="wrap_content" | |
27 | + android:text="@string/game_controller_input_description" | |
28 | + android:padding="12dip" /> | |
29 | + | |
30 | + <LinearLayout | |
31 | + android:orientation="horizontal" | |
32 | + android:layout_width="match_parent" | |
33 | + android:layout_height="0dip" | |
34 | + android:layout_weight="1" | |
35 | + android:padding="12dip"> | |
36 | + <ListView | |
37 | + android:id="@+id/summary" | |
38 | + android:layout_width="0dip" | |
39 | + android:layout_height="match_parent" | |
40 | + android:layout_weight="1" | |
41 | + android:padding="3dip"> | |
42 | + </ListView> | |
43 | + | |
44 | + <com.example.android.apis.view.GameView | |
45 | + android:id="@+id/game" | |
46 | + android:layout_width="0dip" | |
47 | + android:layout_height="match_parent" | |
48 | + android:layout_weight="1" | |
49 | + android:background="#000000" | |
50 | + android:padding="3dip" /> | |
51 | + </LinearLayout> | |
52 | +</LinearLayout> |
@@ -0,0 +1,21 @@ | ||
1 | +<?xml version="1.0" encoding="utf-8"?> | |
2 | +<!-- Copyright (C) 2011 The Android Open Source Project | |
3 | + | |
4 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + you may not use this file except in compliance with the License. | |
6 | + You may obtain a copy of the License at | |
7 | + | |
8 | + http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + | |
10 | + Unless required by applicable law or agreed to in writing, software | |
11 | + distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + See the License for the specific language governing permissions and | |
14 | + limitations under the License. | |
15 | +--> | |
16 | + | |
17 | +<TextView xmlns:android="http://schemas.android.com/apk/res/android" | |
18 | + android:layout_width="wrap_content" | |
19 | + android:layout_height="match_parent" | |
20 | + android:padding="6dip" | |
21 | + android:textAppearance="?android:attr/textAppearanceMedium" /> |
@@ -0,0 +1,34 @@ | ||
1 | +<?xml version="1.0" encoding="utf-8"?> | |
2 | +<!-- Copyright (C) 2011 The Android Open Source Project | |
3 | + | |
4 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + you may not use this file except in compliance with the License. | |
6 | + You may obtain a copy of the License at | |
7 | + | |
8 | + http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + | |
10 | + Unless required by applicable law or agreed to in writing, software | |
11 | + distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + See the License for the specific language governing permissions and | |
14 | + limitations under the License. | |
15 | +--> | |
16 | + | |
17 | +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
18 | + android:layout_width="match_parent" | |
19 | + android:layout_height="wrap_content" | |
20 | + android:orientation="horizontal" | |
21 | + android:padding="6dip"> | |
22 | + <TextView | |
23 | + android:id="@+id/label" | |
24 | + android:gravity="left" | |
25 | + android:layout_width="0dip" | |
26 | + android:layout_height="match_parent" | |
27 | + android:layout_weight="1" /> | |
28 | + <TextView | |
29 | + android:id="@+id/content" | |
30 | + android:gravity="left" | |
31 | + android:layout_width="0dip" | |
32 | + android:layout_height="match_parent" | |
33 | + android:layout_weight="1" /> | |
34 | +</LinearLayout> |
@@ -773,6 +773,20 @@ | ||
773 | 773 | dot will append the drag\'s textual conversion to the EditText. |
774 | 774 | </string> |
775 | 775 | |
776 | + <string name="game_controller_input_description"> | |
777 | + This activity demonstrates how to process input events received from | |
778 | + game controllers. Please connect your game controller now and try | |
779 | + moving the joysticks or pressing buttons. If it helps, try to imagine | |
780 | + that you are a lone space cowboy in hot pursuit of the aliens who kidnapped | |
781 | + your favorite llama on their way back to Andromeda... | |
782 | + </string> | |
783 | + <string name="game_controller_input_heading_device">Input Device</string> | |
784 | + <string name="game_controller_input_heading_axes">Axes</string> | |
785 | + <string name="game_controller_input_heading_keys">Keys and Buttons</string> | |
786 | + <string name="game_controller_input_label_device_name">Name</string> | |
787 | + <string name="game_controller_input_key_pressed">Pressed</string> | |
788 | + <string name="game_controller_input_key_released">Released</string> | |
789 | + | |
776 | 790 | <!-- ============================== --> |
777 | 791 | <!-- GoogleLogin examples strings --> |
778 | 792 | <!-- ============================== --> |
@@ -0,0 +1,449 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2011 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package com.example.android.apis.view; | |
18 | + | |
19 | +import com.example.android.apis.R; | |
20 | + | |
21 | +import android.app.Activity; | |
22 | +import android.content.Context; | |
23 | +import android.content.res.Resources; | |
24 | +import android.os.Bundle; | |
25 | +import android.util.AttributeSet; | |
26 | +import android.util.Log; | |
27 | +import android.util.SparseArray; | |
28 | +import android.util.SparseIntArray; | |
29 | +import android.view.InputDevice; | |
30 | +import android.view.InputEvent; | |
31 | +import android.view.KeyEvent; | |
32 | +import android.view.LayoutInflater; | |
33 | +import android.view.MotionEvent; | |
34 | +import android.view.View; | |
35 | +import android.view.ViewGroup; | |
36 | +import android.widget.AdapterView; | |
37 | +import android.widget.BaseAdapter; | |
38 | +import android.widget.ListView; | |
39 | +import android.widget.TextView; | |
40 | +import android.widget.Toast; | |
41 | + | |
42 | +import java.util.ArrayList; | |
43 | +import java.util.concurrent.atomic.AtomicLong; | |
44 | + | |
45 | + | |
46 | +/** | |
47 | + * Demonstrates how to process input events received from game controllers. | |
48 | + * | |
49 | + * This activity displays button states and joystick positions. | |
50 | + * Also writes detailed information about relevant input events to the log. | |
51 | + * | |
52 | + * The game controller is also uses to control a very simple game. See {@link GameView} | |
53 | + * for the game itself. | |
54 | + */ | |
55 | +public class GameControllerInput extends Activity { | |
56 | + private static final String TAG = "GameControllerInput"; | |
57 | + | |
58 | + private SparseArray<InputDeviceState> mInputDeviceStates; | |
59 | + private GameView mGame; | |
60 | + private ListView mSummaryList; | |
61 | + private SummaryAdapter mSummaryAdapter; | |
62 | + | |
63 | + @Override | |
64 | + protected void onCreate(Bundle savedInstanceState) { | |
65 | + super.onCreate(savedInstanceState); | |
66 | + | |
67 | + mInputDeviceStates = new SparseArray<InputDeviceState>(); | |
68 | + mSummaryAdapter = new SummaryAdapter(this, getResources()); | |
69 | + | |
70 | + setContentView(R.layout.game_controller_input); | |
71 | + | |
72 | + mGame = (GameView) findViewById(R.id.game); | |
73 | + | |
74 | + mSummaryList = (ListView) findViewById(R.id.summary); | |
75 | + mSummaryList.setAdapter(mSummaryAdapter); | |
76 | + mSummaryList.setOnItemClickListener(new AdapterView.OnItemClickListener() { | |
77 | + @Override | |
78 | + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | |
79 | + mSummaryAdapter.onItemClick(position); | |
80 | + } | |
81 | + }); | |
82 | + } | |
83 | + | |
84 | + @Override | |
85 | + public void onWindowFocusChanged(boolean hasFocus) { | |
86 | + super.onWindowFocusChanged(hasFocus); | |
87 | + | |
88 | + mGame.requestFocus(); | |
89 | + } | |
90 | + | |
91 | + @Override | |
92 | + public boolean dispatchKeyEvent(KeyEvent event) { | |
93 | + // Update device state for visualization and logging. | |
94 | + InputDeviceState state = getInputDeviceState(event); | |
95 | + if (state != null) { | |
96 | + switch (event.getAction()) { | |
97 | + case KeyEvent.ACTION_DOWN: | |
98 | + if (state.onKeyDown(event)) { | |
99 | + mSummaryAdapter.show(state); | |
100 | + } | |
101 | + break; | |
102 | + case KeyEvent.ACTION_UP: | |
103 | + if (state.onKeyUp(event)) { | |
104 | + mSummaryAdapter.show(state); | |
105 | + } | |
106 | + break; | |
107 | + } | |
108 | + } | |
109 | + return super.dispatchKeyEvent(event); | |
110 | + } | |
111 | + | |
112 | + @Override | |
113 | + public boolean dispatchGenericMotionEvent(MotionEvent event) { | |
114 | + // Check that the event came from a joystick since a generic motion event | |
115 | + // could be almost anything. | |
116 | + if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 | |
117 | + && event.getAction() == MotionEvent.ACTION_MOVE) { | |
118 | + // Update device state for visualization and logging. | |
119 | + InputDeviceState state = getInputDeviceState(event); | |
120 | + if (state != null && state.onJoystickMotion(event)) { | |
121 | + mSummaryAdapter.show(state); | |
122 | + } | |
123 | + } | |
124 | + return super.dispatchGenericMotionEvent(event); | |
125 | + } | |
126 | + | |
127 | + private InputDeviceState getInputDeviceState(InputEvent event) { | |
128 | + final int deviceId = event.getDeviceId(); | |
129 | + InputDeviceState state = mInputDeviceStates.get(deviceId); | |
130 | + if (state == null) { | |
131 | + final InputDevice device = event.getDevice(); | |
132 | + if (device == null) { | |
133 | + return null; | |
134 | + } | |
135 | + state = new InputDeviceState(device); | |
136 | + mInputDeviceStates.put(deviceId, state); | |
137 | + | |
138 | + Log.i(TAG, device.toString()); | |
139 | + } | |
140 | + return state; | |
141 | + } | |
142 | + | |
143 | + /** | |
144 | + * Tracks the state of joystick axes and game controller buttons for a particular | |
145 | + * input device for diagnostic purposes. | |
146 | + */ | |
147 | + private static class InputDeviceState { | |
148 | + private final InputDevice mDevice; | |
149 | + private final int[] mAxes; | |
150 | + private final float[] mAxisValues; | |
151 | + private final SparseIntArray mKeys; | |
152 | + | |
153 | + public InputDeviceState(InputDevice device) { | |
154 | + mDevice = device; | |
155 | + mAxes = device.getMotionAxes(); | |
156 | + mAxisValues = new float[mAxes.length]; | |
157 | + mKeys = new SparseIntArray(); | |
158 | + } | |
159 | + | |
160 | + public InputDevice getDevice() { | |
161 | + return mDevice; | |
162 | + } | |
163 | + | |
164 | + public int getAxisCount() { | |
165 | + return mAxes.length; | |
166 | + } | |
167 | + | |
168 | + public int getAxis(int axisIndex) { | |
169 | + return mAxes[axisIndex]; | |
170 | + } | |
171 | + | |
172 | + public float getAxisValue(int axisIndex) { | |
173 | + return mAxisValues[axisIndex]; | |
174 | + } | |
175 | + | |
176 | + public int getKeyCount() { | |
177 | + return mKeys.size(); | |
178 | + } | |
179 | + | |
180 | + public int getKeyCode(int keyIndex) { | |
181 | + return mKeys.keyAt(keyIndex); | |
182 | + } | |
183 | + | |
184 | + public boolean isKeyPressed(int keyIndex) { | |
185 | + return mKeys.valueAt(keyIndex) != 0; | |
186 | + } | |
187 | + | |
188 | + public boolean onKeyDown(KeyEvent event) { | |
189 | + final int keyCode = event.getKeyCode(); | |
190 | + if (isGameKey(keyCode)) { | |
191 | + if (event.getRepeatCount() == 0) { | |
192 | + final String symbolicName = KeyEvent.keyCodeToString(keyCode); | |
193 | + mKeys.put(keyCode, 1); | |
194 | + Log.i(TAG, mDevice.getName() + " - Key Down: " + symbolicName); | |
195 | + } | |
196 | + return true; | |
197 | + } | |
198 | + return false; | |
199 | + } | |
200 | + | |
201 | + public boolean onKeyUp(KeyEvent event) { | |
202 | + final int keyCode = event.getKeyCode(); | |
203 | + if (isGameKey(keyCode)) { | |
204 | + int index = mKeys.indexOfKey(keyCode); | |
205 | + if (index >= 0) { | |
206 | + final String symbolicName = KeyEvent.keyCodeToString(keyCode); | |
207 | + mKeys.put(keyCode, 0); | |
208 | + Log.i(TAG, mDevice.getName() + " - Key Up: " + symbolicName); | |
209 | + } | |
210 | + return true; | |
211 | + } | |
212 | + return false; | |
213 | + } | |
214 | + | |
215 | + public boolean onJoystickMotion(MotionEvent event) { | |
216 | + StringBuilder message = new StringBuilder(); | |
217 | + message.append(mDevice.getName()).append(" - Joystick Motion:\n"); | |
218 | + | |
219 | + final int historySize = event.getHistorySize(); | |
220 | + for (int i = 0; i < mAxes.length; i++) { | |
221 | + final int axis = mAxes[i]; | |
222 | + final float value = event.getAxisValue(axis); | |
223 | + mAxisValues[i] = value; | |
224 | + message.append(" ").append(MotionEvent.axisToString(axis)).append(": "); | |
225 | + | |
226 | + // Append all historical values in the batch. | |
227 | + for (int historyPos = 0; historyPos < historySize; historyPos++) { | |
228 | + message.append(event.getHistoricalAxisValue(axis, historyPos)); | |
229 | + message.append(", "); | |
230 | + } | |
231 | + | |
232 | + // Append the current value. | |
233 | + message.append(value); | |
234 | + message.append("\n"); | |
235 | + } | |
236 | + Log.i(TAG, message.toString()); | |
237 | + return true; | |
238 | + } | |
239 | + | |
240 | + // Check whether this is a key we care about. | |
241 | + // In a real game, we would probably let the user configure which keys to use | |
242 | + // instead of hardcoding the keys like this. | |
243 | + private static boolean isGameKey(int keyCode) { | |
244 | + switch (keyCode) { | |
245 | + case KeyEvent.KEYCODE_DPAD_UP: | |
246 | + case KeyEvent.KEYCODE_DPAD_DOWN: | |
247 | + case KeyEvent.KEYCODE_DPAD_LEFT: | |
248 | + case KeyEvent.KEYCODE_DPAD_RIGHT: | |
249 | + case KeyEvent.KEYCODE_DPAD_CENTER: | |
250 | + case KeyEvent.KEYCODE_SPACE: | |
251 | + return true; | |
252 | + default: | |
253 | + return KeyEvent.isGamepadButton(keyCode); | |
254 | + } | |
255 | + } | |
256 | + } | |
257 | + | |
258 | + /** | |
259 | + * A list adapter that displays a summary of the device state. | |
260 | + */ | |
261 | + private static class SummaryAdapter extends BaseAdapter { | |
262 | + private static final int BASE_ID_HEADING = 1 << 10; | |
263 | + private static final int BASE_ID_DEVICE_ITEM = 2 << 10; | |
264 | + private static final int BASE_ID_AXIS_ITEM = 3 << 10; | |
265 | + private static final int BASE_ID_KEY_ITEM = 4 << 10; | |
266 | + | |
267 | + private final Context mContext; | |
268 | + private final Resources mResources; | |
269 | + | |
270 | + private final SparseArray<Item> mDataItems = new SparseArray<Item>(); | |
271 | + private final ArrayList<Item> mVisibleItems = new ArrayList<Item>(); | |
272 | + | |
273 | + private final Heading mDeviceHeading; | |
274 | + private final TextColumn mDeviceNameTextColumn; | |
275 | + | |
276 | + private final Heading mAxesHeading; | |
277 | + private final Heading mKeysHeading; | |
278 | + | |
279 | + private InputDeviceState mState; | |
280 | + | |
281 | + public SummaryAdapter(Context context, Resources resources) { | |
282 | + mContext = context; | |
283 | + mResources = resources; | |
284 | + | |
285 | + mDeviceHeading = new Heading(BASE_ID_HEADING | 0, | |
286 | + mResources.getString(R.string.game_controller_input_heading_device)); | |
287 | + mDeviceNameTextColumn = new TextColumn(BASE_ID_DEVICE_ITEM | 0, | |
288 | + mResources.getString(R.string.game_controller_input_label_device_name)); | |
289 | + | |
290 | + mAxesHeading = new Heading(BASE_ID_HEADING | 1, | |
291 | + mResources.getString(R.string.game_controller_input_heading_axes)); | |
292 | + mKeysHeading = new Heading(BASE_ID_HEADING | 2, | |
293 | + mResources.getString(R.string.game_controller_input_heading_keys)); | |
294 | + } | |
295 | + | |
296 | + public void onItemClick(int position) { | |
297 | + if (mState != null) { | |
298 | + Toast toast = Toast.makeText( | |
299 | + mContext, mState.getDevice().toString(), Toast.LENGTH_LONG); | |
300 | + toast.show(); | |
301 | + } | |
302 | + } | |
303 | + | |
304 | + public void show(InputDeviceState state) { | |
305 | + mState = state; | |
306 | + mVisibleItems.clear(); | |
307 | + | |
308 | + // Populate device information. | |
309 | + mVisibleItems.add(mDeviceHeading); | |
310 | + mDeviceNameTextColumn.setContent(state.getDevice().getName()); | |
311 | + mVisibleItems.add(mDeviceNameTextColumn); | |
312 | + | |
313 | + // Populate axes. | |
314 | + mVisibleItems.add(mAxesHeading); | |
315 | + final int axisCount = state.getAxisCount(); | |
316 | + for (int i = 0; i < axisCount; i++) { | |
317 | + final int axis = state.getAxis(i); | |
318 | + final int id = BASE_ID_AXIS_ITEM | axis; | |
319 | + TextColumn column = (TextColumn) mDataItems.get(id); | |
320 | + if (column == null) { | |
321 | + column = new TextColumn(id, MotionEvent.axisToString(axis)); | |
322 | + mDataItems.put(id, column); | |
323 | + } | |
324 | + column.setContent(Float.toString(state.getAxisValue(i))); | |
325 | + mVisibleItems.add(column); | |
326 | + } | |
327 | + | |
328 | + // Populate keys. | |
329 | + mVisibleItems.add(mKeysHeading); | |
330 | + final int keyCount = state.getKeyCount(); | |
331 | + for (int i = 0; i < keyCount; i++) { | |
332 | + final int keyCode = state.getKeyCode(i); | |
333 | + final int id = BASE_ID_KEY_ITEM | keyCode; | |
334 | + TextColumn column = (TextColumn) mDataItems.get(id); | |
335 | + if (column == null) { | |
336 | + column = new TextColumn(id, KeyEvent.keyCodeToString(keyCode)); | |
337 | + mDataItems.put(id, column); | |
338 | + } | |
339 | + column.setContent(mResources.getString(state.isKeyPressed(i) | |
340 | + ? R.string.game_controller_input_key_pressed | |
341 | + : R.string.game_controller_input_key_released)); | |
342 | + mVisibleItems.add(column); | |
343 | + } | |
344 | + | |
345 | + notifyDataSetChanged(); | |
346 | + } | |
347 | + | |
348 | + @Override | |
349 | + public boolean hasStableIds() { | |
350 | + return true; | |
351 | + } | |
352 | + | |
353 | + @Override | |
354 | + public int getCount() { | |
355 | + return mVisibleItems.size(); | |
356 | + } | |
357 | + | |
358 | + @Override | |
359 | + public Item getItem(int position) { | |
360 | + return mVisibleItems.get(position); | |
361 | + } | |
362 | + | |
363 | + @Override | |
364 | + public long getItemId(int position) { | |
365 | + return getItem(position).getItemId(); | |
366 | + } | |
367 | + | |
368 | + @Override | |
369 | + public View getView(int position, View convertView, ViewGroup parent) { | |
370 | + return getItem(position).getView(convertView, parent); | |
371 | + } | |
372 | + | |
373 | + private static abstract class Item { | |
374 | + private final int mItemId; | |
375 | + private final int mLayoutResourceId; | |
376 | + private View mView; | |
377 | + | |
378 | + public Item(int itemId, int layoutResourceId) { | |
379 | + mItemId = itemId; | |
380 | + mLayoutResourceId = layoutResourceId; | |
381 | + } | |
382 | + | |
383 | + public long getItemId() { | |
384 | + return mItemId; | |
385 | + } | |
386 | + | |
387 | + public View getView(View convertView, ViewGroup parent) { | |
388 | + if (mView == null) { | |
389 | + LayoutInflater inflater = (LayoutInflater) | |
390 | + parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); | |
391 | + mView = inflater.inflate(mLayoutResourceId, parent, false); | |
392 | + initView(mView); | |
393 | + } | |
394 | + updateView(mView); | |
395 | + return mView; | |
396 | + } | |
397 | + | |
398 | + protected void initView(View view) { | |
399 | + } | |
400 | + | |
401 | + protected void updateView(View view) { | |
402 | + } | |
403 | + } | |
404 | + | |
405 | + private static class Heading extends Item { | |
406 | + private final String mLabel; | |
407 | + | |
408 | + public Heading(int itemId, String label) { | |
409 | + super(itemId, R.layout.game_controller_input_heading); | |
410 | + mLabel = label; | |
411 | + } | |
412 | + | |
413 | + @Override | |
414 | + public void initView(View view) { | |
415 | + TextView textView = (TextView) view; | |
416 | + textView.setText(mLabel); | |
417 | + } | |
418 | + } | |
419 | + | |
420 | + private static class TextColumn extends Item { | |
421 | + private final String mLabel; | |
422 | + | |
423 | + private String mContent; | |
424 | + private TextView mContentView; | |
425 | + | |
426 | + public TextColumn(int itemId, String label) { | |
427 | + super(itemId, R.layout.game_controller_input_text_column); | |
428 | + mLabel = label; | |
429 | + } | |
430 | + | |
431 | + public void setContent(String content) { | |
432 | + mContent = content; | |
433 | + } | |
434 | + | |
435 | + @Override | |
436 | + public void initView(View view) { | |
437 | + TextView textView = (TextView) view.findViewById(R.id.label); | |
438 | + textView.setText(mLabel); | |
439 | + | |
440 | + mContentView = (TextView) view.findViewById(R.id.content); | |
441 | + } | |
442 | + | |
443 | + @Override | |
444 | + public void updateView(View view) { | |
445 | + mContentView.setText(mContent); | |
446 | + } | |
447 | + } | |
448 | + } | |
449 | +} |
@@ -0,0 +1,747 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2011 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package com.example.android.apis.view; | |
18 | + | |
19 | +import android.content.Context; | |
20 | +import android.graphics.Canvas; | |
21 | +import android.graphics.Paint; | |
22 | +import android.graphics.Path; | |
23 | +import android.graphics.Paint.Style; | |
24 | +import android.os.Handler; | |
25 | +import android.os.SystemClock; | |
26 | +import android.util.AttributeSet; | |
27 | +import android.view.InputDevice; | |
28 | +import android.view.KeyEvent; | |
29 | +import android.view.MotionEvent; | |
30 | +import android.view.View; | |
31 | + | |
32 | +import java.util.ArrayList; | |
33 | +import java.util.List; | |
34 | +import java.util.Random; | |
35 | + | |
36 | +/** | |
37 | + * A trivial joystick based physics game to demonstrate joystick handling. | |
38 | + * | |
39 | + * @see GameControllerInput | |
40 | + */ | |
41 | +public class GameView extends View { | |
42 | + private final long ANIMATION_TIME_STEP = 1000 / 60; | |
43 | + private final int MAX_OBSTACLES = 12; | |
44 | + | |
45 | + private final Random mRandom; | |
46 | + private Ship mShip; | |
47 | + private final List<Bullet> mBullets; | |
48 | + private final List<Obstacle> mObstacles; | |
49 | + | |
50 | + private long mLastStepTime; | |
51 | + private InputDevice mLastInputDevice; | |
52 | + | |
53 | + private static final int DPAD_STATE_LEFT = 1 << 0; | |
54 | + private static final int DPAD_STATE_RIGHT = 1 << 1; | |
55 | + private static final int DPAD_STATE_UP = 1 << 2; | |
56 | + private static final int DPAD_STATE_DOWN = 1 << 3; | |
57 | + | |
58 | + private int mDPadState; | |
59 | + | |
60 | + private float mShipSize; | |
61 | + private float mMaxShipThrust; | |
62 | + private float mMaxShipSpeed; | |
63 | + | |
64 | + private float mBulletSize; | |
65 | + private float mBulletSpeed; | |
66 | + | |
67 | + private float mMinObstacleSize; | |
68 | + private float mMaxObstacleSize; | |
69 | + private float mMinObstacleSpeed; | |
70 | + private float mMaxObstacleSpeed; | |
71 | + | |
72 | + private final Runnable mAnimationRunnable = new Runnable() { | |
73 | + public void run() { | |
74 | + animate(); | |
75 | + } | |
76 | + }; | |
77 | + | |
78 | + public GameView(Context context, AttributeSet attrs) { | |
79 | + super(context, attrs); | |
80 | + | |
81 | + mRandom = new Random(); | |
82 | + mBullets = new ArrayList<Bullet>(); | |
83 | + mObstacles = new ArrayList<Obstacle>(); | |
84 | + | |
85 | + setFocusable(true); | |
86 | + setFocusableInTouchMode(true); | |
87 | + | |
88 | + float baseSize = getContext().getResources().getDisplayMetrics().density * 5f; | |
89 | + float baseSpeed = baseSize * 3; | |
90 | + | |
91 | + mShipSize = baseSize * 3; | |
92 | + mMaxShipThrust = baseSpeed * 0.25f; | |
93 | + mMaxShipSpeed = baseSpeed * 12; | |
94 | + | |
95 | + mBulletSize = baseSize; | |
96 | + mBulletSpeed = baseSpeed * 12; | |
97 | + | |
98 | + mMinObstacleSize = baseSize * 2; | |
99 | + mMaxObstacleSize = baseSize * 12; | |
100 | + mMinObstacleSpeed = baseSpeed; | |
101 | + mMaxObstacleSpeed = baseSpeed * 3; | |
102 | + } | |
103 | + | |
104 | + @Override | |
105 | + protected void onSizeChanged(int w, int h, int oldw, int oldh) { | |
106 | + super.onSizeChanged(w, h, oldw, oldh); | |
107 | + | |
108 | + // Reset the game when the view changes size. | |
109 | + reset(); | |
110 | + } | |
111 | + | |
112 | + @Override | |
113 | + public boolean onKeyDown(int keyCode, KeyEvent event) { | |
114 | + ensureInitialized(); | |
115 | + | |
116 | + // Handle DPad keys and fire button on initial down but not on auto-repeat. | |
117 | + boolean handled = false; | |
118 | + if (event.getRepeatCount() == 0) { | |
119 | + switch (keyCode) { | |
120 | + case KeyEvent.KEYCODE_DPAD_LEFT: | |
121 | + mShip.setHeadingX(-1); | |
122 | + mDPadState |= DPAD_STATE_LEFT; | |
123 | + handled = true; | |
124 | + break; | |
125 | + case KeyEvent.KEYCODE_DPAD_RIGHT: | |
126 | + mShip.setHeadingX(1); | |
127 | + mDPadState |= DPAD_STATE_RIGHT; | |
128 | + handled = true; | |
129 | + break; | |
130 | + case KeyEvent.KEYCODE_DPAD_UP: | |
131 | + mShip.setHeadingY(-1); | |
132 | + mDPadState |= DPAD_STATE_UP; | |
133 | + handled = true; | |
134 | + break; | |
135 | + case KeyEvent.KEYCODE_DPAD_DOWN: | |
136 | + mShip.setHeadingY(1); | |
137 | + mDPadState |= DPAD_STATE_DOWN; | |
138 | + handled = true; | |
139 | + break; | |
140 | + default: | |
141 | + if (isFireKey(keyCode)) { | |
142 | + fire(); | |
143 | + handled = true; | |
144 | + } | |
145 | + break; | |
146 | + } | |
147 | + } | |
148 | + if (handled) { | |
149 | + step(event.getEventTime()); | |
150 | + return true; | |
151 | + } | |
152 | + return super.onKeyDown(keyCode, event); | |
153 | + } | |
154 | + | |
155 | + @Override | |
156 | + public boolean onKeyUp(int keyCode, KeyEvent event) { | |
157 | + ensureInitialized(); | |
158 | + | |
159 | + // Handle keys going up. | |
160 | + boolean handled = false; | |
161 | + switch (keyCode) { | |
162 | + case KeyEvent.KEYCODE_DPAD_LEFT: | |
163 | + mShip.setHeadingX(0); | |
164 | + mDPadState &= ~DPAD_STATE_LEFT; | |
165 | + handled = true; | |
166 | + break; | |
167 | + case KeyEvent.KEYCODE_DPAD_RIGHT: | |
168 | + mShip.setHeadingX(0); | |
169 | + mDPadState &= ~DPAD_STATE_RIGHT; | |
170 | + handled = true; | |
171 | + break; | |
172 | + case KeyEvent.KEYCODE_DPAD_UP: | |
173 | + mShip.setHeadingY(0); | |
174 | + mDPadState &= ~DPAD_STATE_UP; | |
175 | + handled = true; | |
176 | + break; | |
177 | + case KeyEvent.KEYCODE_DPAD_DOWN: | |
178 | + mShip.setHeadingY(0); | |
179 | + mDPadState &= ~DPAD_STATE_DOWN; | |
180 | + handled = true; | |
181 | + break; | |
182 | + default: | |
183 | + if (isFireKey(keyCode)) { | |
184 | + handled = true; | |
185 | + } | |
186 | + break; | |
187 | + } | |
188 | + if (handled) { | |
189 | + step(event.getEventTime()); | |
190 | + return true; | |
191 | + } | |
192 | + return super.onKeyUp(keyCode, event); | |
193 | + } | |
194 | + | |
195 | + private static boolean isFireKey(int keyCode) { | |
196 | + return KeyEvent.isGamepadButton(keyCode) | |
197 | + || keyCode == KeyEvent.KEYCODE_DPAD_CENTER | |
198 | + || keyCode == KeyEvent.KEYCODE_SPACE; | |
199 | + } | |
200 | + | |
201 | + @Override | |
202 | + public boolean onGenericMotionEvent(MotionEvent event) { | |
203 | + ensureInitialized(); | |
204 | + | |
205 | + // Check that the event came from a joystick since a generic motion event | |
206 | + // could be almost anything. | |
207 | + if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 | |
208 | + && event.getAction() == MotionEvent.ACTION_MOVE) { | |
209 | + // Cache the most recently obtained device information. | |
210 | + // The device information may change over time but it can be | |
211 | + // somewhat expensive to query. | |
212 | + if (mLastInputDevice == null || mLastInputDevice.getId() != event.getDeviceId()) { | |
213 | + mLastInputDevice = event.getDevice(); | |
214 | + // It's possible for the device id to be invalid. | |
215 | + // In that case, getDevice() will return null. | |
216 | + if (mLastInputDevice == null) { | |
217 | + return false; | |
218 | + } | |
219 | + } | |
220 | + | |
221 | + // Ignore joystick while the DPad is pressed to avoid conflicting motions. | |
222 | + if (mDPadState != 0) { | |
223 | + return true; | |
224 | + } | |
225 | + | |
226 | + // Process all historical movement samples in the batch. | |
227 | + final int historySize = event.getHistorySize(); | |
228 | + for (int i = 0; i < historySize; i++) { | |
229 | + processJoystickInput(event, i); | |
230 | + } | |
231 | + | |
232 | + // Process the current movement sample in the batch. | |
233 | + processJoystickInput(event, -1); | |
234 | + return true; | |
235 | + } | |
236 | + return super.onGenericMotionEvent(event); | |
237 | + } | |
238 | + | |
239 | + private void processJoystickInput(MotionEvent event, int historyPos) { | |
240 | + // Get joystick position. | |
241 | + // Many game pads with two joysticks report the position of the second joystick | |
242 | + // using the Z and RZ axes so we also handle those. | |
243 | + // In a real game, we would allow the user to configure the axes manually. | |
244 | + float x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_X, historyPos); | |
245 | + if (x == 0) { | |
246 | + x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_X, historyPos); | |
247 | + } | |
248 | + if (x == 0) { | |
249 | + x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Z, historyPos); | |
250 | + } | |
251 | + | |
252 | + float y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Y, historyPos); | |
253 | + if (y == 0) { | |
254 | + y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_Y, historyPos); | |
255 | + } | |
256 | + if (y == 0) { | |
257 | + y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_RZ, historyPos); | |
258 | + } | |
259 | + | |
260 | + // Set the ship heading. | |
261 | + mShip.setHeading(x, y); | |
262 | + step(historyPos < 0 ? event.getEventTime() : event.getHistoricalEventTime(historyPos)); | |
263 | + } | |
264 | + | |
265 | + private static float getCenteredAxis(MotionEvent event, InputDevice device, | |
266 | + int axis, int historyPos) { | |
267 | + final InputDevice.MotionRange range = device.getMotionRange(axis); | |
268 | + if (range != null) { | |
269 | + final float flat = range.getFlat(); | |
270 | + final float value = historyPos < 0 ? event.getAxisValue(axis) | |
271 | + : event.getHistoricalAxisValue(axis, historyPos); | |
272 | + | |
273 | + // Ignore axis values that are within the 'flat' region of the joystick axis center. | |
274 | + // A joystick at rest does not always report an absolute position of (0,0). | |
275 | + if (Math.abs(value) > flat) { | |
276 | + return value; | |
277 | + } | |
278 | + } | |
279 | + return 0; | |
280 | + } | |
281 | + | |
282 | + @Override | |
283 | + public void onWindowFocusChanged(boolean hasWindowFocus) { | |
284 | + // Turn on and off animations based on the window focus. | |
285 | + // Alternately, we could update the game state using the Activity onResume() | |
286 | + // and onPause() lifecycle events. | |
287 | + if (hasWindowFocus) { | |
288 | + getHandler().postDelayed(mAnimationRunnable, ANIMATION_TIME_STEP); | |
289 | + mLastStepTime = SystemClock.uptimeMillis(); | |
290 | + } else { | |
291 | + getHandler().removeCallbacks(mAnimationRunnable); | |
292 | + | |
293 | + mDPadState = 0; | |
294 | + if (mShip != null) { | |
295 | + mShip.setHeading(0, 0); | |
296 | + mShip.setVelocity(0, 0); | |
297 | + } | |
298 | + } | |
299 | + | |
300 | + super.onWindowFocusChanged(hasWindowFocus); | |
301 | + } | |
302 | + | |
303 | + private void fire() { | |
304 | + if (mShip != null && !mShip.isDestroyed()) { | |
305 | + Bullet bullet = new Bullet(); | |
306 | + bullet.setPosition(mShip.getBulletInitialX(), mShip.getBulletInitialY()); | |
307 | + bullet.setVelocity(mShip.getBulletVelocityX(mBulletSpeed), | |
308 | + mShip.getBulletVelocityY(mBulletSpeed)); | |
309 | + mBullets.add(bullet); | |
310 | + } | |
311 | + } | |
312 | + | |
313 | + private void ensureInitialized() { | |
314 | + if (mShip == null) { | |
315 | + reset(); | |
316 | + } | |
317 | + } | |
318 | + | |
319 | + private void reset() { | |
320 | + mShip = new Ship(); | |
321 | + mBullets.clear(); | |
322 | + mObstacles.clear(); | |
323 | + } | |
324 | + | |
325 | + void animate() { | |
326 | + long currentStepTime = SystemClock.uptimeMillis(); | |
327 | + step(currentStepTime); | |
328 | + | |
329 | + Handler handler = getHandler(); | |
330 | + if (handler != null) { | |
331 | + handler.postAtTime(mAnimationRunnable, currentStepTime + ANIMATION_TIME_STEP); | |
332 | + invalidate(); | |
333 | + } | |
334 | + } | |
335 | + | |
336 | + private void step(long currentStepTime) { | |
337 | + float tau = (currentStepTime - mLastStepTime) * 0.001f; | |
338 | + mLastStepTime = currentStepTime; | |
339 | + | |
340 | + ensureInitialized(); | |
341 | + | |
342 | + // Move the ship. | |
343 | + mShip.accelerate(tau, mMaxShipThrust, mMaxShipSpeed); | |
344 | + if (!mShip.step(tau)) { | |
345 | + reset(); | |
346 | + } | |
347 | + | |
348 | + // Move the bullets. | |
349 | + int numBullets = mBullets.size(); | |
350 | + for (int i = 0; i < numBullets; i++) { | |
351 | + final Bullet bullet = mBullets.get(i); | |
352 | + if (!bullet.step(tau)) { | |
353 | + mBullets.remove(i); | |
354 | + i -= 1; | |
355 | + numBullets -= 1; | |
356 | + } | |
357 | + } | |
358 | + | |
359 | + // Move obstacles. | |
360 | + int numObstacles = mObstacles.size(); | |
361 | + for (int i = 0; i < numObstacles; i++) { | |
362 | + final Obstacle obstacle = mObstacles.get(i); | |
363 | + if (!obstacle.step(tau)) { | |
364 | + mObstacles.remove(i); | |
365 | + i -= 1; | |
366 | + numObstacles -= 1; | |
367 | + } | |
368 | + } | |
369 | + | |
370 | + // Check for collisions between bullets and obstacles. | |
371 | + for (int i = 0; i < numBullets; i++) { | |
372 | + final Bullet bullet = mBullets.get(i); | |
373 | + for (int j = 0; j < numObstacles; j++) { | |
374 | + final Obstacle obstacle = mObstacles.get(j); | |
375 | + if (bullet.collidesWith(obstacle)) { | |
376 | + bullet.destroy(); | |
377 | + obstacle.destroy(); | |
378 | + break; | |
379 | + } | |
380 | + } | |
381 | + } | |
382 | + | |
383 | + // Check for collisions between the ship and obstacles. | |
384 | + for (int i = 0; i < numObstacles; i++) { | |
385 | + final Obstacle obstacle = mObstacles.get(i); | |
386 | + if (mShip.collidesWith(obstacle)) { | |
387 | + mShip.destroy(); | |
388 | + obstacle.destroy(); | |
389 | + break; | |
390 | + } | |
391 | + } | |
392 | + | |
393 | + // Spawn more obstacles offscreen when needed. | |
394 | + // Avoid putting them right on top of the ship. | |
395 | + OuterLoop: while (mObstacles.size() < MAX_OBSTACLES) { | |
396 | + final float minDistance = mShipSize * 4; | |
397 | + float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize) | |
398 | + + mMinObstacleSize; | |
399 | + float positionX, positionY; | |
400 | + int tries = 0; | |
401 | + do { | |
402 | + int edge = mRandom.nextInt(4); | |
403 | + switch (edge) { | |
404 | + case 0: | |
405 | + positionX = -size; | |
406 | + positionY = mRandom.nextInt(getHeight()); | |
407 | + break; | |
408 | + case 1: | |
409 | + positionX = getWidth() + size; | |
410 | + positionY = mRandom.nextInt(getHeight()); | |
411 | + break; | |
412 | + case 2: | |
413 | + positionX = mRandom.nextInt(getWidth()); | |
414 | + positionY = -size; | |
415 | + break; | |
416 | + default: | |
417 | + positionX = mRandom.nextInt(getWidth()); | |
418 | + positionY = getHeight() + size; | |
419 | + break; | |
420 | + } | |
421 | + if (++tries > 10) { | |
422 | + break OuterLoop; | |
423 | + } | |
424 | + } while (mShip.distanceTo(positionX, positionY) < minDistance); | |
425 | + | |
426 | + float direction = mRandom.nextFloat() * (float) Math.PI * 2; | |
427 | + float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed) | |
428 | + + mMinObstacleSpeed; | |
429 | + float velocityX = (float) Math.cos(direction) * speed; | |
430 | + float velocityY = (float) Math.sin(direction) * speed; | |
431 | + | |
432 | + Obstacle obstacle = new Obstacle(); | |
433 | + obstacle.setPosition(positionX, positionY); | |
434 | + obstacle.setSize(size); | |
435 | + obstacle.setVelocity(velocityX, velocityY); | |
436 | + mObstacles.add(obstacle); | |
437 | + } | |
438 | + } | |
439 | + | |
440 | + @Override | |
441 | + protected void onDraw(Canvas canvas) { | |
442 | + super.onDraw(canvas); | |
443 | + | |
444 | + // Draw the ship. | |
445 | + if (mShip != null) { | |
446 | + mShip.draw(canvas); | |
447 | + } | |
448 | + | |
449 | + // Draw bullets. | |
450 | + int numBullets = mBullets.size(); | |
451 | + for (int i = 0; i < numBullets; i++) { | |
452 | + final Bullet bullet = mBullets.get(i); | |
453 | + bullet.draw(canvas); | |
454 | + } | |
455 | + | |
456 | + // Draw obstacles. | |
457 | + int numObstacles = mObstacles.size(); | |
458 | + for (int i = 0; i < numObstacles; i++) { | |
459 | + final Obstacle obstacle = mObstacles.get(i); | |
460 | + obstacle.draw(canvas); | |
461 | + } | |
462 | + } | |
463 | + | |
464 | + static float pythag(float x, float y) { | |
465 | + return (float) Math.sqrt(x * x + y * y); | |
466 | + } | |
467 | + | |
468 | + static int blend(float alpha, int from, int to) { | |
469 | + return from + (int) ((to - from) * alpha); | |
470 | + } | |
471 | + | |
472 | + static void setPaintARGBBlend(Paint paint, float alpha, | |
473 | + int a1, int r1, int g1, int b1, | |
474 | + int a2, int r2, int g2, int b2) { | |
475 | + paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2), | |
476 | + blend(alpha, g1, g2), blend(alpha, b1, b2)); | |
477 | + } | |
478 | + | |
479 | + private abstract class Sprite { | |
480 | + protected float mPositionX; | |
481 | + protected float mPositionY; | |
482 | + protected float mVelocityX; | |
483 | + protected float mVelocityY; | |
484 | + protected float mSize; | |
485 | + protected boolean mDestroyed; | |
486 | + protected float mDestroyAnimProgress; | |
487 | + | |
488 | + public void setPosition(float x, float y) { | |
489 | + mPositionX = x; | |
490 | + mPositionY = y; | |
491 | + } | |
492 | + | |
493 | + public void setVelocity(float x, float y) { | |
494 | + mVelocityX = x; | |
495 | + mVelocityY = y; | |
496 | + } | |
497 | + | |
498 | + public void setSize(float size) { | |
499 | + mSize = size; | |
500 | + } | |
501 | + | |
502 | + public float distanceTo(float x, float y) { | |
503 | + return pythag(mPositionX - x, mPositionY - y); | |
504 | + } | |
505 | + | |
506 | + public float distanceTo(Sprite other) { | |
507 | + return distanceTo(other.mPositionX, other.mPositionY); | |
508 | + } | |
509 | + | |
510 | + public boolean collidesWith(Sprite other) { | |
511 | + // Really bad collision detection. | |
512 | + return !mDestroyed && !other.mDestroyed | |
513 | + && distanceTo(other) <= Math.max(mSize, other.mSize) | |
514 | + + Math.min(mSize, other.mSize) * 0.5f; | |
515 | + } | |
516 | + | |
517 | + public boolean isDestroyed() { | |
518 | + return mDestroyed; | |
519 | + } | |
520 | + | |
521 | + public boolean step(float tau) { | |
522 | + mPositionX += mVelocityX * tau; | |
523 | + mPositionY += mVelocityY * tau; | |
524 | + | |
525 | + if (mDestroyed) { | |
526 | + mDestroyAnimProgress += tau / getDestroyAnimDuration(); | |
527 | + if (mDestroyAnimProgress >= 1.0f) { | |
528 | + return false; | |
529 | + } | |
530 | + } | |
531 | + return true; | |
532 | + } | |
533 | + | |
534 | + public abstract void draw(Canvas canvas); | |
535 | + | |
536 | + public abstract float getDestroyAnimDuration(); | |
537 | + | |
538 | + protected boolean isOutsidePlayfield() { | |
539 | + final int width = GameView.this.getWidth(); | |
540 | + final int height = GameView.this.getHeight(); | |
541 | + return mPositionX < 0 || mPositionX >= width | |
542 | + || mPositionY < 0 || mPositionY >= height; | |
543 | + } | |
544 | + | |
545 | + protected void wrapAtPlayfieldBoundary() { | |
546 | + final int width = GameView.this.getWidth(); | |
547 | + final int height = GameView.this.getHeight(); | |
548 | + while (mPositionX <= -mSize) { | |
549 | + mPositionX += width + mSize * 2; | |
550 | + } | |
551 | + while (mPositionX >= width + mSize) { | |
552 | + mPositionX -= width + mSize * 2; | |
553 | + } | |
554 | + while (mPositionY <= -mSize) { | |
555 | + mPositionY += height + mSize * 2; | |
556 | + } | |
557 | + while (mPositionY >= height + mSize) { | |
558 | + mPositionY -= height + mSize * 2; | |
559 | + } | |
560 | + } | |
561 | + | |
562 | + public void destroy() { | |
563 | + mDestroyed = true; | |
564 | + step(0); | |
565 | + } | |
566 | + } | |
567 | + | |
568 | + private class Ship extends Sprite { | |
569 | + private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3; | |
570 | + private static final float TO_DEGREES = (float) (180.0 / Math.PI); | |
571 | + | |
572 | + private float mHeadingX; | |
573 | + private float mHeadingY; | |
574 | + private float mHeadingAngle; | |
575 | + private float mHeadingMagnitude; | |
576 | + private final Paint mPaint; | |
577 | + private final Path mPath; | |
578 | + | |
579 | + | |
580 | + public Ship() { | |
581 | + mPaint = new Paint(); | |
582 | + mPaint.setStyle(Style.FILL); | |
583 | + | |
584 | + setPosition(getWidth() * 0.5f, getHeight() * 0.5f); | |
585 | + setVelocity(0, 0); | |
586 | + setSize(mShipSize); | |
587 | + | |
588 | + mPath = new Path(); | |
589 | + mPath.moveTo(0, 0); | |
590 | + mPath.lineTo((float)Math.cos(-CORNER_ANGLE) * mSize, | |
591 | + (float)Math.sin(-CORNER_ANGLE) * mSize); | |
592 | + mPath.lineTo(mSize, 0); | |
593 | + mPath.lineTo((float)Math.cos(CORNER_ANGLE) * mSize, | |
594 | + (float)Math.sin(CORNER_ANGLE) * mSize); | |
595 | + mPath.lineTo(0, 0); | |
596 | + } | |
597 | + | |
598 | + public void setHeadingX(float x) { | |
599 | + mHeadingX = x; | |
600 | + updateHeading(); | |
601 | + } | |
602 | + | |
603 | + public void setHeadingY(float y) { | |
604 | + mHeadingY = y; | |
605 | + updateHeading(); | |
606 | + } | |
607 | + | |
608 | + public void setHeading(float x, float y) { | |
609 | + mHeadingX = x; | |
610 | + mHeadingY = y; | |
611 | + updateHeading(); | |
612 | + } | |
613 | + | |
614 | + private void updateHeading() { | |
615 | + mHeadingMagnitude = pythag(mHeadingX, mHeadingY); | |
616 | + if (mHeadingMagnitude > 0.1f) { | |
617 | + mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX); | |
618 | + } | |
619 | + } | |
620 | + | |
621 | + private float polarX(float radius) { | |
622 | + return (float) Math.cos(mHeadingAngle) * radius; | |
623 | + } | |
624 | + | |
625 | + private float polarY(float radius) { | |
626 | + return (float) Math.sin(mHeadingAngle) * radius; | |
627 | + } | |
628 | + | |
629 | + public float getBulletInitialX() { | |
630 | + return mPositionX + polarX(mSize); | |
631 | + } | |
632 | + | |
633 | + public float getBulletInitialY() { | |
634 | + return mPositionY + polarY(mSize); | |
635 | + } | |
636 | + | |
637 | + public float getBulletVelocityX(float relativeSpeed) { | |
638 | + return mVelocityX + polarX(relativeSpeed); | |
639 | + } | |
640 | + | |
641 | + public float getBulletVelocityY(float relativeSpeed) { | |
642 | + return mVelocityY + polarY(relativeSpeed); | |
643 | + } | |
644 | + | |
645 | + public void accelerate(float tau, float maxThrust, float maxSpeed) { | |
646 | + final float thrust = mHeadingMagnitude * maxThrust; | |
647 | + mVelocityX += polarX(thrust); | |
648 | + mVelocityY += polarY(thrust); | |
649 | + | |
650 | + final float speed = pythag(mVelocityX, mVelocityY); | |
651 | + if (speed > maxSpeed) { | |
652 | + final float scale = maxSpeed / speed; | |
653 | + mVelocityX = mVelocityX * scale; | |
654 | + mVelocityY = mVelocityY * scale; | |
655 | + } | |
656 | + } | |
657 | + | |
658 | + @Override | |
659 | + public boolean step(float tau) { | |
660 | + if (!super.step(tau)) { | |
661 | + return false; | |
662 | + } | |
663 | + wrapAtPlayfieldBoundary(); | |
664 | + return true; | |
665 | + } | |
666 | + | |
667 | + public void draw(Canvas canvas) { | |
668 | + setPaintARGBBlend(mPaint, mDestroyAnimProgress, | |
669 | + 255, 63, 255, 63, | |
670 | + 0, 255, 0, 0); | |
671 | + | |
672 | + canvas.save(Canvas.MATRIX_SAVE_FLAG); | |
673 | + canvas.translate(mPositionX, mPositionY); | |
674 | + canvas.rotate(mHeadingAngle * TO_DEGREES); | |
675 | + canvas.drawPath(mPath, mPaint); | |
676 | + canvas.restore(); | |
677 | + } | |
678 | + | |
679 | + @Override | |
680 | + public float getDestroyAnimDuration() { | |
681 | + return 1.0f; | |
682 | + } | |
683 | + } | |
684 | + | |
685 | + private class Bullet extends Sprite { | |
686 | + private final Paint mPaint; | |
687 | + | |
688 | + public Bullet() { | |
689 | + mPaint = new Paint(); | |
690 | + mPaint.setStyle(Style.FILL); | |
691 | + | |
692 | + setSize(mBulletSize); | |
693 | + } | |
694 | + | |
695 | + @Override | |
696 | + public boolean step(float tau) { | |
697 | + if (!super.step(tau)) { | |
698 | + return false; | |
699 | + } | |
700 | + return !isOutsidePlayfield(); | |
701 | + } | |
702 | + | |
703 | + public void draw(Canvas canvas) { | |
704 | + setPaintARGBBlend(mPaint, mDestroyAnimProgress, | |
705 | + 255, 255, 255, 0, | |
706 | + 0, 255, 255, 255); | |
707 | + canvas.drawCircle(mPositionX, mPositionY, mSize, mPaint); | |
708 | + } | |
709 | + | |
710 | + @Override | |
711 | + public float getDestroyAnimDuration() { | |
712 | + return 0.125f; | |
713 | + } | |
714 | + } | |
715 | + | |
716 | + private class Obstacle extends Sprite { | |
717 | + private final Paint mPaint; | |
718 | + | |
719 | + public Obstacle() { | |
720 | + mPaint = new Paint(); | |
721 | + mPaint.setARGB(255, 127, 127, 255); | |
722 | + mPaint.setStyle(Style.FILL); | |
723 | + } | |
724 | + | |
725 | + @Override | |
726 | + public boolean step(float tau) { | |
727 | + if (!super.step(tau)) { | |
728 | + return false; | |
729 | + } | |
730 | + wrapAtPlayfieldBoundary(); | |
731 | + return true; | |
732 | + } | |
733 | + | |
734 | + public void draw(Canvas canvas) { | |
735 | + setPaintARGBBlend(mPaint, mDestroyAnimProgress, | |
736 | + 255, 127, 127, 255, | |
737 | + 0, 255, 0, 0); | |
738 | + canvas.drawCircle(mPositionX, mPositionY, | |
739 | + mSize * (1.0f - mDestroyAnimProgress), mPaint); | |
740 | + } | |
741 | + | |
742 | + @Override | |
743 | + public float getDestroyAnimDuration() { | |
744 | + return 0.25f; | |
745 | + } | |
746 | + } | |
747 | +} | |
\ No newline at end of file |