diff --git a/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt b/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt index fd01b6c60..0f1d095c0 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.sunflower import android.content.Intent +import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -26,13 +27,21 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.app.ShareCompat +import androidx.core.view.ViewCompat import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import androidx.transition.Fade +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.google.android.material.snackbar.Snackbar import com.google.samples.apps.sunflower.databinding.FragmentPlantDetailBinding +import com.google.samples.apps.sunflower.utilities.AnimUtils import com.google.samples.apps.sunflower.utilities.InjectorUtils +import com.google.samples.apps.sunflower.utilities.MoveViews import com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModel /** @@ -61,6 +70,9 @@ class PlantDetailFragment : Fragment() { plantDetailViewModel.addPlantToGarden() Snackbar.make(view, R.string.added_plant_to_garden, Snackbar.LENGTH_LONG).show() } + + ViewCompat.setTransitionName(detailImage, plantId) + requestListener = imageListener } plantDetailViewModel.plant.observe(this, Observer { plant -> @@ -71,6 +83,9 @@ class PlantDetailFragment : Fragment() { } }) + postponeEnterTransition() // wait for Glide callback to start transition + setupTransition() + setHasOptionsMenu(true) return binding.root @@ -105,4 +120,49 @@ class PlantDetailFragment : Fragment() { else -> super.onOptionsItemSelected(item) } } + + val imageListener = object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + startPostponedEnterTransition() + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + startPostponedEnterTransition() + return false + } + } + + private fun setupTransition() { + // Animations when List entering Detail + sharedElementEnterTransition = MoveViews().apply { + interpolator = AnimUtils.getFastOutSlowInInterpolator() + duration = resources.getInteger(R.integer.config_duration_area_large_expand).toLong() + } + enterTransition = Fade().apply { + interpolator = AnimUtils.getLinearOutSlowInInterpolator() + startDelay = resources.getInteger(R.integer.config_duration_area_large_expand).toLong() + } + + // Animations when Detail retuning to List + sharedElementReturnTransition = MoveViews().apply { + interpolator = AnimUtils.getFastOutSlowInInterpolator() + duration = resources.getInteger(R.integer.config_duration_area_large_collapse).toLong() + } + returnTransition = Fade().apply { + interpolator = AnimUtils.getFastOutLinearInInterpolator() + duration = resources.getInteger(R.integer.config_duration_area_small).toLong() + } + } } diff --git a/app/src/main/java/com/google/samples/apps/sunflower/PlantListFragment.kt b/app/src/main/java/com/google/samples/apps/sunflower/PlantListFragment.kt index 208d25aed..187f30af8 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/PlantListFragment.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/PlantListFragment.kt @@ -23,11 +23,14 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.core.view.doOnLayout import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import androidx.transition.Fade import com.google.samples.apps.sunflower.adapters.PlantAdapter import com.google.samples.apps.sunflower.databinding.FragmentPlantListBinding +import com.google.samples.apps.sunflower.utilities.AnimUtils import com.google.samples.apps.sunflower.utilities.InjectorUtils import com.google.samples.apps.sunflower.viewmodels.PlantListViewModel @@ -50,6 +53,13 @@ class PlantListFragment : Fragment() { binding.plantList.adapter = adapter subscribeUi(adapter) + // wait RecyclerView to layout for detail to list image return animation + postponeEnterTransition() + binding.plantList.doOnLayout { + startPostponedEnterTransition() + } + setupTransition() + setHasOptionsMenu(true) return binding.root } @@ -83,4 +93,11 @@ class PlantListFragment : Fragment() { } } } + + private fun setupTransition() { + exitTransition = Fade().apply { + interpolator = AnimUtils.getFastOutSlowInInterpolator() + duration = resources.getInteger(R.integer.config_duration_area_small).toLong() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantAdapter.kt b/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantAdapter.kt index 854f16a1b..6ae8ab113 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantAdapter.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantAdapter.kt @@ -19,7 +19,9 @@ package com.google.samples.apps.sunflower.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.databinding.DataBindingUtil import androidx.navigation.findNavController +import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -47,9 +49,17 @@ class PlantAdapter : ListAdapter(PlantDiffCallba } private fun createOnClickListener(plantId: String): View.OnClickListener { - return View.OnClickListener { - val direction = PlantListFragmentDirections.actionPlantListFragmentToPlantDetailFragment(plantId) - it.findNavController().navigate(direction) + return View.OnClickListener { view -> + val direction = PlantListFragmentDirections + .actionPlantListFragmentToPlantDetailFragment(plantId) + + DataBindingUtil.getBinding(view)?.let { + val navigatorExtras = FragmentNavigatorExtras(it.plantItemImage to plantId) + view.findNavController().navigate(direction, navigatorExtras) + } ?: run { + // fail to getBinding for transition anim. we still proceed to navigate + view.findNavController().navigate(direction) + } } } diff --git a/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantDetailBindingAdapters.kt b/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantDetailBindingAdapters.kt index 7e052a77e..f86d0616b 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantDetailBindingAdapters.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantDetailBindingAdapters.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.sunflower.adapters +import android.graphics.drawable.Drawable import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod import android.widget.ImageView @@ -27,15 +28,17 @@ import androidx.core.text.italic import androidx.databinding.BindingAdapter import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestListener import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.samples.apps.sunflower.R -@BindingAdapter("imageFromUrl") -fun bindImageFromUrl(view: ImageView, imageUrl: String?) { +@BindingAdapter("imageFromUrl", "requestListener", requireAll = false) +fun bindImageFromUrl(view: ImageView, imageUrl: String?, listener: RequestListener?) { if (!imageUrl.isNullOrEmpty()) { Glide.with(view.context) .load(imageUrl) .transition(DrawableTransitionOptions.withCrossFade()) + .listener(listener) .into(view) } } diff --git a/app/src/main/java/com/google/samples/apps/sunflower/utilities/AnimUtils.kt b/app/src/main/java/com/google/samples/apps/sunflower/utilities/AnimUtils.kt new file mode 100644 index 000000000..b7f43fb51 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/sunflower/utilities/AnimUtils.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.sunflower.utilities + +import android.view.animation.Interpolator +import androidx.interpolator.view.animation.FastOutLinearInInterpolator +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator + +object AnimUtils { + + private val fastOutSlowIn by lazy { FastOutSlowInInterpolator() } + private val fastOutLinearIn by lazy { FastOutLinearInInterpolator() } + private val linearOutSlowIn by lazy { LinearOutSlowInInterpolator() } + + /** + * Elements that begin and end at rest use standard easing. They speed up quickly + * and slow down gradually, in order to emphasize the end of the transition. + * + * Suitable timing for animating visible Views moving around on screen. + * + * See + * https://material.io/design/motion/speed.html#easing + */ + fun getFastOutSlowInInterpolator(): Interpolator? { + return fastOutSlowIn + } + + /** + * Incoming elements are animated using deceleration easing, which starts a transition + * at peak velocity (the fastest point of an element’s movement) and ends at rest. + * + * Suitable timing for animating Views entering a screen + * + * See + * https://material.io/design/motion/speed.html#easing + */ + fun getFastOutLinearInInterpolator(): Interpolator? { + return fastOutLinearIn + } + + /** + * Elements exiting a screen use acceleration easing, where they start at rest + * and end at peak velocity. + * + * Suitable timing for animating Views exiting a screen + * + * See + * https://material.io/design/motion/speed.html#easing + */ + fun getLinearOutSlowInInterpolator(): Interpolator? { + return linearOutSlowIn + } +} \ No newline at end of file diff --git a/app/src/main/java/com/google/samples/apps/sunflower/utilities/MoveViews.kt b/app/src/main/java/com/google/samples/apps/sunflower/utilities/MoveViews.kt new file mode 100644 index 000000000..c8796e084 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/sunflower/utilities/MoveViews.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.sunflower.utilities + +import android.content.Context +import android.util.AttributeSet +import androidx.transition.ChangeBounds +import androidx.transition.ChangeImageTransform +import androidx.transition.ChangeTransform +import androidx.transition.TransitionSet + +class MoveViews : TransitionSet { + + constructor() { + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + private fun init() { + addTransition(ChangeBounds()) + .addTransition(ChangeTransform()) + .addTransition(ChangeImageTransform()) + } +} diff --git a/app/src/main/res/layout/fragment_plant_detail.xml b/app/src/main/res/layout/fragment_plant_detail.xml index 20a71227c..1e2b214d1 100644 --- a/app/src/main/res/layout/fragment_plant_detail.xml +++ b/app/src/main/res/layout/fragment_plant_detail.xml @@ -23,6 +23,10 @@ + + + app:layout_collapseMode="parallax" + app:requestListener="@{requestListener}" /> diff --git a/app/src/main/res/layout/list_item_plant.xml b/app/src/main/res/layout/list_item_plant.xml index ad1b0f18e..2ce99374c 100644 --- a/app/src/main/res/layout/list_item_plant.xml +++ b/app/src/main/res/layout/list_item_plant.xml @@ -42,6 +42,7 @@ android:layout_marginStart="@dimen/margin_small" android:contentDescription="@string/a11y_plant_item_image" android:scaleType="centerCrop" + android:transitionName="@{plant.plantId}" app:imageFromUrl="@{plant.imageUrl}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/navigation/nav_garden.xml b/app/src/main/res/navigation/nav_garden.xml index c5b609f82..2cfb441c7 100644 --- a/app/src/main/res/navigation/nav_garden.xml +++ b/app/src/main/res/navigation/nav_garden.xml @@ -43,11 +43,7 @@ + app:destination="@id/plant_detail_fragment" /> + + + + + + 100 + + + 250 + + + 200 + + + 300 + + + 250 + + \ No newline at end of file