Skip to content

Commit f2c9ca0

Browse files
Add new transitions to Tutorial 2-6
1 parent db50398 commit f2c9ca0

19 files changed

+709
-8
lines changed

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ and be aware that Animator from ```onDisAppear``` is called while current transi
372372
#### Summary
373373
* For ```enterTransition``` and ```reEnterTransition``` either check for background color change and set **transitionGroup** to false
374374
or use transition that extends ```Visibility``` with change from INVISIBLE to VISIBLE.
375+
375376
⚠️ With ```reEnterTransition``` even though ```Explode``` does not work without solving background issue, most of
376377
the classes extend ```Transition``` or ```Visibility``` work fine
377378

@@ -384,6 +385,34 @@ the classes extend ```Transition``` or ```Visibility``` work fine
384385
By default all views under a parent/ancestor with a background set (even transparent ones) will be automatically deemed a group. If you need to break them up like we here with a RecyclerView as the shared-root-white-backgrounded layout with transparent child Item views. You’ll need to set the **layout with the background** to **transitionGroup=false.**
385386
But on the other hand, since the Items are “background-less” themselves, to prevent an out-of-body experience you’ll need to do the opposite and set transitionGroup=true on the Item layouts for all the child views in that Item to move together.
386387

388+
#### Material Transitions
389+
390+
[MaterialContainerTransform](https://material.io/develop/android/theming/motion#container-transform)
391+
392+
The container transform pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements.
393+
394+
MaterialContainerTransform is a shared element transition. Unlike traditional Android shared elements, it is not designed around a singular piece of shared content, such as an image, to be moved between two scenes. Instead, the shared element here refers to the bounding container of a start View or ViewGroup (e.g. the entire row layout of an item in a list) transforming its size and shape into that of an end View or ViewGroup (e.g. the root ViewGroup of a full screen Fragment). These start and end container Views are the “shared element” of a container transform. While these containers are being transformed, their contents are swapped to create the transition.
395+
396+
*Examples of the container transform:*
397+
398+
399+
* A card into a details page
400+
* A list item into a details page
401+
* A FAB into a details page
402+
* A search bar into expanded search
403+
404+
[MaterialSharedAxis](https://material.io/develop/android/theming/motion#shared-axis)
405+
406+
The shared axis pattern is used for transitions between UI elements that have a spatial or navigational relationship. This pattern uses a shared transformation on the x, y, or z axis to reinforce the relationship between elements.
407+
408+
[MaterialElevationScale]()
409+
410+
* MaterialFadeThrough
411+
412+
413+
* MaterialArcMotion
414+
415+
387416

388417
### Resources and References
389418

Tutorial3-1Transitions/src/main/java/com/smarttoolfactory/tutorial3_1transitions/chapter2_fragment_transitions/Activity2_6FragmentExpandCollapseTransitions.kt

+91-8
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,150 @@
11
package com.smarttoolfactory.tutorial3_1transitions.chapter2_fragment_transitions
22

33
import android.os.Bundle
4+
import android.os.Handler
45
import android.view.View
56
import android.widget.Toast
7+
import androidx.activity.viewModels
68
import androidx.appcompat.app.AppCompatActivity
79
import androidx.coordinatorlayout.widget.CoordinatorLayout
810
import androidx.core.view.updatePadding
911
import androidx.navigation.findNavController
12+
import androidx.transition.Transition
13+
import androidx.transition.TransitionListenerAdapter
14+
import androidx.transition.TransitionManager
15+
import com.google.android.material.bottomappbar.BottomAppBar
1016
import com.google.android.material.bottomnavigation.BottomNavigationView
1117
import com.google.android.material.floatingactionbutton.FloatingActionButton
18+
import com.google.android.material.transition.MaterialElevationScale
19+
import com.google.android.material.transition.MaterialFadeThrough
20+
import com.google.android.material.transition.MaterialSharedAxis
1221
import com.smarttoolfactory.tutorial3_1transitions.R
1322

1423

1524
@Suppress("DEPRECATION")
1625
class Activity2_6FragmentExpandCollapseTransitions : AppCompatActivity() {
1726

27+
val viewModel: ExpandCollapseViewModel by viewModels<ExpandCollapseViewModel>()
28+
1829
private lateinit var fab: FloatingActionButton
1930

2031
override fun onCreate(savedInstanceState: Bundle?) {
21-
2232
super.onCreate(savedInstanceState)
2333
setContentView(R.layout.activity2_6_rv_transitions_expand)
2434
title = getString(R.string.activity2_6)
2535

26-
val rootView = findViewById<CoordinatorLayout>(R.id.coordinatorLayout)
2736

37+
val rootView = findViewById<CoordinatorLayout>(R.id.coordinatorLayout)
2838

39+
/*
40+
🔥 Use root View of your Activity, using decorView prevents
41+
android:windowLightStatusBar from changing icon tint(black) for true flag
42+
*/
2943
hideSystemUI(rootView, true)
30-
44+
45+
val bottomAppbar = findViewById<BottomAppBar>(R.id.bottom_app_bar)
46+
3147
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_navigation)
3248

49+
val navHostFragment = findViewById<View>(R.id.nav_host_fragment)
50+
51+
bottomNavigationView.setOnNavigationItemSelectedListener {
52+
53+
navHostFragment.visibility = View.INVISIBLE
54+
55+
val transition: Transition = when (it.itemId) {
56+
R.id.item_home_fragment -> {
57+
MaterialFadeThrough()
58+
}
59+
R.id.item_dashboard -> {
60+
MaterialSharedAxis(MaterialSharedAxis.X, true)
61+
}
62+
R.id.item_notification -> {
63+
MaterialSharedAxis(MaterialSharedAxis.Y, true)
64+
}
65+
else -> {
66+
MaterialElevationScale(true)
67+
}
68+
}
69+
70+
/*
71+
🔥 This is deliberately slow to show how Material Transitions work.
72+
MaterialFadeThrough, MaterialElevationScale, and MaterialSharedAxis with Z
73+
almost look like each other
74+
*/
75+
76+
transition.apply {
77+
duration = 900
78+
}
79+
.addListener(object : TransitionListenerAdapter() {
80+
81+
override fun onTransitionEnd(transition: Transition) {
82+
super.onTransitionEnd(transition)
83+
Toast.makeText(
84+
applicationContext,
85+
"Activity ${transition::class.java.simpleName} transition end",
86+
Toast.LENGTH_SHORT
87+
).show()
88+
}
89+
})
90+
91+
TransitionManager.beginDelayedTransition(rootView, transition)
92+
navHostFragment.visibility = View.VISIBLE
93+
true
94+
}
95+
3396
bottomNavigationView.setOnApplyWindowInsetsListener { view, insets ->
3497
view.updatePadding(bottom = 0)
98+
3599
insets
36100
}
37101

38-
fab = findViewById<FloatingActionButton>(R.id.fab)
102+
fab = findViewById(R.id.fab)
39103

40104
val navController = findNavController(R.id.nav_host_fragment)
41105

106+
val handler = Handler()
107+
handler.postDelayed({
108+
109+
}, 1000)
110+
42111

43112
navController.addOnDestinationChangedListener { controller, destination, arguments ->
44113

45114
when (destination.id) {
46115
R.id.fragment2_6ExpandCollapseList -> {
47116
fab.setImageState(intArrayOf(-android.R.attr.state_activated), true)
117+
fab.show()
118+
bottomAppbar.performShow()
48119
}
49120

50121
R.id.fragment2_6ExpandCollapseDetails -> {
51122
fab.setImageState(intArrayOf(android.R.attr.state_activated), true)
52123
}
124+
125+
R.id.fragment2_6Compose -> {
126+
127+
128+
}
53129
}
54130
}
55131

56132
fab.setOnClickListener {
57-
58133
if (navController.currentDestination?.id == R.id.fragment2_6ExpandCollapseList) {
59134
Toast.makeText(applicationContext, "Compose", Toast.LENGTH_SHORT).show()
135+
136+
137+
// Set a custom animation for showing and hiding the FAB
138+
fab.setShowMotionSpecResource(R.animator.fab_show)
139+
fab.setHideMotionSpecResource(R.animator.fab_hide)
140+
bottomAppbar.performHide()
141+
fab.hide()
142+
143+
findNavController(R.id.nav_host_fragment).navigate(R.id.action_fragment2_6ExpandCollapseList_to_fragment2_6Compose)
60144
} else if (navController.currentDestination?.id == R.id.fragment2_6ExpandCollapseDetails) {
61145
onBackPressed()
62-
}
63146

147+
}
64148
}
65149
}
66150

@@ -69,7 +153,6 @@ class Activity2_6FragmentExpandCollapseTransitions : AppCompatActivity() {
69153
var uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
70154

71155
if (isFullScreen) {
72-
// hide status bar
73156
uiOptions = (
74157
uiOptions
75158
// or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
@@ -78,7 +161,7 @@ class Activity2_6FragmentExpandCollapseTransitions : AppCompatActivity() {
78161
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
79162
// // Removes Status bar
80163
// or View.SYSTEM_UI_FLAG_FULLSCREEN
81-
// // hide nav bar
164+
// // Removes nav bar
82165
// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
83166
)
84167
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.smarttoolfactory.tutorial3_1transitions.chapter2_fragment_transitions
2+
3+
import androidx.lifecycle.MutableLiveData
4+
import androidx.lifecycle.ViewModel
5+
6+
class ExpandCollapseViewModel : ViewModel() {
7+
8+
val goToComposePage = MutableLiveData<Boolean>()
9+
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.smarttoolfactory.tutorial3_1transitions.chapter2_fragment_transitions
2+
3+
import android.graphics.Color
4+
import android.os.Bundle
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import androidx.cardview.widget.CardView
9+
import androidx.fragment.app.Fragment
10+
import androidx.transition.Slide
11+
import com.google.android.material.transition.MaterialArcMotion
12+
import com.google.android.material.transition.MaterialContainerTransform
13+
import com.smarttoolfactory.tutorial3_1transitions.R
14+
15+
class Fragment2_6Compose : Fragment() {
16+
17+
18+
override fun onCreateView(
19+
inflater: LayoutInflater,
20+
container: ViewGroup?,
21+
savedInstanceState: Bundle?
22+
): View? {
23+
return inflater.inflate(R.layout.fragment2_6compose, container, false)
24+
}
25+
26+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
27+
super.onViewCreated(view, savedInstanceState)
28+
29+
val emailCardView = view.findViewById<CardView>(R.id.email_card_view)
30+
31+
// Set transitions here so we are able to access Fragment's binding views.
32+
enterTransition = MaterialContainerTransform().apply {
33+
// Manually add the Views to be shared since this is not a standard Fragment to
34+
// Fragment shared element transition.
35+
startView = requireActivity().findViewById(R.id.fab)
36+
endView = emailCardView
37+
// duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
38+
// Optionally add a curved path to the transform
39+
setPathMotion(MaterialArcMotion())
40+
41+
duration = 1300
42+
scrimColor = Color.TRANSPARENT
43+
// containerColor = requireContext().themeColor(R.attr.colorSurface)
44+
// startContainerColor = requireContext().themeColor(R.attr.colorSecondary)
45+
// endContainerColor = requireContext().themeColor(R.attr.colorSurface)
46+
}
47+
returnTransition = Slide().apply {
48+
duration = resources.getInteger(R.integer.reply_motion_duration_medium).toLong()
49+
addTarget(R.id.email_card_view)
50+
}
51+
}
52+
53+
}

Tutorial3-1Transitions/src/main/java/com/smarttoolfactory/tutorial3_1transitions/chapter2_fragment_transitions/Fragment2_6ExpandCollapseList.kt

+23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.core.view.doOnNextLayout
88
import androidx.core.view.doOnPreDraw
99
import androidx.core.view.updatePadding
1010
import androidx.fragment.app.Fragment
11+
import androidx.fragment.app.activityViewModels
1112
import androidx.navigation.NavDirections
1213
import androidx.navigation.fragment.FragmentNavigatorExtras
1314
import androidx.navigation.fragment.findNavController
@@ -22,6 +23,9 @@ import com.smarttoolfactory.tutorial3_1transitions.databinding.ItemTravelBinding
2223
@Suppress("UNCHECKED_CAST", "DEPRECATION")
2324
class Fragment2_6ExpandCollapseList : Fragment() {
2425

26+
val viewModel by activityViewModels<ExpandCollapseViewModel>()
27+
28+
2529
private val recycledViewPool by lazy {
2630
RecyclerView.RecycledViewPool()
2731
}
@@ -85,6 +89,12 @@ class Fragment2_6ExpandCollapseList : Fragment() {
8589
startPostponedEnterTransition()
8690
}
8791
}
92+
93+
viewModel.goToComposePage.observe(viewLifecycleOwner, {
94+
95+
96+
})
97+
8898
}
8999

90100
private fun goToDetailPage(binding: ItemTravelBinding, model: TravelModel) {
@@ -99,4 +109,17 @@ class Fragment2_6ExpandCollapseList : Fragment() {
99109
findNavController().navigate(direction, extras)
100110

101111
}
112+
113+
private fun goToComposePage(binding: ItemTravelBinding) {
114+
115+
val direction: NavDirections = Fragment2_6ExpandCollapseListDirections
116+
.actionFragment26ExpandCollapseListToFragment26Compose()
117+
118+
val extras = FragmentNavigatorExtras(
119+
binding.cardView to binding.cardView.transitionName
120+
)
121+
122+
findNavController().navigate(direction, extras)
123+
124+
}
102125
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright (c) 2019 Google Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
6+
in compliance with the License. You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software distributed under the License
11+
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
or implied. See the License for the specific language governing permissions and limitations under
13+
the License.
14+
-->
15+
<set xmlns:android="http://schemas.android.com/apk/res/android">
16+
<objectAnimator
17+
android:propertyName="scale"
18+
android:valueFrom="1"
19+
android:valueTo="0"
20+
android:duration="@integer/reply_motion_duration_small"
21+
android:repeatMode="restart"
22+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
23+
<objectAnimator
24+
android:propertyName="iconScale"
25+
android:valueFrom="1"
26+
android:valueTo="0"
27+
android:duration="@integer/reply_motion_duration_small"
28+
android:repeatMode="restart"
29+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
30+
<objectAnimator
31+
android:propertyName="opacity"
32+
android:valueFrom="1"
33+
android:valueTo="0"
34+
android:duration="@integer/reply_motion_duration_small"
35+
android:repeatMode="restart"
36+
android:interpolator="@android:interpolator/fast_out_slow_in"/>
37+
</set>

0 commit comments

Comments
 (0)