Merge pull request #2664 from sigmabeta/android-load-animation

Android: Show transition animations on game start / end.
This commit is contained in:
Ryan Houdek 2015-06-30 22:19:25 -05:00
commit 605ce3788d
9 changed files with 220 additions and 23 deletions

View File

@ -277,7 +277,7 @@ public final class NativeLibrary
public static void endEmulationActivity() public static void endEmulationActivity()
{ {
Log.v("DolphinEmu", "Ending EmulationActivity."); Log.v("DolphinEmu", "Ending EmulationActivity.");
mEmulationActivity.finish(); mEmulationActivity.exitWithAnimation();
} }
public static void setEmulationActivity(EmulationActivity emulationActivity) public static void setEmulationActivity(EmulationActivity emulationActivity)

View File

@ -12,6 +12,12 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
@ -22,10 +28,15 @@ import java.util.List;
public final class EmulationActivity extends AppCompatActivity public final class EmulationActivity extends AppCompatActivity
{ {
private View mDecorView; private View mDecorView;
private ImageView mImageView;
private FrameLayout mFrameEmulation;
private boolean mDeviceHasTouchScreen; private boolean mDeviceHasTouchScreen;
private boolean mSystemUiVisible; private boolean mSystemUiVisible;
// So that MainActivity knows which view to invalidate before the return animation.
private int mPosition;
/** /**
* Handlers are a way to pass a message to an Activity telling it to do something * 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 * on the UI thread. This Handler responds to any message, even blank ones, by
@ -39,12 +50,18 @@ public final class EmulationActivity extends AppCompatActivity
hideSystemUI(); hideSystemUI();
} }
}; };
private String mScreenPath;
private FrameLayout mFrameContent;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
super.onCreate(savedInstanceState); 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"); mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen");
// Get a handle to the Window containing the UI. // Get a handle to the Window containing the UI.
@ -79,9 +96,57 @@ public final class EmulationActivity extends AppCompatActivity
setContentView(R.layout.activity_emulation); 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);
Intent gameToEmulate = getIntent(); Intent gameToEmulate = getIntent();
String path = gameToEmulate.getStringExtra("SelectedGame"); String path = gameToEmulate.getStringExtra("SelectedGame");
String title = gameToEmulate.getStringExtra("SelectedTitle"); String title = gameToEmulate.getStringExtra("SelectedTitle");
mScreenPath = gameToEmulate.getStringExtra("ScreenPath");
mPosition = gameToEmulate.getIntExtra("GridPosition", -1);
Picasso.with(this)
.load(mScreenPath)
.noFade()
.noPlaceholder()
.into(mImageView, new Callback()
{
@Override
public void onSuccess()
{
scheduleStartPostponedTransition(mImageView);
}
@Override
public void onError()
{
// Still have to do this, or else the app will crash.
scheduleStartPostponedTransition(mImageView);
}
});
mImageView.animate()
.withLayer()
.setStartDelay(2000)
.setDuration(500)
.alpha(0.0f)
.withStartAction(new Runnable()
{
@Override
public void run()
{
mFrameEmulation.setVisibility(View.VISIBLE);
}
})
.withEndAction(new Runnable()
{
@Override
public void run()
{
mImageView.setVisibility(View.GONE);
}
});
setTitle(title); setTitle(title);
@ -90,7 +155,7 @@ public final class EmulationActivity extends AppCompatActivity
// Add fragment to the activity - this triggers all its lifecycle callbacks. // Add fragment to the activity - this triggers all its lifecycle callbacks.
getFragmentManager().beginTransaction() getFragmentManager().beginTransaction()
.add(R.id.frame_content, emulationFragment, EmulationFragment.FRAGMENT_TAG) .add(R.id.frame_emulation_fragment, emulationFragment, EmulationFragment.FRAGMENT_TAG)
.commit(); .commit();
} }
@ -155,6 +220,59 @@ public final class EmulationActivity extends AppCompatActivity
} }
} }
public void exitWithAnimation()
{
runOnUiThread(new Runnable()
{
@Override
public void run()
{
Picasso.with(EmulationActivity.this)
.invalidate(mScreenPath);
Picasso.with(EmulationActivity.this)
.load(mScreenPath)
.noFade()
.noPlaceholder()
.into(mImageView, new Callback()
{
@Override
public void onSuccess()
{
showScreenshot();
}
@Override
public void onError()
{
finish();
}
});
}
});
}
private void showScreenshot()
{
mImageView.setVisibility(View.VISIBLE);
mImageView.animate()
.withLayer()
.setDuration(500)
.alpha(1.0f)
.withEndAction(afterShowingScreenshot);
}
private Runnable afterShowingScreenshot = new Runnable()
{
@Override
public void run()
{
mFrameContent.removeView(mFrameEmulation);
setResult(mPosition);
finishAfterTransition();
}
};
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) public boolean onCreateOptionsMenu(Menu menu)
{ {
@ -324,4 +442,20 @@ public final class EmulationActivity extends AppCompatActivity
hideSystemUiAfterDelay(); hideSystemUiAfterDelay();
} }
private void scheduleStartPostponedTransition(final View sharedElement)
{
sharedElement.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener()
{
@Override
public boolean onPreDraw()
{
sharedElement.getViewTreeObserver().removeOnPreDrawListener(this);
startPostponedEnterTransition();
return true;
}
});
}
} }

View File

@ -36,6 +36,7 @@ import org.dolphinemu.dolphinemu.services.AssetCopyService;
public final class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> public final class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>
{ {
private static final int REQUEST_ADD_DIRECTORY = 1; private static final int REQUEST_ADD_DIRECTORY = 1;
public static final int REQUEST_EMULATE_GAME = 2;
/** /**
* It is important to keep track of loader ID separately from platform ID (see Game.java) * It is important to keep track of loader ID separately from platform ID (see Game.java)
@ -115,15 +116,29 @@ public final class MainActivity extends AppCompatActivity implements LoaderManag
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) protected void onActivityResult(int requestCode, int resultCode, Intent result)
{ {
// If the user picked a file, as opposed to just backing out. switch (requestCode)
if (resultCode == RESULT_OK)
{ {
// Sanity check to make sure the Activity that just returned was the AddDirectoryActivity; case REQUEST_ADD_DIRECTORY:
// other activities might use this callback in the future (don't forget to change Javadoc!) // If the user picked a file, as opposed to just backing out.
if (requestCode == REQUEST_ADD_DIRECTORY) if (resultCode == RESULT_OK)
{ {
refreshFragment(); // Sanity check to make sure the Activity that just returned was the AddDirectoryActivity;
} // other activities might use this callback in the future (don't forget to change Javadoc!)
if (requestCode == REQUEST_ADD_DIRECTORY)
{
refreshFragment();
}
}
break;
case REQUEST_EMULATE_GAME:
// Invalidate Picasso image so that the new screenshot is animated in.
PlatformGamesFragment fragment = getPlatformFragment(mViewPager.getCurrentItem());
if (fragment != null)
{
fragment.refreshScreenshotAtPosition(resultCode);
}
} }
} }

View File

@ -1,9 +1,11 @@
package org.dolphinemu.dolphinemu.adapters; package org.dolphinemu.dolphinemu.adapters;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityOptions;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.database.DataSetObserver; import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.Rect; import android.graphics.Rect;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log; import android.util.Log;
@ -15,6 +17,7 @@ import com.squareup.picasso.Picasso;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.EmulationActivity; import org.dolphinemu.dolphinemu.activities.EmulationActivity;
import org.dolphinemu.dolphinemu.activities.MainActivity;
import org.dolphinemu.dolphinemu.dialogs.GameDetailsDialog; import org.dolphinemu.dolphinemu.dialogs.GameDetailsDialog;
import org.dolphinemu.dolphinemu.model.GameDatabase; import org.dolphinemu.dolphinemu.model.GameDatabase;
import org.dolphinemu.dolphinemu.viewholders.GameViewHolder; import org.dolphinemu.dolphinemu.viewholders.GameViewHolder;
@ -80,14 +83,15 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
if (mCursor.moveToPosition(position)) if (mCursor.moveToPosition(position))
{ {
String screenPath = mCursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH); String screenPath = mCursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH);
Picasso.with(holder.imageScreenshot.getContext())
.invalidate(screenPath);
// Fill in the view contents. // Fill in the view contents.
Picasso.with(holder.imageScreenshot.getContext()) Picasso.with(holder.imageScreenshot.getContext())
.load(screenPath) .load(screenPath)
.fit() .fit()
.centerCrop() .centerCrop()
.noFade()
.noPlaceholder()
.config(Bitmap.Config.RGB_565)
.error(R.drawable.no_banner) .error(R.drawable.no_banner)
.into(holder.imageScreenshot); .into(holder.imageScreenshot);
@ -112,8 +116,6 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
{ {
Log.e("DolphinEmu", "Can't bind view; dataset is not valid."); Log.e("DolphinEmu", "Can't bind view; dataset is not valid.");
} }
} }
/** /**
@ -220,8 +222,17 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
intent.putExtra("SelectedGame", holder.path); intent.putExtra("SelectedGame", holder.path);
intent.putExtra("SelectedTitle", holder.title); intent.putExtra("SelectedTitle", holder.title);
intent.putExtra("ScreenPath", holder.screenshotPath);
intent.putExtra("GridPosition", holder.getAdapterPosition());
view.getContext().startActivity(intent); ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
(Activity) view.getContext(),
holder.imageScreenshot,
"image_game_screenshot");
((Activity) view.getContext()).startActivityForResult(intent,
MainActivity.REQUEST_EMULATE_GAME,
options.toBundle());
} }
/** /**

View File

@ -1,12 +1,12 @@
package org.dolphinemu.dolphinemu.fragments; package org.dolphinemu.dolphinemu.fragments;
import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.app.LoaderManager; import android.app.LoaderManager;
import android.content.Loader; import android.content.Loader;
import android.database.Cursor; import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log; import android.util.Log;
@ -57,6 +57,15 @@ public class PlatformGamesFragment extends Fragment
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(),
getResources().getInteger(R.integer.game_grid_columns)); getResources().getInteger(R.integer.game_grid_columns));
recyclerView.setLayoutManager(layoutManager); recyclerView.setLayoutManager(layoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator()
{
@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY)
{
dispatchChangeFinished(newHolder, false);
return true;
}
});
recyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8)); recyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8));
@ -70,10 +79,9 @@ public class PlatformGamesFragment extends Fragment
return rootView; return rootView;
} }
@Override public void refreshScreenshotAtPosition(int position)
public void onAttach(Activity activity)
{ {
super.onAttach(activity); mAdapter.notifyItemChanged(position);
} }
public void refresh() public void refresh()

View File

@ -1,5 +1,19 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/frame_content"> android:id="@+id/frame_content"
>
<FrameLayout
android:id="@+id/frame_emulation_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/image_screenshot"
android:transitionName="image_game_screenshot"/>
</FrameLayout> </FrameLayout>

View File

@ -20,7 +20,7 @@
android:id="@+id/image_game_screen" android:id="@+id/image_game_screen"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:transitionName="image_game_screen" android:transitionName="image_game_screenshot"
android:layout_weight="1" android:layout_weight="1"
tools:src="@drawable/placeholder_screenshot" tools:src="@drawable/placeholder_screenshot"
tools:scaleType="centerCrop"/> tools:scaleType="centerCrop"/>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<transitionSet>
<changeImageTransform/>
</transitionSet>

View File

@ -7,6 +7,11 @@
<item name="colorPrimary">@color/dolphin_blue</item> <item name="colorPrimary">@color/dolphin_blue</item>
<!-- darker variant for the status bar and contextual app bars --> <!-- darker variant for the status bar and contextual app bars -->
<item name="colorPrimaryDark">@color/dolphin_blue_dark</item> <item name="colorPrimaryDark">@color/dolphin_blue_dark</item>
<!--enable window content transitions-->
<item name="android:windowContentTransitions">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item>
</style> </style>
<!-- Same as above, but use default action bar, and mandate margins. --> <!-- Same as above, but use default action bar, and mandate margins. -->
@ -62,10 +67,17 @@
<item name="colorAccent">@color/dolphin_accent_wiiware</item> <item name="colorAccent">@color/dolphin_accent_wiiware</item>
</style> </style>
<style name="DolphinEmulationBase" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="DolphinEmulationBase" parent="Theme.AppCompat">
<item name="colorPrimary">@color/dolphin_blue</item> <item name="colorPrimary">@color/dolphin_blue</item>
<item name="colorPrimaryDark">@color/dolphin_blue_dark</item> <item name="colorPrimaryDark">@color/dolphin_blue_dark</item>
<item name="android:windowTranslucentNavigation">true</item> <item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowBackground">@android:color/black</item>
<!--enable window content transitions-->
<item name="android:windowContentTransitions">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item>
</style> </style>
<!-- Inherit from the Base Dolphin Emulation Theme--> <!-- Inherit from the Base Dolphin Emulation Theme-->
@ -81,7 +93,6 @@
<item name="colorAccent">@color/dolphin_accent_wiiware</item> <item name="colorAccent">@color/dolphin_accent_wiiware</item>
</style> </style>
<!-- Hax to make Tablayout render icons --> <!-- Hax to make Tablayout render icons -->
<style name="MyCustomTextAppearance" parent="TextAppearance.Design.Tab"> <style name="MyCustomTextAppearance" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</item> <item name="textAllCaps">false</item>