diff --git a/catalog/java/io/material/catalog/feature/DemoActivity.java b/catalog/java/io/material/catalog/feature/DemoActivity.java index 99328764e..a80cff1eb 100644 --- a/catalog/java/io/material/catalog/feature/DemoActivity.java +++ b/catalog/java/io/material/catalog/feature/DemoActivity.java @@ -18,8 +18,11 @@ package io.material.catalog.feature; import io.material.catalog.R; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; @@ -28,6 +31,9 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.transition.MaterialContainerTransform; +import com.google.android.material.transition.MaterialContainerTransformSharedElementCallback; import dagger.android.AndroidInjection; import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; @@ -40,6 +46,11 @@ public abstract class DemoActivity extends AppCompatActivity implements HasAndro public static final String EXTRA_DEMO_TITLE = "demo_title"; + static final String EXTRA_TRANSITION_NAME = "EXTRA_TRANSITION_NAME"; + + private static final long DURATION_ENTER = 300; + private static final long DURATION_RETURN = 275; + private Toolbar toolbar; private ViewGroup demoContainer; @@ -47,6 +58,15 @@ public abstract class DemoActivity extends AppCompatActivity implements HasAndro @Override protected void onCreate(@Nullable Bundle bundle) { + String transitionName = getIntent().getStringExtra(EXTRA_TRANSITION_NAME); + + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && transitionName != null) { + findViewById(android.R.id.content).setTransitionName(transitionName); + setEnterSharedElementCallback(new MaterialContainerTransformSharedElementCallback()); + getWindow().setSharedElementEnterTransition(buildContainerTransform(DURATION_ENTER)); + getWindow().setSharedElementReturnTransition(buildContainerTransform(DURATION_RETURN)); + } + safeInject(); super.onCreate(bundle); WindowPreferencesManager windowPreferencesManager = new WindowPreferencesManager(this); @@ -96,6 +116,17 @@ public abstract class DemoActivity extends AppCompatActivity implements HasAndro } } + @RequiresApi(VERSION_CODES.LOLLIPOP) + private MaterialContainerTransform buildContainerTransform(long duration) { + MaterialContainerTransform transform = new MaterialContainerTransform(); + transform.setDuration(duration); + transform.addTarget(android.R.id.content); + transform.setContainerColor( + MaterialColors.getColor(findViewById(android.R.id.content), R.attr.colorSurface)); + transform.setFadeMode(MaterialContainerTransform.FADE_MODE_THROUGH); + return transform; + } + private void initDemoActionBar() { if (shouldShowDefaultDemoActionBar()) { setSupportActionBar(toolbar); diff --git a/catalog/java/io/material/catalog/feature/DemoFragment.java b/catalog/java/io/material/catalog/feature/DemoFragment.java index a4d26242c..68f1797ee 100644 --- a/catalog/java/io/material/catalog/feature/DemoFragment.java +++ b/catalog/java/io/material/catalog/feature/DemoFragment.java @@ -26,6 +26,7 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.core.view.ViewCompat; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; @@ -86,6 +87,12 @@ public abstract class DemoFragment extends Fragment View view = layoutInflater.inflate(R.layout.cat_demo_fragment, viewGroup, false /* attachToRoot */); + Bundle arguments = getArguments(); + if (arguments != null) { + String transitionName = arguments.getString(FeatureDemoUtils.ARG_TRANSITION_NAME); + ViewCompat.setTransitionName(view, transitionName); + } + toolbar = view.findViewById(R.id.toolbar); // show a memory widget on Kitkat if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { @@ -180,9 +187,12 @@ public abstract class DemoFragment extends Fragment private final FragmentActivity activity = getActivity(); - private final Runnable listener = () -> activity.runOnUiThread(() -> { - memoryWidget.refreshMemStats(Runtime.getRuntime()); - }); + private final Runnable listener = + () -> + activity.runOnUiThread( + () -> { + memoryWidget.refreshMemStats(Runtime.getRuntime()); + }); private boolean memoryWidgetShown; diff --git a/catalog/java/io/material/catalog/feature/DemoLandingFragment.java b/catalog/java/io/material/catalog/feature/DemoLandingFragment.java index e4adbb1ae..1435a5d0f 100644 --- a/catalog/java/io/material/catalog/feature/DemoLandingFragment.java +++ b/catalog/java/io/material/catalog/feature/DemoLandingFragment.java @@ -18,10 +18,13 @@ package io.material.catalog.feature; import io.material.catalog.R; +import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.TypedArray; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import androidx.annotation.ArrayRes; import androidx.annotation.ColorInt; @@ -29,8 +32,10 @@ import androidx.annotation.DimenRes; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.core.view.MarginLayoutParamsCompat; import androidx.core.view.MenuItemCompat; +import androidx.core.view.ViewCompat; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import android.view.LayoutInflater; @@ -42,6 +47,7 @@ import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.widget.TextView; import com.google.android.material.resources.MaterialResources; +import com.google.android.material.transition.MaterialContainerTransformSharedElementCallback; import dagger.android.support.DaggerFragment; import java.util.Collections; import java.util.List; @@ -68,6 +74,12 @@ public abstract class DemoLandingFragment extends DaggerFragment { layoutInflater.inflate( R.layout.cat_demo_landing_fragment, viewGroup, false /* attachToRoot */); + Bundle arguments = getArguments(); + if (arguments != null) { + String transitionName = arguments.getString(FeatureDemoUtils.ARG_TRANSITION_NAME); + ViewCompat.setTransitionName(view, transitionName); + } + Toolbar toolbar = view.findViewById(R.id.toolbar); AppCompatActivity activity = (AppCompatActivity) getActivity(); @@ -145,7 +157,9 @@ public abstract class DemoLandingFragment extends DaggerFragment { TextView titleTextView = demoView.findViewById(R.id.cat_demo_landing_row_title); TextView subtitleTextView = demoView.findViewById(R.id.cat_demo_landing_row_subtitle); - rootView.setOnClickListener(v -> startDemo(demo)); + String transitionName = getString(demo.getTitleResId()); + ViewCompat.setTransitionName(rootView, transitionName); + rootView.setOnClickListener(v -> startDemo(v, demo, transitionName)); titleTextView.setText(demo.getTitleResId()); subtitleTextView.setText(getDemoClassName(demo)); @@ -169,26 +183,41 @@ public abstract class DemoLandingFragment extends DaggerFragment { } } - private void startDemo(Demo demo) { + private void startDemo(View sharedElement, Demo demo, String transitionName) { if (demo.createFragment() != null) { - startDemoFragment(demo.createFragment()); + startDemoFragment(sharedElement, demo.createFragment(), transitionName); } else if (demo.createActivityIntent() != null) { - startDemoActivity(demo.createActivityIntent()); + startDemoActivity(sharedElement, demo.createActivityIntent(), transitionName); } else { throw new IllegalStateException("Demo must implement createFragment or createActivityIntent"); } } - private void startDemoFragment(Fragment fragment) { + private void startDemoFragment(View sharedElement, Fragment fragment, String transitionName) { Bundle args = new Bundle(); args.putString(DemoFragment.ARG_DEMO_TITLE, getString(getTitleResId())); + args.putString(FeatureDemoUtils.ARG_TRANSITION_NAME, transitionName); fragment.setArguments(args); - FeatureDemoUtils.startFragment(getActivity(), fragment, FRAGMENT_DEMO_CONTENT); + FeatureDemoUtils.startFragment( + getActivity(), fragment, FRAGMENT_DEMO_CONTENT, sharedElement, transitionName); } - private void startDemoActivity(Intent intent) { + private void startDemoActivity(View sharedElement, Intent intent, String transitionName) { intent.putExtra(DemoActivity.EXTRA_DEMO_TITLE, getString(getTitleResId())); - startActivity(intent); + intent.putExtra(DemoActivity.EXTRA_TRANSITION_NAME, transitionName); + + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + // Set up shared element transition and disable overlay so views don't show above system bars + FragmentActivity activity = getActivity(); + activity.setExitSharedElementCallback(new MaterialContainerTransformSharedElementCallback()); + activity.getWindow().setSharedElementsUseOverlay(false); + + ActivityOptions options = + ActivityOptions.makeSceneTransitionAnimation(activity, sharedElement, transitionName); + startActivity(intent, options.toBundle()); + } else { + startActivity(intent); + } } private void setMarginStart(View view, @DimenRes int marginResId) { @@ -231,9 +260,9 @@ public abstract class DemoLandingFragment extends DaggerFragment { /** * Whether or not the feature shown by this fragment should be flagged as restricted. * - * Examples of restricted feature could be features which depends on an API level that is - * greater than MDCs min sdk version. If overriding this method, you should also override - * {@link #getRestrictedMessageId()} and provide information about why the feature is restricted. + *
Examples of restricted feature could be features which depends on an API level that is + * greater than MDCs min sdk version. If overriding this method, you should also override {@link + * #getRestrictedMessageId()} and provide information about why the feature is restricted. */ public boolean isRestricted() { return false; @@ -242,10 +271,10 @@ public abstract class DemoLandingFragment extends DaggerFragment { /** * The message to display if a feature {@link #isRestricted()}. * - * This message should provide insight into why the feature is restricted for the device it - * is running on. This message will be displayed in the description area of the demo fragment - * instead of the the provided {@link #getDescriptionResId()}. Additionally, all demos, both the - * main demo and any additional demos will not be shown. + *
This message should provide insight into why the feature is restricted for the device it is + * running on. This message will be displayed in the description area of the demo fragment instead + * of the the provided {@link #getDescriptionResId()}. Additionally, all demos, both the main demo + * and any additional demos will not be shown. */ @StringRes public int getRestrictedMessageId() { diff --git a/catalog/java/io/material/catalog/feature/FeatureDemoUtils.java b/catalog/java/io/material/catalog/feature/FeatureDemoUtils.java index 4cd32f977..a32bb30d1 100644 --- a/catalog/java/io/material/catalog/feature/FeatureDemoUtils.java +++ b/catalog/java/io/material/catalog/feature/FeatureDemoUtils.java @@ -20,25 +20,76 @@ import io.material.catalog.R; import android.content.Context; import android.content.SharedPreferences; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; import android.preference.PreferenceManager; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentTransaction; +import android.view.View; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.transition.Hold; +import com.google.android.material.transition.MaterialContainerTransform; /** Utils for feature demos. */ public abstract class FeatureDemoUtils { + static final String ARG_TRANSITION_NAME = "ARG_TRANSITION_NAME"; + private static final int MAIN_ACTIVITY_FRAGMENT_CONTAINER_ID = R.id.container; private static final String DEFAULT_CATALOG_DEMO = "default_catalog_demo"; public static void startFragment(FragmentActivity activity, Fragment fragment, String tag) { - activity - .getSupportFragmentManager() - .beginTransaction() - .setCustomAnimations( - R.anim.abc_grow_fade_in_from_bottom, - R.anim.abc_fade_out, - R.anim.abc_fade_in, - R.anim.abc_shrink_fade_out_from_bottom) + startFragmentInternal(activity, fragment, tag, null, null); + } + + public static void startFragment( + FragmentActivity activity, + Fragment fragment, + String tag, + View sharedElement, + String sharedElementName) { + startFragmentInternal(activity, fragment, tag, sharedElement, sharedElementName); + } + + public static void startFragmentInternal( + FragmentActivity activity, + Fragment fragment, + String tag, + @Nullable View sharedElement, + @Nullable String sharedElementName) { + FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction(); + + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP + && sharedElement != null + && sharedElementName != null) { + Fragment currentFragment = getCurrentFragment(activity); + currentFragment.setExitTransition(new Hold()); + + MaterialContainerTransform transform = new MaterialContainerTransform(); + transform.setContainerColor(MaterialColors.getColor(sharedElement, R.attr.colorSurface)); + transform.setFadeMode(MaterialContainerTransform.FADE_MODE_THROUGH); + fragment.setSharedElementEnterTransition(transform); + transaction.addSharedElement(sharedElement, sharedElementName); + + if (fragment.getArguments() == null) { + Bundle args = new Bundle(); + args.putString(ARG_TRANSITION_NAME, sharedElementName); + fragment.setArguments(args); + } else { + fragment.getArguments().putString(ARG_TRANSITION_NAME, sharedElementName); + } + } else { + transaction.setCustomAnimations( + R.anim.abc_grow_fade_in_from_bottom, + R.anim.abc_fade_out, + R.anim.abc_fade_in, + R.anim.abc_shrink_fade_out_from_bottom); + } + + transaction .replace(MAIN_ACTIVITY_FRAGMENT_CONTAINER_ID, fragment, tag) .addToBackStack(null /* name */) .commit(); diff --git a/catalog/java/io/material/catalog/tableofcontents/TocViewHolder.java b/catalog/java/io/material/catalog/tableofcontents/TocViewHolder.java index 0b5728bb2..f307b6f58 100644 --- a/catalog/java/io/material/catalog/tableofcontents/TocViewHolder.java +++ b/catalog/java/io/material/catalog/tableofcontents/TocViewHolder.java @@ -19,6 +19,7 @@ package io.material.catalog.tableofcontents; import io.material.catalog.R; import androidx.fragment.app.FragmentActivity; +import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView.ViewHolder; import android.view.LayoutInflater; import android.view.View; @@ -40,6 +41,7 @@ class TocViewHolder extends ViewHolder { private FragmentActivity activity; private FeatureDemo featureDemo; + private String transitionName; TocViewHolder(FragmentActivity activity, ViewGroup viewGroup) { super( @@ -54,7 +56,9 @@ class TocViewHolder extends ViewHolder { void bind(FragmentActivity activity, FeatureDemo featureDemo) { this.activity = activity; this.featureDemo = featureDemo; + this.transitionName = activity.getString(featureDemo.getTitleResId()); + ViewCompat.setTransitionName(itemView, transitionName); titleView.setText(featureDemo.getTitleResId()); imageView.setImageResource(featureDemo.getDrawableResId()); itemView.setOnClickListener(clickListener); @@ -63,5 +67,7 @@ class TocViewHolder extends ViewHolder { } private final OnClickListener clickListener = - v -> FeatureDemoUtils.startFragment(activity, featureDemo.createFragment(), FRAGMENT_CONTENT); + v -> + FeatureDemoUtils.startFragment( + activity, featureDemo.createFragment(), FRAGMENT_CONTENT, v, transitionName); }