Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hero image transition #1

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

/**
Expand Down Expand Up @@ -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 ->
Expand All @@ -71,6 +83,9 @@ class PlantDetailFragment : Fragment() {
}
})

postponeEnterTransition() // wait for Glide callback to start transition
setupTransition()

setHasOptionsMenu(true)

return binding.root
Expand Down Expand Up @@ -105,4 +120,49 @@ class PlantDetailFragment : Fragment() {
else -> super.onOptionsItemSelected(item)
}
}

val imageListener = object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
startPostponedEnterTransition()
return false
}

override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
Expand Down Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,9 +49,17 @@ class PlantAdapter : ListAdapter<Plant, PlantAdapter.ViewHolder>(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<ListItemPlantBinding>(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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Drawable>?) {
if (!imageUrl.isNullOrEmpty()) {
Glide.with(view.context)
.load(imageUrl)
.transition(DrawableTransitionOptions.withCrossFade())
.listener(listener)
.into(view)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="https://material.io/design/motion/speed.html#easing">
* https://material.io/design/motion/speed.html#easing</a>
*/
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 <a href="https://material.io/design/motion/speed.html#easing">
* https://material.io/design/motion/speed.html#easing</a>
*/
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 <a href="https://material.io/design/motion/speed.html#easing">
* https://material.io/design/motion/speed.html#easing</a>
*/
fun getLinearOutSlowInInterpolator(): Interpolator? {
return linearOutSlowIn
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
7 changes: 6 additions & 1 deletion app/src/main/res/layout/fragment_plant_detail.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<variable
name="viewModel"
type="com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModel" />

<variable
name="requestListener"
type="com.bumptech.glide.request.RequestListener&lt;android.graphics.drawable.Drawable>" />
</data>

<androidx.coordinatorlayout.widget.CoordinatorLayout
Expand Down Expand Up @@ -57,7 +61,8 @@
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:imageFromUrl="@{viewModel.plant.imageUrl}"
app:layout_collapseMode="parallax" />
app:layout_collapseMode="parallax"
app:requestListener="@{requestListener}" />

<androidx.appcompat.widget.Toolbar
android:id="@+id/detail_toolbar"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/layout/list_item_plant.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 1 addition & 5 deletions app/src/main/res/navigation/nav_garden.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@

<action
android:id="@+id/action_plant_list_fragment_to_plant_detail_fragment"
app:destination="@id/plant_detail_fragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
app:destination="@id/plant_detail_fragment" />
</fragment>

<fragment
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/res/values/integer.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<!-- Animation durations https://material.io/design/motion/speed.html#duration -->

<!-- Duration suitable for icons and selection controls. -->
<integer name="config_duration_area_small">100</integer>

<!-- Expanding duration suitable for bottom sheets and expanding chips -->
<integer name="config_duration_area_medium_expand">250</integer>

<!-- Collapsing duration suitable for bottom sheets and expanding chips -->
<integer name="config_duration_area_medium_collapse">200</integer>

<!-- Expanding duration suitable for cards and persistent sheet -->
<integer name="config_duration_area_large_expand">300</integer>

<!-- Expanding duration suitable for cards and persistent sheet -->
<integer name="config_duration_area_large_collapse">250</integer>

</resources>