Implemented shared element transition for TV shows

Added SharedElementCallback to be able to detect if shared element
is still visible. If not, the shared element transition should not
be performed when returning to the previous fragment.
Added the pager tab strip to the fade animation to keep shared element
transition smooth when poster is partly below the toolbar.
This commit is contained in:
Martijn Brekhof 2015-12-08 14:29:15 +01:00
parent 9b4c4d6315
commit 17c7609d72
6 changed files with 207 additions and 67 deletions

View File

@ -15,6 +15,7 @@
*/
package org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
@ -23,9 +24,12 @@ import android.view.View;
import android.view.ViewGroup;
import com.astuetz.PagerSlidingTabStrip;
import org.xbmc.kore.R;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import butterknife.ButterKnife;
import butterknife.InjectView;
@ -36,22 +40,45 @@ import butterknife.InjectView;
public class TVShowDetailsFragment extends Fragment {
private static final String TAG = LogUtils.makeLogTag(TVShowDetailsFragment.class);
public static final String TVSHOWID = "tvshow_id";
public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
public static final String BUNDLE_KEY_TVSHOWID = "tvshow_id";
public static final String BUNDLE_KEY_TITLE = "title";
public static final String BUNDLE_KEY_PREMIERED = "premiered";
public static final String BUNDLE_KEY_STUDIO = "studio";
public static final String BUNDLE_KEY_EPISODE = "episode";
public static final String BUNDLE_KEY_WATCHEDEPISODES = "watchedepisodes";
public static final String BUNDLE_KEY_RATING = "rating";
public static final String BUNDLE_KEY_PLOT = "plot";
public static final String BUNDLE_KEY_GENRES = "genres";
// Displayed movie id
private int tvshowId = -1;
private TabsAdapter tabsAdapter;
@InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip;
@InjectView(R.id.pager) ViewPager viewPager;
/**
* Create a new instance of this, initialized to show tvshowId
*/
public static TVShowDetailsFragment newInstance(int tvshowId) {
@TargetApi(21)
public static TVShowDetailsFragment newInstance(TVShowListFragment.ViewHolder vh) {
TVShowDetailsFragment fragment = new TVShowDetailsFragment();
Bundle args = new Bundle();
args.putInt(TVSHOWID, tvshowId);
args.putInt(BUNDLE_KEY_TVSHOWID, vh.tvshowId);
args.putInt(BUNDLE_KEY_EPISODE, vh.episode);
args.putString(BUNDLE_KEY_GENRES, vh.genres);
args.putString(BUNDLE_KEY_PLOT, vh.plot);
args.putString(BUNDLE_KEY_PREMIERED, vh.premiered);
args.putDouble(BUNDLE_KEY_RATING, vh.rating);
args.putString(BUNDLE_KEY_STUDIO, vh.studio);
args.putString(BUNDLE_KEY_TITLE, vh.tvshowTitle);
args.putInt(BUNDLE_KEY_WATCHEDEPISODES, vh.watchedEpisodes);
if( Utils.isLollipopOrLater()) {
args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@ -63,7 +90,7 @@ public class TVShowDetailsFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
tvshowId = getArguments().getInt(TVSHOWID, -1);
tvshowId = getArguments().getInt(BUNDLE_KEY_TVSHOWID, -1);
if ((container == null) || (tvshowId == -1)) {
// We're not being shown or there's nothing to show
@ -74,7 +101,7 @@ public class TVShowDetailsFragment extends Fragment {
ButterKnife.inject(this, root);
long baseFragmentId = tvshowId * 10;
TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager())
tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager())
.addTab(TVShowOverviewFragment.class, getArguments(), R.string.tvshow_overview,
baseFragmentId)
.addTab(TVShowEpisodeListFragment.class, getArguments(),
@ -92,19 +119,26 @@ public class TVShowDetailsFragment extends Fragment {
setHasOptionsMenu(false);
}
@Override
public void onResume() {
super.onResume();
public Fragment getCurrentTabFragment() {
return tabsAdapter.getItem(viewPager.getCurrentItem());
}
@Override
public void onPause() {
super.onPause();
}
public View getSharedElement() {
View view = getView();
if (view == null)
return null;
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// outState.putInt(TVSHOWID, tvshowId);
//Note: this works as R.id.poster is only used in TVShowOverviewFragment.
//If the same id is used in other fragments in the TabsAdapter we
//need to check which fragment is currently displayed
View artView = view.findViewById(R.id.poster);
View scrollView = view.findViewById(R.id.media_panel);
if (( artView != null ) &&
( scrollView != null ) &&
UIUtils.isViewInBounds(scrollView, artView)) {
return artView;
}
return null;
}
}

View File

@ -15,6 +15,7 @@
*/
package org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
@ -47,6 +48,7 @@ import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.service.LibrarySyncService;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
/**
* Fragment that presents the tv show list
@ -55,7 +57,7 @@ public class TVShowListFragment extends AbstractListFragment {
private static final String TAG = LogUtils.makeLogTag(TVShowListFragment.class);
public interface OnTVShowSelectedListener {
public void onTVShowSelected(int tvshowId, String tvshowTitle);
public void onTVShowSelected(TVShowListFragment.ViewHolder vh);
}
// Activity listener
@ -72,7 +74,7 @@ public class TVShowListFragment extends AbstractListFragment {
// Get the movie id from the tag
ViewHolder tag = (ViewHolder) view.getTag();
// Notify the activity
listenerActivity.onTVShowSelected(tag.tvshowId, tag.tvshowTitle);
listenerActivity.onTVShowSelected(tag);
}
};
}
@ -225,9 +227,16 @@ public class TVShowListFragment extends AbstractListFragment {
MediaContract.TVShows.TVSHOWID,
MediaContract.TVShows.TITLE,
MediaContract.TVShows.THUMBNAIL,
MediaContract.TVShows.FANART,
MediaContract.TVShows.PREMIERED,
MediaContract.TVShows.STUDIO,
MediaContract.TVShows.EPISODE,
MediaContract.TVShows.WATCHEDEPISODES,
MediaContract.TVShows.RATING,
MediaContract.TVShows.PLOT,
MediaContract.TVShows.PLAYCOUNT,
MediaContract.TVShows.IMDBNUMBER,
MediaContract.TVShows.GENRES,
};
String SORT_BY_NAME = MediaContract.TVShows.TITLE + " ASC";
@ -238,9 +247,16 @@ public class TVShowListFragment extends AbstractListFragment {
final int TVSHOWID = 1;
final int TITLE = 2;
final int THUMBNAIL = 3;
final int PREMIERED = 4;
final int EPISODE = 5;
final int WATCHEDEPISODES = 6;
final int FANART = 4;
final int PREMIERED = 5;
final int STUDIO = 6;
final int EPISODE = 7;
final int WATCHEDEPISODES = 8;
final int RATING = 9;
final int PLOT = 10;
final int PLAYCOUNT = 11;
final int IMDBNUMBER = 12;
final int GENRES = 13;
}
private static class TVShowsAdapter extends CursorAdapter {
@ -257,16 +273,16 @@ public class TVShowListFragment extends AbstractListFragment {
// the user transitions to that fragment, avoiding another call and imediatelly showing the image
Resources resources = context.getResources();
artWidth = (int)(resources.getDimension(R.dimen.now_playing_poster_width) /
UIUtils.IMAGE_RESIZE_FACTOR);
UIUtils.IMAGE_RESIZE_FACTOR);
artHeight = (int)(resources.getDimension(R.dimen.now_playing_poster_height) /
UIUtils.IMAGE_RESIZE_FACTOR);
UIUtils.IMAGE_RESIZE_FACTOR);
}
/** {@inheritDoc} */
@Override
public View newView(Context context, final Cursor cursor, ViewGroup parent) {
final View view = LayoutInflater.from(context)
.inflate(R.layout.grid_item_tvshow, parent, false);
.inflate(R.layout.grid_item_tvshow, parent, false);
// Setup View holder pattern
ViewHolder viewHolder = new ViewHolder();
@ -281,6 +297,7 @@ public class TVShowListFragment extends AbstractListFragment {
}
/** {@inheritDoc} */
@TargetApi(21)
@Override
public void bindView(View view, Context context, Cursor cursor) {
final ViewHolder viewHolder = (ViewHolder)view.getTag();
@ -288,16 +305,25 @@ public class TVShowListFragment extends AbstractListFragment {
// Save the movie id
viewHolder.tvshowId = cursor.getInt(TVShowListQuery.TVSHOWID);
viewHolder.tvshowTitle = cursor.getString(TVShowListQuery.TITLE);
viewHolder.episode = cursor.getInt(TVShowListQuery.EPISODE);
viewHolder.genres = cursor.getString(TVShowListQuery.GENRES);
viewHolder.plot = cursor.getString(TVShowListQuery.PLOT);
viewHolder.premiered = cursor.getString(TVShowListQuery.PREMIERED);
viewHolder.rating = cursor.getInt(TVShowListQuery.RATING);
viewHolder.studio = cursor.getString(TVShowListQuery.STUDIO);
viewHolder.watchedEpisodes = cursor.getInt(TVShowListQuery.WATCHEDEPISODES);
if(Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a"+viewHolder.tvshowId);
}
viewHolder.titleView.setText(viewHolder.tvshowTitle);
int numEpisodes = cursor.getInt(TVShowListQuery.EPISODE),
watchedEpisodes = cursor.getInt(TVShowListQuery.WATCHEDEPISODES);
String details = String.format(context.getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes);
viewHolder.episode, viewHolder.episode - viewHolder.watchedEpisodes);
viewHolder.detailsView.setText(details);
String premiered = String.format(context.getString(R.string.premiered),
cursor.getString(TVShowListQuery.PREMIERED));
viewHolder.premiered);
viewHolder.premieredView.setText(premiered);
UIUtils.loadImageWithCharacterAvatar(context, hostManager,
cursor.getString(TVShowListQuery.THUMBNAIL), viewHolder.tvshowTitle,
@ -308,14 +334,21 @@ public class TVShowListFragment extends AbstractListFragment {
/**
* View holder pattern
*/
private static class ViewHolder {
public static class ViewHolder {
TextView titleView;
TextView detailsView;
// TextView yearView;
// TextView yearView;
TextView premieredView;
ImageView artView;
int tvshowId;
String tvshowTitle;
String premiered;
String studio;
int episode;
int watchedEpisodes;
double rating;
String plot;
String genres;
}
}

View File

@ -15,6 +15,7 @@
*/
package org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
@ -42,6 +43,7 @@ import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.service.LibrarySyncService;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.util.ArrayList;
@ -105,8 +107,10 @@ public class TVShowOverviewFragment extends AbstractDetailsFragment
}
@Override
@TargetApi(21)
protected View createView(LayoutInflater inflater, ViewGroup container) {
tvshowId = getArguments().getInt(TVSHOWID, -1);
Bundle bundle = getArguments();
tvshowId = bundle.getInt(TVShowDetailsFragment.BUNDLE_KEY_TVSHOWID, -1);
if (tvshowId == -1) {
// There's nothing to show
@ -130,6 +134,18 @@ public class TVShowOverviewFragment extends AbstractDetailsFragment
}
});
tvshowTitle = bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_TITLE);
mediaTitle.setText(tvshowTitle);
setMediaUndertitle(bundle.getInt(TVShowDetailsFragment.BUNDLE_KEY_EPISODE), bundle.getInt(TVShowDetailsFragment.BUNDLE_KEY_WATCHEDEPISODES));
setMediaPremiered(bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_PREMIERED), bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_STUDIO));
mediaGenres.setText(bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_GENRES));
setMediaRating(bundle.getDouble(TVShowDetailsFragment.BUNDLE_KEY_RATING));
mediaDescription.setText(bundle.getString(TVShowDetailsFragment.BUNDLE_KEY_PLOT));
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(getArguments().getString(TVShowDetailsFragment.POSTER_TRANS_NAME));
}
// Pad main content view to overlap with bottom system bar
// UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true);
// mediaPanel.setClipToPadding(false);
@ -244,26 +260,13 @@ public class TVShowOverviewFragment extends AbstractDetailsFragment
mediaTitle.setText(tvshowTitle);
int numEpisodes = cursor.getInt(TVShowDetailsQuery.EPISODE),
watchedEpisodes = cursor.getInt(TVShowDetailsQuery.WATCHEDEPISODES);
String episodes = String.format(getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes);
mediaUndertitle.setText(episodes);
setMediaUndertitle(numEpisodes, watchedEpisodes);
setMediaPremiered(cursor.getString(TVShowDetailsQuery.PREMIERED), cursor.getString(TVShowDetailsQuery.STUDIO));
String premiered = String.format(getString(R.string.premiered),
cursor.getString(TVShowDetailsQuery.PREMIERED)) +
" | " + cursor.getString(TVShowDetailsQuery.STUDIO);
mediaPremiered.setText(premiered);
mediaGenres.setText(cursor.getString(TVShowDetailsQuery.GENRES));
double rating = cursor.getDouble(TVShowDetailsQuery.RATING);
if (rating > 0) {
mediaRating.setVisibility(View.VISIBLE);
mediaMaxRating.setVisibility(View.VISIBLE);
mediaRating.setText(String.format("%01.01f", rating));
mediaMaxRating.setText(getString(R.string.max_rating_video));
} else {
mediaRating.setVisibility(View.INVISIBLE);
mediaMaxRating.setVisibility(View.INVISIBLE);
}
setMediaRating(cursor.getDouble(TVShowDetailsQuery.RATING));
mediaDescription.setText(cursor.getString(TVShowDetailsQuery.PLOT));
@ -286,6 +289,28 @@ public class TVShowOverviewFragment extends AbstractDetailsFragment
mediaArt, displayMetrics.widthPixels, artHeight);
}
private void setMediaUndertitle(int numEpisodes, int watchedEpisodes) {
String episodes = String.format(getString(R.string.num_episodes),
numEpisodes, numEpisodes - watchedEpisodes);
mediaUndertitle.setText(episodes);
}
private void setMediaPremiered(String premiered, String studio) {
mediaPremiered.setText(String.format(getString(R.string.premiered),
premiered) + " | " + studio);
}
private void setMediaRating(double rating) {
if (rating > 0) {
mediaRating.setVisibility(View.VISIBLE);
mediaMaxRating.setVisibility(View.VISIBLE);
mediaRating.setText(String.format("%01.01f", rating));
mediaMaxRating.setText(getString(R.string.max_rating_video));
} else {
mediaRating.setVisibility(View.INVISIBLE);
mediaMaxRating.setVisibility(View.INVISIBLE);
}
}
/**
* Display the cast details
*

View File

@ -22,15 +22,20 @@ import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import org.xbmc.kore.R;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.Utils;
import java.util.List;
import java.util.Map;
/**
* Controls the presentation of TV Shows information (list, details)
* All the information is presented by specific fragments
@ -48,6 +53,9 @@ public class TVShowsActivity extends BaseActivity
private String selectedTVShowTitle = null;
private int selectedEpisodeId = -1;
private TVShowDetailsFragment tvshowDetailsFragment;
private boolean clearSharedElements;
private NavigationDrawerFragment navigationDrawerFragment;
@TargetApi(21)
@ -70,10 +78,27 @@ public class TVShowsActivity extends BaseActivity
// Setup animations
if (Utils.isLollipopOrLater()) {
tvshowListFragment.setExitTransition(null);
tvshowListFragment.setReenterTransition(TransitionInflater
//Fade added to prevent shared element from disappearing very shortly at the start of the transition.
Transition fade = TransitionInflater
.from(this)
.inflateTransition(android.R.transition.fade));
.inflateTransition(android.R.transition.fade);
tvshowListFragment.setExitTransition(fade);
tvshowListFragment.setReenterTransition(fade);
tvshowListFragment.setSharedElementReturnTransition(TransitionInflater.from(
this).inflateTransition(R.transition.change_image));
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (clearSharedElements) {
names.clear();
sharedElements.clear();
clearSharedElements = false;
}
}
};
tvshowListFragment.setExitSharedElementCallback(seCallback);
}
getSupportFragmentManager()
.beginTransaction()
@ -93,16 +118,6 @@ public class TVShowsActivity extends BaseActivity
// UIUtils.setPaddingForSystemBars(this, findViewById(R.id.drawer_layout), true, true, true);
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
protected void onSaveInstanceState (Bundle outState) {
super.onSaveInstanceState(outState);
@ -192,24 +207,46 @@ public class TVShowsActivity extends BaseActivity
/**
* Callback from tvshows list fragment when a show is selected.
* Switch fragment in portrait
* @param tvshowId Selected show
* @param tvshowTitle Title
* @param vh
*/
@TargetApi(21)
public void onTVShowSelected(int tvshowId, String tvshowTitle) {
selectedTVShowId = tvshowId;
selectedTVShowTitle = tvshowTitle;
public void onTVShowSelected(TVShowListFragment.ViewHolder vh) {
selectedTVShowId = vh.tvshowId;
selectedTVShowTitle = vh.tvshowTitle;
// Replace list fragment
TVShowDetailsFragment tvshowDetailsFragment = TVShowDetailsFragment.newInstance(tvshowId);
tvshowDetailsFragment = TVShowDetailsFragment.newInstance(vh);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Set up transitions
if (Utils.isLollipopOrLater()) {
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
//On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements
// for the reentering fragment. We use this to determine if we are returning and if
// we should clear the shared element lists. Note that, clearing must be done in the reentering fragment
// as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (tvshowDetailsFragment.isVisible()) {
View sharedView = tvshowDetailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
clearSharedElements = true;
}
}
}
};
tvshowDetailsFragment.setEnterSharedElementCallback(seCallback);
tvshowDetailsFragment.setEnterTransition(TransitionInflater
.from(this)
.inflateTransition(R.transition.media_details));
tvshowDetailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from(
this).inflateTransition(R.transition.change_image);
tvshowDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
tvshowDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName());
} else {
fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0,
R.anim.fragment_list_popenter, 0);

View File

@ -22,6 +22,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
@ -462,4 +463,13 @@ public class UIUtils {
}
});
}
/**
* Returns true if {@param view} is contained within {@param container}'s bounds.
*/
public static boolean isViewInBounds(@NonNull View container, @NonNull View view) {
Rect containerBounds = new Rect();
container.getHitRect(containerBounds);
return view.getLocalVisibleRect(containerBounds);
}
}

View File

@ -18,6 +18,7 @@
<fade>
<targets>
<target android:targetId="@id/art"/>
<target android:targetId="@id/pager_tab_strip"/>
</targets>
</fade>