感谢@Sheldon 为我指出关于setRetainInstance(true)
“无头”片段的正确方向。这就是我解决这个问题的方法,现在我想把我的代码贴在这里,希望能帮助别人。但首先:
口头解释
如问题中所述,设备方向更改将破坏MainActivity extends BaseGameActivity
,并随之破坏您的游戏状态(即您与 Google Play 服务的连接)。但是,我们可以将所有 GameHelper 代码放入一个“无头”片段(没有 UI 的片段)中,并setRetainInstance(true)
声明。现在,当我们MainActivity extends FragmentActivity
在方向改变时被破坏时,无头片段会停止,甚至分离,但不会被破坏!(onDestroy()
未调用)当MainActivity
被 Android 重新创建时,我们的无头片段会自动重新附加到它。此时,在我们的无头片段中,onCreate()
没有调用。我们onCreate()
连接到 GameHelper 的地方也是如此。我们可以断开与 GameHelper 的连接onDestroy()
因为这永远不会被调用,除非应用程序完成(那时,可以终止我们的连接)。
注意:我认为GameHeaderFragment.java
可能应该分解为一个抽象类和一个继承自它的游戏特定类(但我在这里没有这样做)。
这是我想出的(请原谅我的游戏特定代码交织的区域):
GameHeaderFragment.java
public class GameHelperFragment extends Fragment implements GameHelperListener, OnInvitationReceivedListener, RoomUpdateListener, RoomStatusUpdateListener, RealTimeMessageReceivedListener {
protected MainActivity mActivity = null;
// The game helper object. This class is mainly a wrapper around this object.
protected GameHelper mHelper;
final static int MAX_NUM_PLAYERS = 4;
// Request codes for the UIs that we show with startActivityForResult:
final static int RC_SELECT_PLAYERS = 10000;
final static int RC_INVITATION_INBOX = 10001;
final static int RC_WAITING_ROOM = 10002;
// We expose these constants here because we don't want users of this class
// to have to know about GameHelper at all.
public static final int CLIENT_GAMES = GameHelper.CLIENT_GAMES;
public static final int CLIENT_APPSTATE = GameHelper.CLIENT_APPSTATE;
public static final int CLIENT_PLUS = GameHelper.CLIENT_PLUS;
public static final int CLIENT_ALL = GameHelper.CLIENT_ALL;
// Requested clients. By default, that's just the games client.
protected int mRequestedClients = CLIENT_GAMES;
protected String mSigningInMessage = "Signing in with Google";
protected String mSigningOutMessage = "Signing out";
// Custom Members
String mMyId = "";
String mRoomId = "";
ArrayList<Participant> mParticipants = null;
int mCurrentlyPlayingIdx = 0; // idx into mParticipants
boolean mIsMultiplayer = false;
boolean mWaitRoomDismissedFromCode = false;
public interface GameHelperFragmentListener {
void onSignInFailed();
void onSignInSucceeded();
void onInvitationReceived(Invitation invitation);
void showMainMenu();
void showWaitScreen();
void startGame();
void participantLeftAtIdx(int idx);
void handleRealTimeMessage(RealTimeMessage rtm);
}
GameHelperFragmentListener mListener;
public GameHelperFragment() {
super();
Log.d("mab", "GHFrag.Constructor()");
}
/**
* Sets the requested clients. The preferred way to set the requested clients is
* via the constructor, but this method is available if for some reason your code
* cannot do this in the constructor. This must be called before onCreate in order to
* have any effect. If called after onCreate, this method is a no-op.
*
* @param requestedClients A combination of the flags CLIENT_GAMES, CLIENT_PLUS
* and CLIENT_APPSTATE, or CLIENT_ALL to request all available clients.
*/
protected void setRequestedClients(int requestedClients) {
mRequestedClients = requestedClients;
}
@Override
public void onAttach(Activity activity) {
Log.d("mab", this + ": onAttach(" + activity + ")");
super.onAttach(activity);
mActivity = (MainActivity) activity;
mListener = (GameHelperFragmentListener) activity;
}
@Override
public void onCreate(Bundle b) {
Log.d("mab", this + ": onCreate()");
super.onCreate(b);
setRetainInstance(true);
mHelper = new GameHelper(mActivity);
mHelper.setup(this, mRequestedClients); //'this' => GameHelperListener
mHelper.setSigningInMessage(mSigningInMessage);
mHelper.setSigningOutMessage(mSigningOutMessage);
mHelper.onStart(mActivity);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return null; // Headless Fragment
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.d("mab", this + ": onActivityCreated()");
super.onActivityCreated(savedInstanceState);
}
@Override
public void onDestroy() {
Log.d("mab", this + ": onDestroy()");
super.onDestroy();
mHelper.onStop();
}
@Override
public void onActivityResult(int requestCode, int responseCode, Intent data) {
Log.d("mab", this + ": onActivityResult(" + requestCode + ")");
super.onActivityResult(requestCode, responseCode, data);
mHelper.onActivityResult(requestCode, responseCode, data);
switch (requestCode) {
case RC_SELECT_PLAYERS:
// we got the result from the "select players" UI -- ready to create the room
handleSelectPlayersResult(responseCode, data);
break;
case RC_INVITATION_INBOX:
// we got the result from the "select invitation" UI (invitation inbox). We're
// ready to accept the selected invitation:
handleInvitationInboxResult(responseCode, data);
break;
case RC_WAITING_ROOM:
// ignore result if we dismissed the waiting room from code:
if (mWaitRoomDismissedFromCode) break;
// we got the result from the "waiting room" UI.
if (responseCode == Activity.RESULT_OK) {
} else if (responseCode == GamesActivityResultCodes.RESULT_LEFT_ROOM) {
// player actively indicated that they want to leave the room
leaveRoom();
} else if (responseCode == Activity.RESULT_CANCELED) {
leaveRoom();
}
break;
}
}
// Handle the result of the "Select players UI" we launched when the user clicked the
// "Invite friends" button. We react by creating a room with those players.
private void handleSelectPlayersResult(int responseCode, Intent data) {
if (responseCode != Activity.RESULT_OK) {
Log.w("mab", "*** select players UI cancelled, " + responseCode);
showMainMenu();
return;
}
Log.d("mab", "Select players UI succeeded.");
// get the invitee list
final ArrayList<String> invitees = data.getStringArrayListExtra(GamesClient.EXTRA_PLAYERS);
Log.d("mab", "Invitee count: " + invitees.size());
// get the automatch criteria
Bundle autoMatchCriteria = null;
int minAutoMatchPlayers = data.getIntExtra(GamesClient.EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
int maxAutoMatchPlayers = data.getIntExtra(GamesClient.EXTRA_MAX_AUTOMATCH_PLAYERS, 0);
if (minAutoMatchPlayers > 0 || maxAutoMatchPlayers > 0) {
autoMatchCriteria = RoomConfig.createAutoMatchCriteria(
minAutoMatchPlayers, maxAutoMatchPlayers, 0);
Log.d("mab", "Automatch criteria: " + autoMatchCriteria);
}
// create the room
Log.d("mab", "Creating room...");
RoomConfig.Builder rtmConfigBuilder = RoomConfig.builder(this);
rtmConfigBuilder.addPlayersToInvite(invitees);
rtmConfigBuilder.setMessageReceivedListener(this);
rtmConfigBuilder.setRoomStatusUpdateListener(this);
if (autoMatchCriteria != null) {
rtmConfigBuilder.setAutoMatchCriteria(autoMatchCriteria);
}
showWaitScreen();
keepScreenOn();
getGamesClient().createRoom(rtmConfigBuilder.build());
Log.d("mab", "Room configured, waiting for it to be created...");
}
// Handle the result of the invitation inbox UI, where the player can pick an invitation
// to accept. We react by accepting the selected invitation, if any.
private void handleInvitationInboxResult(int response, Intent data) {
if (response != Activity.RESULT_OK) {
Log.d("mab", "*** invitation inbox UI cancelled, " + response);
showMainMenu();
return;
}
Log.d("mab", "Invitation inbox UI succeeded.");
Invitation inv = data.getExtras().getParcelable(GamesClient.EXTRA_INVITATION);
// accept invitation
acceptInviteToRoom(inv.getInvitationId());
}
protected GamesClient getGamesClient() {
return mHelper.getGamesClient();
}
protected AppStateClient getAppStateClient() {
return mHelper.getAppStateClient();
}
protected PlusClient getPlusClient() {
return mHelper.getPlusClient();
}
protected boolean isSignedIn() {
return mHelper.isSignedIn();
}
protected void beginUserInitiatedSignIn() {
mHelper.beginUserInitiatedSignIn();
}
protected void signOut() {
mHelper.signOut();
}
protected void showAlert(String title, String message) {
mHelper.showAlert(title, message);
}
protected void showAlert(String message) {
mHelper.showAlert(message);
}
protected void enableDebugLog(boolean enabled, String tag) {
mHelper.enableDebugLog(enabled, tag);
}
protected String getInvitationId() {
return mHelper.getInvitationId();
}
protected void reconnectClients(int whichClients) {
mHelper.reconnectClients(whichClients);
}
protected String getScopes() {
return mHelper.getScopes();
}
protected boolean hasSignInError() {
return mHelper.hasSignInError();
}
protected ConnectionResult getSignInError() {
return mHelper.getSignInError();
}
protected void setSignInMessages(String signingInMessage, String signingOutMessage) {
mSigningInMessage = signingInMessage;
mSigningOutMessage = signingOutMessage;
}
public void setRoomId(String rid) {
mRoomId = rid;
}
public String getRoomId() {
return mRoomId;
}
@Override
public void onRealTimeMessageReceived(RealTimeMessage rtm) {
mListener.handleRealTimeMessage(rtm);
}
// Called when we are connected to the room. We're not ready to play yet! (maybe not everybody is connected yet).
@Override
public void onConnectedToRoom(Room room) {
Log.d("mab", "onConnectedToRoom.");
// get room ID, participants and my ID:
mRoomId = room.getRoomId();
mParticipants = room.getParticipants();
mMyId = room.getParticipantId(getGamesClient().getCurrentPlayerId());
// print out the list of participants (for debug purposes)
Log.d("mab", "Room ID: " + mRoomId);
Log.d("mab", "My ID " + mMyId);
Log.d("mab", "<< CONNECTED TO ROOM>>");
Log.d("mab", " Number of Joined Participants: " + getNumJoinedParticipants());
}
// Called when we get disconnected from the room. We return to the main screen.
@Override
public void onDisconnectedFromRoom(Room room) {
mIsMultiplayer = false;
mRoomId = null;
showGameError("Disconnected from room");
}
@Override
public void onJoinedRoom(int statusCode, Room room) {
Log.d("mab", "onJoinedRoom(" + statusCode + ")");
if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
if (statusCode != GamesClient.STATUS_OK) {
mIsMultiplayer = false;
Log.e("mab", "*** Error: onJoinedRoom, status " + statusCode);
showGameError("Joined room unsuccessfully: " + statusCode);
return;
}
mRoomId = room.getRoomId();
// show the waiting room UI
showWaitingRoom(room);
}
// Called when we've successfully left the room (this happens a result of voluntarily leaving
// via a call to leaveRoom(). If we get disconnected, we get onDisconnectedFromRoom()).
@Override
public void onLeftRoom(int statusCode, String roomId) {
// we have left the room; return to main screen.
Log.d("mab", "onLeftRoom, code " + statusCode);
mRoomId = null; //????? right?
showMainMenu();
}
// Called when room is fully connected.
@Override
public void onRoomConnected(int statusCode, Room room) {
Log.d("mab", "onRoomConnected(" + statusCode + ")");
if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
if (statusCode != GamesClient.STATUS_OK) {
mIsMultiplayer = false;
Log.d("mab", "*** Error: onRoomConnected, status " + statusCode);
showGameError("Roon connected unsuccessfully: " + statusCode);
return;
}
mRoomId = room.getRoomId();
mParticipants = room.getParticipants(); // not sure if we need this here again, but shouldn't hurt (or maybe we want this ONLY here)
mIsMultiplayer = true;
// Set 1st player to take a turn
mCurrentlyPlayingIdx = 0;
// Start Game!
mListener.startGame();
}
// Called when room has been created
@Override
public void onRoomCreated(int statusCode, Room room) {
Log.d("mab", "onRoomCreated(" + statusCode + ")");
if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
if (statusCode != GamesClient.STATUS_OK) {
mIsMultiplayer = false;
Log.e("mab", "*** Error: onRoomCreated, status " + statusCode);
showGameError("Room not created successfully: " + statusCode);
return;
}
mRoomId = room.getRoomId();
// show the waiting room UI
showWaitingRoom(room);
}
// Called when we get an invitation to play a game. We react by showing that to the user.
@Override
public void onInvitationReceived(Invitation invitation) {
Log.d("mab", "ghFrag.onInvitationReceived()");
mListener.onInvitationReceived(invitation);
}
@Override
public void onSignInFailed() {
mListener.onSignInFailed();
}
@Override
public void onSignInSucceeded() {
// install invitation listener so we get notified if we receive an invitation to play a game.
getGamesClient().registerInvitationListener(this);
if (getInvitationId() != null) {
acceptInviteToRoom(getInvitationId());
return;
}
mListener.onSignInSucceeded();
}
// Accept the given invitation.
void acceptInviteToRoom(String invId) {
// accept the invitation
Log.d("mab", "Accepting invitation: " + invId);
keepScreenOn();
RoomConfig.Builder roomConfigBuilder = RoomConfig.builder(this);
roomConfigBuilder.setInvitationIdToAccept(invId)
.setMessageReceivedListener(this)
.setRoomStatusUpdateListener(this);
showWaitScreen();
getGamesClient().joinRoom(roomConfigBuilder.build());
}
// Sets the flag to keep this screen on. It's recommended to do that during the handshake when setting up a game, because if the screen turns off, the game will be cancelled.
void keepScreenOn() {
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
// Clears the flag that keeps the screen on.
void stopKeepingScreenOn() {
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
public void inviteFriends() {
// show list of invitable players
Intent intent = getGamesClient().getSelectPlayersIntent(1, 3);
showWaitScreen();
startActivityForResult(intent, RC_SELECT_PLAYERS);
}
// Leave the room.
void leaveRoom() {
Log.d("mab", "Leaving room.");
mIsMultiplayer = false;
stopKeepingScreenOn();
if (mRoomId != null) {
getGamesClient().leaveRoom(this, mRoomId);
mRoomId = null;
showWaitScreen();
} else {
showMainMenu();
}
}
// Show the waiting room UI to track the progress of other players as they enter the
// room and get connected.
void showWaitingRoom(Room room) {
Log.d("mab", "GHFrag.showWaitingRoom()");
mWaitRoomDismissedFromCode = false;
int minPlayers = MAX_NUM_PLAYERS; // This just means the "Start" menu item will never be enabled (waiting room will exit automatically once everyone has made a decision)
Intent i = getGamesClient().getRealTimeWaitingRoomIntent(room, minPlayers);
// show waiting room UI
getActivity().startActivityForResult(i, RC_WAITING_ROOM);
}
// Forcibly dismiss the waiting room UI (this is useful, for example, if we realize the
// game needs to start because someone else is starting to play).
void dismissWaitingRoom() {
mWaitRoomDismissedFromCode = true;
getActivity().finishActivity(RC_WAITING_ROOM); //getActivity() ?????
}
// Show error message about game being cancelled and return to main screen.
void showGameError(String msg) {
showAlert("Error", "Game Error: " + msg);
showMainMenu();
}
private void showMainMenu() {
mListener.showMainMenu();
}
private void showWaitScreen() {
mListener.showWaitScreen();
}
}
MainActivity.java
public class MainActivity extends FragmentActivity implements MainMenuFragment.Listener, PlayFragment.Listener, GameHelperFragmentListener, AlertDialogFragmentListener {
public static final String MAIN_MENU_FRAGMENT = "MainMenuFragment";
public static final String PLAY_FRAGMENT = "PlayFragment";
public static final String WAIT_FRAGMENT = "WaitFragment";
// Fragments
MainMenuFragment mMainMenuFragment;
PlayFragment mPlayFragment;
WaitFragment mWaitFragment;
GameHelperFragment gameHelperFragment = null;
String mIncomingInvitationId = null;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d("mab", "MainActivity.onCreate()");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Add Headless Fragment (if not already retained)
gameHelperFragment = (GameHelperFragment) getSupportFragmentManager().findFragmentByTag("GameHelperFragment");
if (gameHelperFragment == null) {
Log.d("mab", this + ": Existing fragment not found.!!!");
gameHelperFragment = new GameHelperFragment();
gameHelperFragment.setSignInMessages("Signing in with Google", "Signing out");
getSupportFragmentManager().beginTransaction().add(gameHelperFragment, "GameHelperFragment").commit();
} else {
Log.d("mab", this + ": Existing fragment found.!!!");
}
}
@Override
public void onSignInFailed() {
Log.d("mab", "MainActivity.onSignInFailed()");
if (mMainMenuFragment != null) {
mMainMenuFragment.updateUi();
}
}
@Override
public void onSignInSucceeded() {
Log.d("mab", "MainActivity.onSignInSuccedded()");
if (mMainMenuFragment != null) {
mMainMenuFragment.updateUi();
}
}
@Override
public void onSignInButtonClicked() {
Log.d("mab", "MainActivity.onSignInButtonClicked()");
// start the sign-in flow
beginUserInitiatedSignIn();
}
@Override
public void onSignOutButtonClicked() {
Log.d("mab", "MainActivity.onSignOutButtonClicked()");
signOut();
if (mMainMenuFragment != null) {
mMainMenuFragment.updateUi();
}
}
@Override
public void onInvitationReceived(Invitation invitation) {
mIncomingInvitationId = invitation.getInvitationId();
// show accept/decline dialog box here.
String dispName = invitation.getInviter().getDisplayName();
DialogFragment alertInvitationReceived = AlertDialogFragment.newInstance("Invitation Received", dispName +
" is inviting you to play Yahtzee Blast.", "Accept", "Decline", null);
alertInvitationReceived.show(getSupportFragmentManager(), DLG_INVITATION_RECVD);
}
@Override
protected void onPause() {
Log.d("mab", "MainActivity.onPause()");
super.onPause();
}
@Override
protected void onStop() {
Log.d("mab", "MainActivity.onStop()");
super.onStop();
}
@Override
protected void onStart() {
Log.d("mab", "MainActivity.onStart()");
super.onStart();
}
@Override
protected void onResume() {
Log.d("mab", "MainActivity.onResume()");
super.onResume();
}
@Override
protected void onDestroy() {
Log.d("mab", "MainActivity.onDestroy()");
super.onDestroy();
mHelper = null;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("mIncomingInvitationId", mIncomingInvitationId); // ? need this ?
}
@Override
public void onInviteFriendsClicked() {
Log.d("mab", "MainActivity.onInviteFriendsClicked()");
gameHelperFragment.inviteFriends();
}
@Override
public void onSeeAllInvitationsClicked() {
Log.d("mab", "MainActivity.onSeeAllInvitationsClicked()");
gameHelperFragment.seeAllInvitations();
}
@Override
public void onActivityResult(int requestCode, int responseCode, Intent intent) {
Log.d("mab", this + ": onActivityResult(requestCode: " + requestCode + ", responseCode: " + responseCode + ")");
super.onActivityResult(requestCode, responseCode, intent);
// Call GameHelper's onActivityResult in case this result pertains to it
gameHelperFragment.onActivityResult(requestCode, responseCode, intent);
}
public void onAlertDialogFragmentPositiveClicked(String tag) {
Log.d("mab", "MainActivity.onAlertDialogFragmentPositiveClicked(" + tag + ")");
if (tag == DLG_INVITATION_RECVD) {
gameHelperFragment.acceptInviteToRoom(mIncomingInvitationId);
}
}
// Called when we receive a real-time message from the network.
public void handleRealTimeMessage(RealTimeMessage rtm) {
Log.d(TAG, "MainActivity.onRealTimeMessageReceived()");
// Handle it here...
}
// Headless Fragment Functions
private void setSignInMessages(String signingInMessage, String signingOutMessage) {
gameHelperFragment.setSignInMessages(signingInMessage, signingOutMessage);
}
private GamesClient getGamesClient() {
return gameHelperFragment.getGamesClient();
}
private String getInvitationId() {
return gameHelperFragment.getInvitationId();
}
private void beginUserInitiatedSignIn() {
gameHelperFragment.beginUserInitiatedSignIn();
}
private void signOut() {
gameHelperFragment.signOut();
}
private void showAlert(String message) {
gameHelperFragment.showAlert(message);
}
private void showAlert(String title, String message) {
gameHelperFragment.showAlert(title, message);
}
public GameHelperFragment getGameHelperFragment() {
return gameHelperFragment;
}
@Override
public void showMainMenu() {
switchToFragment(MAIN_MENU_FRAGMENT, false);
}
@Override
public void showWaitScreen() {
switchToFragment(WAIT_FRAGMENT, false);
}
@Override
public void participantLeftAtIdx(int idx) {
// Handle here, if there's anything you need to do.
}
}