diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java index 6b27c08c4a..c4cc9e1f3a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -1,5 +1,6 @@ package org.dolphinemu.dolphinemu.activities; +import android.app.Fragment; import android.content.Intent; import android.os.Bundle; import android.os.Handler; @@ -13,8 +14,12 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.LinearLayout; import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; @@ -22,6 +27,9 @@ import com.squareup.picasso.Picasso; import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.fragments.EmulationFragment; +import org.dolphinemu.dolphinemu.fragments.LoadStateFragment; +import org.dolphinemu.dolphinemu.fragments.MenuFragment; +import org.dolphinemu.dolphinemu.fragments.SaveStateFragment; import java.util.List; @@ -29,14 +37,22 @@ public final class EmulationActivity extends AppCompatActivity { private View mDecorView; private ImageView mImageView; - private FrameLayout mFrameEmulation; - private boolean mDeviceHasTouchScreen; - private boolean mSystemUiVisible; + private FrameLayout mFrameEmulation; + private LinearLayout mMenuLayout; + + private String mSubmenuFragmentTag; // So that MainActivity knows which view to invalidate before the return animation. private int mPosition; + private boolean mDeviceHasTouchScreen; + private boolean mSystemUiVisible; + private boolean mMenuVisible; + + private static Interpolator sDecelerator = new DecelerateInterpolator(); + private static Interpolator sAccelerator = new AccelerateInterpolator(); + /** * Handlers are a way to pass a message to an Activity telling it to do something * on the UI thread. This Handler responds to any message, even blank ones, by @@ -52,57 +68,70 @@ public final class EmulationActivity extends AppCompatActivity }; private String mScreenPath; private FrameLayout mFrameContent; + private String mSelectedTitle; @Override protected void onCreate(Bundle savedInstanceState) { + mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen"); + + int themeId; + if (mDeviceHasTouchScreen) + { + themeId = R.style.DolphinEmulationGamecube; + + // Get a handle to the Window containing the UI. + mDecorView = getWindow().getDecorView(); + + // Set these options now so that the SurfaceView the game renders into is the right size. + mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + + // Set the ActionBar to follow the navigation/status bar's visibility changes. + mDecorView.setOnSystemUiVisibilityChangeListener( + new View.OnSystemUiVisibilityChangeListener() + { + @Override + public void onSystemUiVisibilityChange(int flags) + { + mSystemUiVisible = (flags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; + + if (mSystemUiVisible) + { + getSupportActionBar().show(); + hideSystemUiAfterDelay(); + } + else + { + getSupportActionBar().hide(); + } + } + } + ); + } + else + { + themeId = R.style.DolphinEmulationTvGamecube; + } + + setTheme(themeId); super.onCreate(savedInstanceState); // Picasso will take a while to load these big-ass screenshots. So don't run // the animation until we say so. postponeEnterTransition(); - mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen"); - - // Get a handle to the Window containing the UI. - mDecorView = getWindow().getDecorView(); - - // Set these options now so that the SurfaceView the game renders into is the right size. - mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - - // Set the ActionBar to follow the navigation/status bar's visibility changes. - mDecorView.setOnSystemUiVisibilityChangeListener( - new View.OnSystemUiVisibilityChangeListener() - { - @Override - public void onSystemUiVisibilityChange(int flags) - { - mSystemUiVisible = (flags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; - - if (mSystemUiVisible) - { - getSupportActionBar().show(); - hideSystemUiAfterDelay(); - } - else - { - getSupportActionBar().hide(); - } - } - } - ); - setContentView(R.layout.activity_emulation); mImageView = (ImageView) findViewById(R.id.image_screenshot); mFrameContent = (FrameLayout) findViewById(R.id.frame_content); mFrameEmulation = (FrameLayout) findViewById(R.id.frame_emulation_fragment); + mMenuLayout = (LinearLayout) findViewById(R.id.layout_ingame_menu); Intent gameToEmulate = getIntent(); String path = gameToEmulate.getStringExtra("SelectedGame"); - String title = gameToEmulate.getStringExtra("SelectedTitle"); + mSelectedTitle = gameToEmulate.getStringExtra("SelectedTitle"); mScreenPath = gameToEmulate.getStringExtra("ScreenPath"); mPosition = gameToEmulate.getIntExtra("GridPosition", -1); @@ -148,8 +177,6 @@ public final class EmulationActivity extends AppCompatActivity } }); - setTitle(title); - // Instantiate an EmulationFragment. EmulationFragment emulationFragment = EmulationFragment.newInstance(path); @@ -157,6 +184,21 @@ public final class EmulationActivity extends AppCompatActivity getFragmentManager().beginTransaction() .add(R.id.frame_emulation_fragment, emulationFragment, EmulationFragment.FRAGMENT_TAG) .commit(); + + if (mDeviceHasTouchScreen) + { + setTitle(mSelectedTitle); + } + else + { + MenuFragment menuFragment = (MenuFragment) getFragmentManager() + .findFragmentById(R.id.fragment_menu); + + if (menuFragment != null) + { + menuFragment.setTitleText(mSelectedTitle); + } + } } @Override @@ -181,8 +223,11 @@ public final class EmulationActivity extends AppCompatActivity { super.onPostCreate(savedInstanceState); - // Give the user a few seconds to see what the controls look like, then hide them. - hideSystemUiAfterDelay(); + if (mDeviceHasTouchScreen) + { + // Give the user a few seconds to see what the controls look like, then hide them. + hideSystemUiAfterDelay(); + } } @Override @@ -190,36 +235,91 @@ public final class EmulationActivity extends AppCompatActivity { super.onWindowFocusChanged(hasFocus); - if (hasFocus) + if (mDeviceHasTouchScreen) { - hideSystemUiAfterDelay(); - } - else - { - // If the window loses focus (i.e. a dialog box, or a popup menu is on screen - // stop hiding the UI. - mSystemUiHider.removeMessages(0); + if (hasFocus) + { + hideSystemUiAfterDelay(); + } + else + { + // If the window loses focus (i.e. a dialog box, or a popup menu is on screen + // stop hiding the UI. + mSystemUiHider.removeMessages(0); + } } } @Override public void onBackPressed() { - if (!mDeviceHasTouchScreen && !mSystemUiVisible) + if (!mDeviceHasTouchScreen) { - showSystemUI(); + if (mSubmenuFragmentTag != null) + { + removeMenu(); + } + else + { + toggleMenu(); + } } else { - // Let the system handle it; i.e. quit the activity TODO or show "are you sure?" dialog. - EmulationFragment fragment = (EmulationFragment) getFragmentManager() - .findFragmentByTag(EmulationFragment.FRAGMENT_TAG); - fragment.notifyEmulationStopped(); - - NativeLibrary.StopEmulation(); + stopEmulation(); } } + private void toggleMenu() + { + if (mMenuVisible) + { + mMenuVisible = false; + + mMenuLayout.animate() + .withLayer() + .setDuration(200) + .setInterpolator(sAccelerator) + .alpha(0.0f) + .translationX(-400.0f) + .withEndAction(new Runnable() + { + @Override + public void run() + { + if (mMenuVisible) + { + mMenuLayout.setVisibility(View.GONE); + } + } + }); + } + else + { + mMenuVisible = true; + mMenuLayout.setVisibility(View.VISIBLE); + +// mMenuLayout.setTranslationX(-400.0f); + mMenuLayout.setAlpha(0.0f); + + mMenuLayout.animate() + .withLayer() + .setDuration(300) + .setInterpolator(sDecelerator) + .alpha(1.0f) + .translationX(0.0f); + } + } + + private void stopEmulation() + { + EmulationFragment fragment = (EmulationFragment) getFragmentManager() + .findFragmentByTag(EmulationFragment.FRAGMENT_TAG); + fragment.notifyEmulationStopped(); + + NativeLibrary.StopEmulation(); + } + public void exitWithAnimation() { runOnUiThread(new Runnable() @@ -257,7 +357,7 @@ public final class EmulationActivity extends AppCompatActivity mImageView.setVisibility(View.VISIBLE); mImageView.animate() .withLayer() - .setDuration(500) + .setDuration(100) .alpha(1.0f) .withEndAction(afterShowingScreenshot); } @@ -284,7 +384,13 @@ public final class EmulationActivity extends AppCompatActivity @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) + onMenuItemClicked(item.getItemId()); + return true; + } + + public void onMenuItemClicked(int id) + { + switch (id) { // Enable/Disable input overlay. case R.id.menu_emulation_input_overlay: @@ -294,67 +400,78 @@ public final class EmulationActivity extends AppCompatActivity emulationFragment.toggleInputOverlayVisibility(); - return true; + return; } // Screenshot capturing case R.id.menu_emulation_screenshot: NativeLibrary.SaveScreenShot(); - return true; + return; // Quicksave / Load case R.id.menu_quicksave: NativeLibrary.SaveState(9); - return true; + return; case R.id.menu_quickload: NativeLibrary.LoadState(9); - return true; + return; + + // TV Menu only + case R.id.menu_emulation_save_root: + showMenu(SaveStateFragment.FRAGMENT_ID); + return; + + case R.id.menu_emulation_load_root: + showMenu(LoadStateFragment.FRAGMENT_ID); + return; // Save state slots case R.id.menu_emulation_save_1: NativeLibrary.SaveState(0); - return true; + return; case R.id.menu_emulation_save_2: NativeLibrary.SaveState(1); - return true; + return; case R.id.menu_emulation_save_3: NativeLibrary.SaveState(2); - return true; + return; case R.id.menu_emulation_save_4: NativeLibrary.SaveState(3); - return true; + return; case R.id.menu_emulation_save_5: NativeLibrary.SaveState(4); - return true; + return; // Load state slots case R.id.menu_emulation_load_1: NativeLibrary.LoadState(0); - return true; + return; case R.id.menu_emulation_load_2: NativeLibrary.LoadState(1); - return true; + return; case R.id.menu_emulation_load_3: NativeLibrary.LoadState(2); - return true; + return; case R.id.menu_emulation_load_4: NativeLibrary.LoadState(3); - return true; + return; case R.id.menu_emulation_load_5: NativeLibrary.LoadState(4); - return true; + return; - default: - return super.onOptionsItemSelected(item); + case R.id.menu_exit: + toggleMenu(); + stopEmulation(); + return; } } @@ -362,6 +479,11 @@ public final class EmulationActivity extends AppCompatActivity @Override public boolean dispatchKeyEvent(KeyEvent event) { + if (mMenuVisible) + { + return super.dispatchKeyEvent(event); + } + int action = 0; switch (event.getAction()) @@ -391,6 +513,11 @@ public final class EmulationActivity extends AppCompatActivity @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mMenuVisible) + { + return false; + } + if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) { return super.dispatchGenericMotionEvent(event); @@ -458,4 +585,78 @@ public final class EmulationActivity extends AppCompatActivity } }); } + + private void showMenu(int menuId) + { + Fragment fragment; + + switch (menuId) + { + case SaveStateFragment.FRAGMENT_ID: + fragment = SaveStateFragment.newInstance(); + mSubmenuFragmentTag = SaveStateFragment.FRAGMENT_TAG; + break; + + case LoadStateFragment.FRAGMENT_ID: + fragment = LoadStateFragment.newInstance(); + mSubmenuFragmentTag = LoadStateFragment.FRAGMENT_TAG; + break; + + default: + return; + } + + getFragmentManager().beginTransaction() + .setCustomAnimations(R.animator.menu_slide_in, R.animator.menu_slide_out) + .replace(R.id.frame_submenu, fragment, mSubmenuFragmentTag) + .commit(); + } + + private void removeMenu() + { + if (mSubmenuFragmentTag != null) + { + final Fragment fragment = getFragmentManager().findFragmentByTag(mSubmenuFragmentTag); + + if (fragment != null) + { + // When removing a fragment without replacement, its aniimation must be done + // manually beforehand. + fragment.getView().animate() + .withLayer() + .setDuration(200) + .setInterpolator(sAccelerator) + .alpha(0.0f) + .translationX(600.0f) + .withEndAction(new Runnable() + { + @Override + public void run() + { + if (mMenuVisible) + { + getFragmentManager().beginTransaction() + .remove(fragment) + .commit(); + } + } + }); + } + else + { + Log.e("DolphinEmu", "[EmulationActivity] Fragment not found, can't remove."); + } + + mSubmenuFragmentTag = null; + } + else + { + Log.e("DolphinEmu", "[EmulationActivity] Fragment Tag empty."); + } + } + + public String getSelectedTitle() + { + return mSelectedTitle; + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java index 9d23736cb6..1eb1684531 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java @@ -81,9 +81,12 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C mSurfaceView.getHolder().addCallback(this); // If the input overlay was previously disabled, then don't show it. - if (!mPreferences.getBoolean("showInputOverlay", true)) + if (mInputOverlay != null) { - mInputOverlay.setVisibility(View.GONE); + if (!mPreferences.getBoolean("showInputOverlay", true)) + { + mInputOverlay.setVisibility(View.GONE); + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/LoadStateFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/LoadStateFragment.java new file mode 100644 index 0000000000..bbd9807b9b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/LoadStateFragment.java @@ -0,0 +1,55 @@ +package org.dolphinemu.dolphinemu.fragments; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.GridLayout; + +import org.dolphinemu.dolphinemu.BuildConfig; +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.activities.EmulationActivity; + +public final class LoadStateFragment extends Fragment implements View.OnClickListener +{ + public static final String FRAGMENT_TAG = BuildConfig.APPLICATION_ID + ".load_state"; + public static final int FRAGMENT_ID = R.layout.fragment_state_load; + + public static LoadStateFragment newInstance() + { + LoadStateFragment fragment = new LoadStateFragment(); + + // TODO Add any appropriate arguments to this fragment. + + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + View rootView = inflater.inflate(FRAGMENT_ID, container, false); + + GridLayout grid = (GridLayout) rootView.findViewById(R.id.grid_state_slots); + for (int childIndex = 0; childIndex < grid.getChildCount(); childIndex++) + { + Button button = (Button) grid.getChildAt(childIndex); + + button.setOnClickListener(this); + } + + // So that item clicked to start this Fragment is no longer the focused item. + grid.requestFocus(); + + return rootView; + } + + @Override + public void onClick(View button) + { + ((EmulationActivity) getActivity()).onMenuItemClicked(button.getId()); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java new file mode 100644 index 0000000000..d894f32373 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java @@ -0,0 +1,52 @@ +package org.dolphinemu.dolphinemu.fragments; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.dolphinemu.dolphinemu.BuildConfig; +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.activities.EmulationActivity; + +public final class MenuFragment extends Fragment implements View.OnClickListener +{ + public static final String FRAGMENT_TAG = BuildConfig.APPLICATION_ID + ".ingame_menu"; + public static final int FRAGMENT_ID = R.layout.fragment_ingame_menu; + private TextView mTitleText; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + View rootView = inflater.inflate(FRAGMENT_ID, container, false); + + LinearLayout options = (LinearLayout) rootView.findViewById(R.id.layout_options); + for (int childIndex = 0; childIndex < options.getChildCount(); childIndex++) + { + Button button = (Button) options.getChildAt(childIndex); + + button.setOnClickListener(this); + } + + mTitleText = (TextView) rootView.findViewById(R.id.text_game_title); + + return rootView; + } + + @Override + public void onClick(View button) + { + ((EmulationActivity) getActivity()).onMenuItemClicked(button.getId()); + } + + public void setTitleText(String title) + { + mTitleText.setText(title); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveStateFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveStateFragment.java new file mode 100644 index 0000000000..a5e9e6d441 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveStateFragment.java @@ -0,0 +1,55 @@ +package org.dolphinemu.dolphinemu.fragments; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.GridLayout; + +import org.dolphinemu.dolphinemu.BuildConfig; +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.activities.EmulationActivity; + +public final class SaveStateFragment extends Fragment implements View.OnClickListener +{ + public static final String FRAGMENT_TAG = BuildConfig.APPLICATION_ID + ".save_state"; + public static final int FRAGMENT_ID = R.layout.fragment_state_save; + + public static SaveStateFragment newInstance() + { + SaveStateFragment fragment = new SaveStateFragment(); + + // TODO Add any appropriate arguments to this fragment. + + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + View rootView = inflater.inflate(FRAGMENT_ID, container, false); + + GridLayout grid = (GridLayout) rootView.findViewById(R.id.grid_state_slots); + for (int childIndex = 0; childIndex < grid.getChildCount(); childIndex++) + { + Button button = (Button) grid.getChildAt(childIndex); + + button.setOnClickListener(this); + } + + // So that item clicked to start this Fragment is no longer the focused item. + grid.requestFocus(); + + return rootView; + } + + @Override + public void onClick(View button) + { + ((EmulationActivity) getActivity()).onMenuItemClicked(button.getId()); + } +} diff --git a/Source/Android/app/src/main/res/animator/menu_slide_in.xml b/Source/Android/app/src/main/res/animator/menu_slide_in.xml new file mode 100644 index 0000000000..e6f9ae6d8e --- /dev/null +++ b/Source/Android/app/src/main/res/animator/menu_slide_in.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/Source/Android/app/src/main/res/animator/menu_slide_out.xml b/Source/Android/app/src/main/res/animator/menu_slide_out.xml new file mode 100644 index 0000000000..85c2556e48 --- /dev/null +++ b/Source/Android/app/src/main/res/animator/menu_slide_out.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/Source/Android/app/src/main/res/layout-television/activity_emulation.xml b/Source/Android/app/src/main/res/layout-television/activity_emulation.xml new file mode 100644 index 0000000000..06fe747c39 --- /dev/null +++ b/Source/Android/app/src/main/res/layout-television/activity_emulation.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Android/app/src/main/res/layout-television/fragment_emulation.xml b/Source/Android/app/src/main/res/layout-television/fragment_emulation.xml new file mode 100644 index 0000000000..787de65186 --- /dev/null +++ b/Source/Android/app/src/main/res/layout-television/fragment_emulation.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml new file mode 100644 index 0000000000..44093dc324 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml @@ -0,0 +1,69 @@ + + + + + + + + + +