MainMenuActivity
Activity界面
MainMenuActivity启动的时候,会用fade in动画逐个显示菜单按钮,并在屏幕最下方显示一个Ticker:
点击任一菜单按钮后,被点击的按钮会flick几下,然后执行期望的切换:
EXTRAS 菜单中,LINEAR MODE 和 LEVEL SELECT 两个菜单一开始是 locked 的,并且红色 LOCKED 水印会不断闪烁,估计玩到后期会解锁吧:
点击任一菜单按钮,背景也会fade out,我们可以通过点击 OPTIONS 来看看效果:
代码分析
public class MainMenuActivity extends Activity {
private boolean mPaused;
private View mStartButton;
private View mOptionsButton;
private View mExtrasButton;
private View mBackground;
private View mTicker;
private Animation mButtonFlickerAnimation;
private Animation mFadeOutAnimation;
private Animation mAlternateFadeOutAnimation;
private Animation mFadeInAnimation;
private boolean mJustCreated;
private String mSelectedControlsString;
private final static int WHATS_NEW_DIALOG = 0;
private final static int TILT_TO_SCREEN_CONTROLS_DIALOG = 1;
private final static int CONTROL_SETUP_DIALOG = 2;
// Create an anonymous implementation of OnClickListener
private View.OnClickListener sContinueButtonListener = new View.OnClickListener() {
public void onClick(View v) {
if (!mPaused) {
Intent i = new Intent(getBaseContext(), AndouKun.class);
v.startAnimation(mButtonFlickerAnimation);
mFadeOutAnimation.setAnimationListener(new StartActivityAfterAnimation(i));
mBackground.startAnimation(mFadeOutAnimation);
mOptionsButton.startAnimation(mAlternateFadeOutAnimation);
mExtrasButton.startAnimation(mAlternateFadeOutAnimation);
mTicker.startAnimation(mAlternateFadeOutAnimation);
mPaused = true;
}
}
};
private View.OnClickListener sOptionButtonListener = new View.OnClickListener() {
public void onClick(View v) {
if (!mPaused) {
Intent i = new Intent(getBaseContext(), SetPreferencesActivity.class);
v.startAnimation(mButtonFlickerAnimation);
mFadeOutAnimation.setAnimationListener(new StartActivityAfterAnimation(i));
mBackground.startAnimation(mFadeOutAnimation);
mStartButton.startAnimation(mAlternateFadeOutAnimation);
mExtrasButton.startAnimation(mAlternateFadeOutAnimation);
mTicker.startAnimation(mAlternateFadeOutAnimation);
mPaused = true;
}
}
};
private View.OnClickListener sExtrasButtonListener = new View.OnClickListener() {
public void onClick(View v) {
if (!mPaused) {
Intent i = new Intent(getBaseContext(), ExtrasMenuActivity.class);
v.startAnimation(mButtonFlickerAnimation);
mButtonFlickerAnimation.setAnimationListener(new StartActivityAfterAnimation(i));
mPaused = true;
}
}
};
private View.OnClickListener sStartButtonListener = new View.OnClickListener() {
public void onClick(View v) {
if (!mPaused) {
Intent i = new Intent(getBaseContext(), DifficultyMenuActivity.class);
i.putExtra("newGame", true);
v.startAnimation(mButtonFlickerAnimation);
mButtonFlickerAnimation.setAnimationListener(new StartActivityAfterAnimation(i));
mPaused = true;
}
}
};
以上是几个菜单按钮的响应方法,都利用变量mPaused防止快速连击,并通过 mButtonFlickerAnimation 来闪烁点击的按钮菜单。其中前两个listener将背景和其它按钮用fade out动画隐去,而后面两个listener只是把点击的按钮用fade out动画隐去。fade out动画结束后,跳转到目标Activity。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mainmenu);
mPaused = true;
onPause方法里将mPaused设置为true,onResume方法里将mPaused设置为false,这都好理解。但onCreate方法里也将mPaused设置为true,就有点奇怪了。查看了下Android的文档,才回忆起Activity的几个回调方法的顺序:onCreate -> onStart -> onResume。所以在onCreate将mPaused设置为true也很合理了。
mStartButton = findViewById(R.id.startButton);
mOptionsButton = findViewById(R.id.optionButton);
mBackground = findViewById(R.id.mainMenuBackground);
if (mOptionsButton != null) {
mOptionsButton.setOnClickListener(sOptionButtonListener);
}
mExtrasButton = findViewById(R.id.extrasButton);
mExtrasButton.setOnClickListener(sExtrasButtonListener);
初始化各个菜单按钮。
mButtonFlickerAnimation = AnimationUtils.loadAnimation(this, R.anim.button_flicker);
mFadeOutAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_out);
mAlternateFadeOutAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_out);
mFadeInAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_in);
加载fade in/out动画。这里,mFadeOutAnimation和mAlternateFadeOutAnimation是一个动画,也可以加载不同的fade out动画。AnimationUtils.loadAnimation(Context, int)这个方法是Android SDK的android.view.animation包提供的。
SharedPreferences prefs =
getSharedPreferences(PreferenceConstants.PREFERENCE_NAME, MODE_PRIVATE);
final int row = prefs.getInt(PreferenceConstants.PREFERENCE_LEVEL_ROW, 0);
final int index = prefs.getInt(PreferenceConstants.PREFERENCE_LEVEL_INDEX, 0);
int levelTreeResource = R.xml.level_tree;
if (row != 0 || index != 0) {
final int linear = prefs.getInt(PreferenceConstants.PREFERENCE_LINEAR_MODE, 0);
if (linear != 0) {
levelTreeResource = R.xml.linear_level_tree;
}
}
if (!LevelTree.isLoaded(levelTreeResource)) {
LevelTree.loadLevelTree(levelTreeResource, this);
LevelTree.loadAllDialog(this);
}
加载level数据,其实就是NPC对话数据。这里涉及到一大堆文件,先mark一下,后面再专门研究。
mTicker = findViewById(R.id.ticker);
if (mTicker != null) {
mTicker.setFocusable(true);
mTicker.requestFocus();
mTicker.setSelected(true);
}
初始化Ticker。
mJustCreated = true;
// Keep the volume control type consistent across all activities.
setVolumeControlStream(AudioManager.STREAM_MUSIC);
// MediaPlayer mp = MediaPlayer.create(this, R.raw.bwv_115);
// mp.start();
初始化媒体播放器。游戏中只播放了声效,没有背景音乐。这里的MediaPlayer变量应该是留待以后扩展的。
}
@Override
protected void onPause() {
super.onPause();
mPaused = true;
}
@Override
protected void onResume() {
super.onResume();
mPaused = false;
mButtonFlickerAnimation.setAnimationListener(null);
if (mStartButton != null) {
// Change "start" to "continue" if there's a saved game.
SharedPreferences prefs =
getSharedPreferences(PreferenceConstants.PREFERENCE_NAME, MODE_PRIVATE);
final int row = prefs.getInt(PreferenceConstants.PREFERENCE_LEVEL_ROW, 0);
final int index = prefs.getInt(PreferenceConstants.PREFERENCE_LEVEL_INDEX, 0);
if (row != 0 || index != 0) {
((ImageView) mStartButton).setImageDrawable(getResources().getDrawable(
R.drawable.ui_button_continue));
mStartButton.setOnClickListener(sContinueButtonListener);
} else {
((ImageView) mStartButton).setImageDrawable(getResources().getDrawable(
R.drawable.ui_button_start));
mStartButton.setOnClickListener(sStartButtonListener);
}
TouchFilter touch;
final int sdkVersion = Integer.parseInt(Build.VERSION.SDK);
if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
touch = new SingleTouchFilter();
} else {
touch = new MultiTouchFilter();
}
TouchFilter是Replica Island的一个抽象类,其继承关系见《Replica Island 学习笔记 02 - 初步分析》。TouchFilter负责通过ObjectRegistry实例将触摸屏的Touch事件传递给InputSystem实例。ObjectRegistry类只有一个全局静态实例,且记录于BaseObject类的成员变量sSystemRegistry。sSystemRegistry各成员的初始化在Game类的方法bootstrap里。这里,ObjectRegistry、InputSystem、BaseObject和Game都是指Replica Island内部实现的类。
final int lastVersion = prefs.getInt(PreferenceConstants.PREFERENCE_LAST_VERSION, 0);
if (lastVersion == 0) {
// This is the first time the game has been run.
// Pre-configure the control options to match the device.
// The resource system can tell us what this device has.
// TODO: is there a better way to do this? Seems like a kind of neat
// way to do custom device profiles.
final String navType = getString(R.string.nav_type);
mSelectedControlsString = getString(R.string.control_setup_dialog_trackball);
if (navType != null) {
if (navType.equalsIgnoreCase("DPad")) {
// Turn off the click-to-attack pref on devices that have a dpad.
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PreferenceConstants.PREFERENCE_CLICK_ATTACK, false);
editor.commit();
mSelectedControlsString = getString(R.string.control_setup_dialog_dpad);
} else if (navType.equalsIgnoreCase("None")) {
SharedPreferences.Editor editor = prefs.edit();
// This test relies on the PackageManager if api version >= 5.
if (touch.supportsMultitouch(this)) {
// Default to screen controls.
editor.putBoolean(PreferenceConstants.PREFERENCE_SCREEN_CONTROLS, true);
mSelectedControlsString =
getString(R.string.control_setup_dialog_screen);
} else {
// Turn on tilt controls if there's nothing else.
editor.putBoolean(PreferenceConstants.PREFERENCE_TILT_CONTROLS, true);
mSelectedControlsString = getString(R.string.control_setup_dialog_tilt);
}
editor.commit();
}
}
}
if (Math.abs(lastVersion) < Math.abs(AndouKun.VERSION)) {
// This is a new install or an upgrade.
// Check the safe mode option.
// Useful reference: http://en.wikipedia.org/wiki/List_of_Android_devices
if (Build.PRODUCT.contains("morrison") || // Motorola Cliq/Dext
Build.MODEL.contains("Pulse") || // Huawei Pulse
Build.MODEL.contains("U8220") || // Huawei Pulse
Build.MODEL.contains("U8230") || // Huawei U8230
Build.MODEL.contains("MB300") || // Motorola Backflip
Build.MODEL.contains("MB501") || // Motorola Quench / Cliq XT
Build.MODEL.contains("Behold+II")) { // Samsung Behold II
// These are all models that users have complained about. They likely use
// the same buggy QTC graphics driver. Turn on Safe Mode by default
// for these devices.
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PreferenceConstants.PREFERENCE_SAFE_MODE, true);
editor.commit();
}
SharedPreferences.Editor editor = prefs.edit();
if (lastVersion > 0 && lastVersion < 14) {
// if the user has beat the game once, go ahead and unlock stuff for them.
if (prefs.getInt(PreferenceConstants.PREFERENCE_LAST_ENDING, -1) != -1) {
editor.putBoolean(PreferenceConstants.PREFERENCE_EXTRAS_UNLOCKED, true);
}
}
解锁EXTRAS菜单。
// show what's new message
editor.putInt(PreferenceConstants.PREFERENCE_LAST_VERSION, AndouKun.VERSION);
editor.commit();
showDialog(WHATS_NEW_DIALOG);
// screen controls were added in version 14
if (lastVersion > 0 && lastVersion < 14
&& prefs.getBoolean(PreferenceConstants.PREFERENCE_TILT_CONTROLS, false)) {
if (touch.supportsMultitouch(this)) {
// show message about switching from tilt to screen controls
showDialog(TILT_TO_SCREEN_CONTROLS_DIALOG);
}
} else if (lastVersion == 0) {
// show message about auto-selected control schemes.
showDialog(CONTROL_SETUP_DIALOG);
}
}
}
if (mBackground != null) {
mBackground.clearAnimation();
}
if (mTicker != null) {
mTicker.clearAnimation();
mTicker.setAnimation(mFadeInAnimation);
}
if (mJustCreated) {
if (mStartButton != null) {
mStartButton
.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_slide));
}
if (mExtrasButton != null) {
Animation anim = AnimationUtils.loadAnimation(this, R.anim.button_slide);
anim.setStartOffset(500L);
mExtrasButton.startAnimation(anim);
}
if (mOptionsButton != null) {
Animation anim = AnimationUtils.loadAnimation(this, R.anim.button_slide);
anim.setStartOffset(1000L);
mOptionsButton.startAnimation(anim);
}
mJustCreated = false;
逐个显示菜单按钮。注意:mJustCreated决定了这个动作只会在MainMenuActivity创建时执行。
} else {
mStartButton.clearAnimation();
mOptionsButton.clearAnimation();
mExtrasButton.clearAnimation();
}
}
@Override
protected Dialog onCreateDialog(int id) {
Dialog dialog;
if (id == WHATS_NEW_DIALOG) {
dialog =
new AlertDialog.Builder(this).setTitle(R.string.whats_new_dialog_title)
.setPositiveButton(R.string.whats_new_dialog_ok, null)
.setMessage(R.string.whats_new_dialog_message).create();
现实更新信息,即字符串资源R.string.whats_new_dialog_message,则任一新版本的更新都可以放到这个字符串资源中。
} else if (id == TILT_TO_SCREEN_CONTROLS_DIALOG) {
dialog =
new AlertDialog.Builder(this)
.setTitle(R.string.onscreen_tilt_dialog_title)
.setPositiveButton(R.string.onscreen_tilt_dialog_ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
SharedPreferences prefs =
getSharedPreferences(
PreferenceConstants.PREFERENCE_NAME,
MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(
PreferenceConstants.PREFERENCE_SCREEN_CONTROLS,
true);
editor.commit();
}
})
.setNegativeButton(R.string.onscreen_tilt_dialog_cancel, null)
.setMessage(R.string.onscreen_tilt_dialog_message).create();
} else if (id == CONTROL_SETUP_DIALOG) {
String messageFormat = getResources().getString(R.string.control_setup_dialog_message);
String message = String.format(messageFormat, mSelectedControlsString);
CharSequence sytledMessage = Html.fromHtml(message); // lame.
dialog =
new AlertDialog.Builder(this)
.setTitle(R.string.control_setup_dialog_title)
.setPositiveButton(R.string.control_setup_dialog_ok, null)
.setNegativeButton(R.string.control_setup_dialog_change,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
Intent i =
new Intent(getBaseContext(),
SetPreferencesActivity.class);
i.putExtra("controlConfig", true);
startActivity(i);
}
}).setMessage(sytledMessage).create();
} else {
dialog = super.onCreateDialog(id);
}
return dialog;
}
protected class StartActivityAfterAnimation implements Animation.AnimationListener {
private Intent mIntent;
StartActivityAfterAnimation(Intent intent) {
mIntent = intent;
}
public void onAnimationEnd(Animation animation) {
startActivity(mIntent);
if (UIConstants.mOverridePendingTransition != null) {
try {
UIConstants.mOverridePendingTransition.invoke(MainMenuActivity.this,
R.anim.activity_fade_in, R.anim.activity_fade_out);
} catch (InvocationTargetException ite) {
DebugLog.d("Activity Transition", "Invocation Target Exception");
} catch (IllegalAccessException ie) {
DebugLog.d("Activity Transition", "Illegal Access Exception");
}
}
}
UIConstants类尝试通过relect获取Activity的overridePendingTransition方法,并记录到成员变量mOverridePendingTransition中去,具体可以查看UIConstants类的代码。
public void onAnimationRepeat(Animation animation) {
// TODO Auto-generated method stub
}
public void onAnimationStart(Animation animation) {
// TODO Auto-generated method stub
}
}
}
AndouKun、DifficultyMenuActivity、ExtrasMenuActivity、SetPreferencesActivity
DifficultyMenuActivity、ExtrasMenuActivity的代码结构与MainMenuActivity差不多,这里就都不贴出来了。这些Activity里的下列函数、子类的代码都差不多,其实可以将它们提取到一个父类里:
- public boolean onKeyDown(int keyCode, KeyEvent event)
- protected class StartActivityAfterAnimation implements Animation.AnimationListener
SetPreferencesActivity的代码比较简单,这里也不贴出来了。
AndouKun这个Activity我会另起一篇来分析。
以下是这些Activity的调用关系: