From a908e1e4667073e9c19f3e797f11819f4439d3dd Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Fri, 11 Dec 2015 17:09:44 +0100 Subject: [PATCH] Implemented shared element transitions for addons Removed getting details in AddonDetailsFragment as the AddonListFragment already provides all information required in the AddonDetailsFragment. Therefore, we do not need to retrieve the same information in the AddonDetailsFragment as well. --- .../xbmc/kore/ui/AddonDetailsFragment.java | 143 ++++++++---------- .../org/xbmc/kore/ui/AddonListFragment.java | 39 +++-- .../java/org/xbmc/kore/ui/AddonsActivity.java | 75 +++++++-- app/src/main/res/values-sw600dp/dimens.xml | 2 +- app/src/main/res/values/dimens.xml | 2 +- 5 files changed, 158 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/org/xbmc/kore/ui/AddonDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/AddonDetailsFragment.java index dbb7ea3..0496647 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AddonDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AddonDetailsFragment.java @@ -15,6 +15,7 @@ */ package org.xbmc.kore.ui; +import android.annotation.TargetApi; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Bundle; @@ -39,9 +40,9 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.method.Addons; -import org.xbmc.kore.jsonrpc.type.AddonType; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; +import org.xbmc.kore.utils.Utils; import butterknife.ButterKnife; import butterknife.InjectView; @@ -53,7 +54,15 @@ import butterknife.OnClick; public class AddonDetailsFragment extends Fragment { private static final String TAG = LogUtils.makeLogTag(AddonDetailsFragment.class); - public static final String ADDONID = "addon_id"; + public static final String BUNDLE_KEY_ADDONID = "addon_id"; + public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; + public static final String BUNDLE_KEY_NAME = "name"; + public static final String BUNDLE_KEY_AUTHOR = "author"; + public static final String BUNDLE_KEY_SUMMARY = "summary"; + public static final String BUNDLE_KEY_VERSION = "version"; + public static final String BUNDLE_KEY_DESCRIPTION = "description"; + public static final String BUNDLE_KEY_FANART = "fanart"; + public static final String BUNDLE_KEY_POSTER = "poster"; private HostManager hostManager; private HostInfo hostInfo; @@ -87,11 +96,23 @@ public class AddonDetailsFragment extends Fragment { /** * Create a new instance of this, initialized to show the addon addonId */ - public static AddonDetailsFragment newInstance(String addonId) { + @TargetApi(21) + public static AddonDetailsFragment newInstance(AddonListFragment.ViewHolder vh) { AddonDetailsFragment fragment = new AddonDetailsFragment(); Bundle args = new Bundle(); - args.putString(ADDONID, addonId); + args.putString(BUNDLE_KEY_ADDONID, vh.addonId); + args.putString(BUNDLE_KEY_NAME, vh.addonName); + args.putString(BUNDLE_KEY_AUTHOR, vh.author); + args.putString(BUNDLE_KEY_VERSION, vh.version); + args.putString(BUNDLE_KEY_SUMMARY, vh.summary); + args.putString(BUNDLE_KEY_DESCRIPTION, vh.description); + args.putString(BUNDLE_KEY_FANART, vh.fanart); + args.putString(BUNDLE_KEY_POSTER, vh.poster); + + if( Utils.isLollipopOrLater()) { + args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName()); + } fragment.setArguments(args); return fragment; } @@ -101,9 +122,11 @@ public class AddonDetailsFragment extends Fragment { super.onCreate(savedInstanceState); } + @TargetApi(21) @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - addonId = getArguments().getString(ADDONID, null); + Bundle bundle = getArguments(); + addonId = bundle.getString(BUNDLE_KEY_ADDONID, null); if ((container == null) || (addonId == null)) { // We're not being shown or there's nothing to show @@ -131,6 +154,18 @@ public class AddonDetailsFragment extends Fragment { FloatingActionButton fab = (FloatingActionButton)fabButton; fab.attachToScrollView((ObservableScrollView) mediaPanel); + if(Utils.isLollipopOrLater()) { + mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME)); + } + + mediaTitle.setText(bundle.getString(BUNDLE_KEY_NAME)); + mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_SUMMARY)); + mediaAuthor.setText(bundle.getString(BUNDLE_KEY_AUTHOR)); + mediaVersion.setText(bundle.getString(BUNDLE_KEY_VERSION)); + mediaDescription.setText(bundle.getString(BUNDLE_KEY_DESCRIPTION)); + + setImages(bundle.getString(BUNDLE_KEY_POSTER), bundle.getString(BUNDLE_KEY_FANART)); + // Pad main content view to overlap with bottom system bar // UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true); // mediaPanel.setClipToPadding(false); @@ -142,31 +177,6 @@ public class AddonDetailsFragment extends Fragment { public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(false); - - // Get the addon details, this is done asyhnchronously - String[] properties = new String[] { - AddonType.Fields.NAME, AddonType.Fields.VERSION, AddonType.Fields.SUMMARY, - AddonType.Fields.DESCRIPTION, AddonType.Fields.PATH, AddonType.Fields.AUTHOR, - AddonType.Fields.THUMBNAIL, AddonType.Fields.DISCLAIMER, AddonType.Fields.FANART, - //AddonType.Fields.DEPENDENCIES, AddonType.Fields.BROKEN, AddonType.Fields.EXTRAINFO, - AddonType.Fields.RATING, AddonType.Fields.ENABLED - }; - Addons.GetAddonDetails action = new Addons.GetAddonDetails(addonId, properties); - action.execute(hostManager.getConnection(), new ApiCallback() { - @Override - public void onSuccess(AddonType.Details result) { - if (!isAdded()) return; - displayAddonDetails(result); - } - - @Override - public void onError(int errorCode, String description) { - if (!isAdded()) return; - Toast.makeText(getActivity(), - String.format(getString(R.string.error_getting_addon_info), description), - Toast.LENGTH_SHORT).show(); - } - }, callbackHandler); } @Override @@ -225,70 +235,39 @@ public class AddonDetailsFragment extends Fragment { public void onError(int errorCode, String description) { if (!isAdded()) return; Toast.makeText(getActivity(), - String.format(getString(R.string.general_error_executing_action), description), - Toast.LENGTH_SHORT) - .show(); + String.format(getString(R.string.general_error_executing_action), description), + Toast.LENGTH_SHORT) + .show(); } }, callbackHandler); } - /** - * Display the addon details - * - * @param addonDetails Addon details - */ - private void displayAddonDetails(AddonType.Details addonDetails) { - mediaTitle.setText(addonDetails.name); - mediaUndertitle.setText(addonDetails.summary); - - mediaAuthor.setText(addonDetails.author); - mediaVersion.setText(addonDetails.version); - - mediaDescription.setText(addonDetails.description); - - // Images + private void setImages(String poster, String fanart) { Resources resources = getActivity().getResources(); DisplayMetrics displayMetrics = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height), artWidth = displayMetrics.widthPixels; - if (!TextUtils.isEmpty(addonDetails.fanart)) { - int posterWidth = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_width); - int posterHeight = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_heigth); - mediaPoster.setVisibility(View.VISIBLE); - UIUtils.loadImageIntoImageview(hostManager, - addonDetails.thumbnail, - mediaPoster, posterWidth, posterHeight); - UIUtils.loadImageIntoImageview(hostManager, - addonDetails.fanart, - mediaArt, artWidth, artHeight); - } else { - // No fanart, just present the poster - mediaPoster.setVisibility(View.GONE); - UIUtils.loadImageIntoImageview(hostManager, - addonDetails.thumbnail, - mediaArt, artWidth, artHeight); - // Reset padding - int paddingLeft = mediaTitle.getPaddingRight(), - paddingRight = mediaTitle.getPaddingRight(), - paddingTop = mediaTitle.getPaddingTop(), - paddingBottom = mediaTitle.getPaddingBottom(); - mediaTitle.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); - mediaUndertitle.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); - } + int posterWidth = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_width); + int posterHeight = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_height); + + UIUtils.loadImageIntoImageview(hostManager, + TextUtils.isEmpty(fanart)? poster : fanart, + mediaArt, artWidth, artHeight); + UIUtils.loadImageIntoImageview(hostManager, + poster, + mediaPoster, posterWidth, posterHeight); - setupEnableButton(addonDetails.enabled); } private void setupEnableButton(boolean enabled) { // Enabled button if (enabled) { Resources.Theme theme = getActivity().getTheme(); - TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { + TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{ R.attr.colorAccent}); enabledButton.setColorFilter(styledAttributes.getColor(0, - getActivity().getResources().getColor(R.color.accent_default))); + getActivity().getResources().getColor(R.color.accent_default))); styledAttributes.recycle(); fabButton.setVisibility(View.VISIBLE); @@ -298,4 +277,16 @@ public class AddonDetailsFragment extends Fragment { } enabledButton.setTag(enabled); } + + /** + * Returns the shared element if visible + * @return View if visible, null otherwise + */ + public View getSharedElement() { + if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) { + return mediaPoster; + } + + return null; + } } diff --git a/app/src/main/java/org/xbmc/kore/ui/AddonListFragment.java b/app/src/main/java/org/xbmc/kore/ui/AddonListFragment.java index 7c6a331..5f1bbc5 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AddonListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AddonListFragment.java @@ -15,6 +15,7 @@ */ package org.xbmc.kore.ui; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.res.Resources; @@ -39,6 +40,7 @@ import org.xbmc.kore.jsonrpc.method.Addons; import org.xbmc.kore.jsonrpc.type.AddonType; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; +import org.xbmc.kore.utils.Utils; import java.util.List; @@ -53,7 +55,7 @@ public class AddonListFragment extends Fragment private static final String TAG = LogUtils.makeLogTag(AddonListFragment.class); public interface OnAddonSelectedListener { - public void onAddonSelected(String addonId, String addonTitle); + public void onAddonSelected(ViewHolder vh); } // Activity listener @@ -99,7 +101,7 @@ public class AddonListFragment extends Fragment // Get the movie id from the tag ViewHolder tag = (ViewHolder) view.getTag(); // Notify the activity - listenerActivity.onAddonSelected(tag.addonId, tag.addonName); + listenerActivity.onAddonSelected(tag); } }); @@ -210,8 +212,8 @@ public class AddonListFragment extends Fragment // To prevent the empty text from appearing on the first load, set it now emptyView.setText(getString(R.string.no_addons_found_refresh)); Toast.makeText(getActivity(), - String.format(getString(R.string.error_getting_addon_info), description), - Toast.LENGTH_SHORT).show(); + String.format(getString(R.string.error_getting_addon_info), description), + Toast.LENGTH_SHORT).show(); swipeRefreshLayout.setRefreshing(false); } }, callbackHandler); @@ -230,13 +232,12 @@ public class AddonListFragment extends Fragment // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when // the user transitions to that fragment, avoiding another call and imediatelly showing the image Resources resources = context.getResources(); - artWidth = (int)(resources.getDimension(R.dimen.addondetail_poster_width) / - UIUtils.IMAGE_RESIZE_FACTOR); - artHeight = (int)(resources.getDimension(R.dimen.addondetail_poster_heigth) / - UIUtils.IMAGE_RESIZE_FACTOR); + artWidth = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_width);; + artHeight = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_height);; } /** {@inheritDoc} */ + @TargetApi(21) @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { @@ -257,13 +258,23 @@ public class AddonListFragment extends Fragment // Save the movie id viewHolder.addonId = addonDetails.addonid; viewHolder.addonName = addonDetails.name; + viewHolder.author = addonDetails.author; + viewHolder.description = addonDetails.description; + viewHolder.summary = addonDetails.summary; + viewHolder.version = addonDetails.version; + viewHolder.fanart = addonDetails.fanart; + viewHolder.poster = addonDetails.thumbnail; viewHolder.titleView.setText(viewHolder.addonName); viewHolder.detailsView.setText(addonDetails.summary); UIUtils.loadImageWithCharacterAvatar(getContext(), hostManager, - addonDetails.thumbnail, viewHolder.addonName, - viewHolder.artView, artWidth, artHeight); + addonDetails.thumbnail, viewHolder.addonName, + viewHolder.artView, artWidth, artHeight); + + if(Utils.isLollipopOrLater()) { + viewHolder.artView.setTransitionName("a"+viewHolder.addonId); + } return convertView; } } @@ -271,12 +282,18 @@ public class AddonListFragment extends Fragment /** * View holder pattern */ - private static class ViewHolder { + public static class ViewHolder { TextView titleView; TextView detailsView; ImageView artView; String addonId; String addonName; + String summary; + String author; + String version; + String description; + String fanart; + String poster; } } diff --git a/app/src/main/java/org/xbmc/kore/ui/AddonsActivity.java b/app/src/main/java/org/xbmc/kore/ui/AddonsActivity.java index 306e7e6..5bd3fc6 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AddonsActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/AddonsActivity.java @@ -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 Addons information (list, details) * All the information is presented by specific fragments @@ -47,6 +52,8 @@ public class AddonsActivity extends BaseActivity private NavigationDrawerFragment navigationDrawerFragment; + private boolean clearSharedElements; + @TargetApi(21) @Override protected void onCreate(Bundle savedInstanceState) { @@ -67,10 +74,26 @@ public class AddonsActivity extends BaseActivity // Setup animations if (Utils.isLollipopOrLater()) { - addonListFragment.setExitTransition(null); - addonListFragment.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); + addonListFragment.setExitTransition(fade); + addonListFragment.setReenterTransition(fade); + addonListFragment.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 names, Map sharedElements) { + if (clearSharedElements) { + names.clear(); + sharedElements.clear(); + clearSharedElements = false; + } + } + }; + addonListFragment.setExitSharedElementCallback(seCallback); } getSupportFragmentManager() .beginTransaction() @@ -175,32 +198,56 @@ public class AddonsActivity extends BaseActivity /** * Callback from list fragment when a addon is selected. * Switch fragment in portrait - * @param addonId Addon selected - * @param addonTitle Title + * @param vh */ @TargetApi(21) - public void onAddonSelected(String addonId, String addonTitle) { - selectedAddonId = addonId; - selectedAddonTitle = addonTitle; + public void onAddonSelected(AddonListFragment.ViewHolder vh) { + selectedAddonId = vh.addonId; + selectedAddonTitle = vh.addonName; // Replace list fragment - AddonDetailsFragment addonDetailsFragment = AddonDetailsFragment.newInstance(addonId); + final AddonDetailsFragment addonDetailsFragment = AddonDetailsFragment.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 names, Map 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 (addonDetailsFragment.isVisible()) { + View sharedView = addonDetailsFragment.getSharedElement(); + if (sharedView == null) { // shared element not visible + clearSharedElements = true; + } + } + } + }; + addonDetailsFragment.setEnterSharedElementCallback(seCallback); + addonDetailsFragment.setEnterTransition(TransitionInflater - .from(this) - .inflateTransition(R.transition.media_details)); + .from(this) + .inflateTransition(R.transition.media_details)); addonDetailsFragment.setReturnTransition(null); + + Transition changeImageTransition = TransitionInflater.from( + this).inflateTransition(R.transition.change_image); + addonDetailsFragment.setSharedElementReturnTransition(changeImageTransition); + addonDetailsFragment.setSharedElementEnterTransition(changeImageTransition); + + fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName()); } else { fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, - R.anim.fragment_list_popenter, 0); + R.anim.fragment_list_popenter, 0); } fragTrans.replace(R.id.fragment_container, addonDetailsFragment) - .addToBackStack(null) - .commit(); + .addToBackStack(null) + .commit(); setupActionBar(selectedAddonTitle); } diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml index 012545d..ea7d9e8 100644 --- a/app/src/main/res/values-sw600dp/dimens.xml +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -69,7 +69,7 @@ 88dp 146dp - 146dp + 146dp 88dp 88dp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 875f988..fbc7249 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -114,7 +114,7 @@ 74dp 112dp - 112dp + 112dp 52dp 76dp