Skip to content
This repository was archived by the owner on Nov 21, 2024. It is now read-only.

Commit c964817

Browse files
committed
Animate top level screen transitions.
Change-Id: Ia1134ad5939098a7f0b93f2c137f891e0a1b2e78
1 parent e3783bc commit c964817

File tree

5 files changed

+155
-9
lines changed

5 files changed

+155
-9
lines changed

app/src/main/java/com/materialstudies/owl/ui/featured/FeaturedFragment.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import android.os.Bundle
2020
import android.view.LayoutInflater
2121
import android.view.View
2222
import android.view.ViewGroup
23+
import androidx.core.view.postDelayed
2324
import androidx.fragment.app.Fragment
2425
import com.materialstudies.owl.databinding.FragmentFeaturedBinding
2526
import com.materialstudies.owl.model.courses
27+
import com.materialstudies.owl.util.SpringAddItemAnimator
2628

2729
class FeaturedFragment : Fragment() {
2830

@@ -32,8 +34,15 @@ class FeaturedFragment : Fragment() {
3234
savedInstanceState: Bundle?
3335
): View? {
3436
val binding = FragmentFeaturedBinding.inflate(inflater, container, false).apply {
35-
featuredGrid.adapter = FeaturedAdapter().apply {
36-
submitList(courses)
37+
featuredGrid.apply {
38+
itemAnimator = SpringAddItemAnimator()
39+
adapter = FeaturedAdapter().apply {
40+
// Add animations not running without this delay
41+
// TODO(nickbutcher) work out why
42+
postDelayed(100L) {
43+
submitList(courses)
44+
}
45+
}
3746
}
3847
}
3948
return binding.root

app/src/main/java/com/materialstudies/owl/ui/mycourses/MyCoursesFragment.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import android.os.Bundle
1717
import android.view.LayoutInflater
1818
import android.view.View
1919
import android.view.ViewGroup
20+
import androidx.core.view.postDelayed
2021
import androidx.fragment.app.Fragment
2122
import com.materialstudies.owl.databinding.FragmentMyCoursesBinding
2223
import com.materialstudies.owl.model.courses
24+
import com.materialstudies.owl.util.SpringAddItemAnimator
2325

2426
class MyCoursesFragment : Fragment() {
2527

@@ -29,8 +31,15 @@ class MyCoursesFragment : Fragment() {
2931
savedInstanceState: Bundle?
3032
): View {
3133
val binding = FragmentMyCoursesBinding.inflate(inflater, container, false).apply {
32-
list.adapter = MyCoursesAdapter().apply {
33-
submitList(courses)
34+
list.apply {
35+
itemAnimator = SpringAddItemAnimator()
36+
adapter = MyCoursesAdapter().apply {
37+
// Add animations not running without this delay
38+
// TODO(nickbutcher) work out why
39+
postDelayed(100L) {
40+
submitList(courses)
41+
}
42+
}
3443
}
3544
}
3645
return binding.root

app/src/main/java/com/materialstudies/owl/ui/search/SearchFragment.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import android.os.Bundle
2020
import android.view.LayoutInflater
2121
import android.view.View
2222
import android.view.ViewGroup
23+
import androidx.core.view.postDelayed
2324
import androidx.fragment.app.Fragment
2425
import com.materialstudies.owl.databinding.FragmentSearchBinding
2526
import com.materialstudies.owl.model.topics
27+
import com.materialstudies.owl.util.SpringAddItemAnimator
2628

2729
class SearchFragment : Fragment() {
2830

@@ -33,8 +35,13 @@ class SearchFragment : Fragment() {
3335
): View? {
3436
val binding = FragmentSearchBinding.inflate(inflater, container, false).apply {
3537
searchResults.apply {
38+
itemAnimator = SpringAddItemAnimator()
3639
adapter = SearchAdapter().apply {
37-
submitList(topics)
40+
// Add animations not running without this delay
41+
// TODO(nickbutcher) work out why
42+
postDelayed(100L) {
43+
submitList(topics)
44+
}
3845
}
3946
}
4047
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2019 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.materialstudies.owl.util
18+
19+
import android.view.View
20+
import androidx.dynamicanimation.animation.DynamicAnimation
21+
import androidx.dynamicanimation.animation.SpringAnimation
22+
import androidx.dynamicanimation.animation.SpringForce
23+
import androidx.recyclerview.widget.DefaultItemAnimator
24+
import androidx.recyclerview.widget.RecyclerView
25+
26+
class SpringAddItemAnimator : DefaultItemAnimator() {
27+
28+
private val pendingAdds = mutableListOf<RecyclerView.ViewHolder>()
29+
30+
/**
31+
* Setup initial values to animate. Derive initial translationY from the view's bottom to
32+
* produce a stagger effect, as lower items arrive from father displaced.
33+
*/
34+
override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
35+
holder.itemView.alpha = 0f
36+
holder.itemView.translationY = holder.itemView.bottom / 3f
37+
pendingAdds.add(holder)
38+
return true
39+
}
40+
41+
/**
42+
* Animate back to full alpha and no translation.
43+
*/
44+
override fun runPendingAnimations() {
45+
super.runPendingAnimations()
46+
if (pendingAdds.isNotEmpty()) {
47+
for (i in pendingAdds.indices.reversed()) {
48+
val holder = pendingAdds[i]
49+
50+
val tySpring = holder.itemView.spring(
51+
SpringAnimation.TRANSLATION_Y,
52+
stiffness = 350f,
53+
damping = 0.6f
54+
)
55+
val aSpring = holder.itemView.spring(
56+
SpringAnimation.ALPHA,
57+
stiffness = 100f,
58+
damping = SpringForce.DAMPING_RATIO_NO_BOUNCY
59+
)
60+
val endListener = object : DynamicAnimation.OnAnimationEndListener {
61+
override fun onAnimationEnd(
62+
animation: DynamicAnimation<out DynamicAnimation<*>>?,
63+
canceled: Boolean,
64+
value: Float,
65+
velocity: Float
66+
) {
67+
if (!canceled) {
68+
animation?.removeEndListener(this)
69+
dispatchAddFinished(holder)
70+
dispatchFinishedWhenDone()
71+
} else {
72+
clearAnimatedValues(holder.itemView)
73+
}
74+
}
75+
}
76+
tySpring.addEndListener(endListener)
77+
dispatchAddStarting(holder)
78+
aSpring.animateToFinalPosition(1f)
79+
tySpring.animateToFinalPosition(0f)
80+
pendingAdds.removeAt(i)
81+
}
82+
}
83+
}
84+
85+
override fun endAnimation(holder: RecyclerView.ViewHolder) {
86+
holder.itemView.spring(SpringAnimation.TRANSLATION_Y).cancel()
87+
holder.itemView.spring(SpringAnimation.ALPHA).cancel()
88+
if (pendingAdds.remove(holder)) {
89+
dispatchAddFinished(holder)
90+
clearAnimatedValues(holder.itemView)
91+
}
92+
super.endAnimation(holder)
93+
}
94+
95+
override fun endAnimations() {
96+
for (i in pendingAdds.indices.reversed()) {
97+
val holder = pendingAdds[i]
98+
clearAnimatedValues(holder.itemView)
99+
dispatchAddFinished(holder)
100+
pendingAdds.removeAt(i)
101+
}
102+
super.endAnimations()
103+
}
104+
105+
override fun isRunning(): Boolean {
106+
return pendingAdds.isNotEmpty() || super.isRunning()
107+
}
108+
109+
private fun dispatchFinishedWhenDone() {
110+
if (!isRunning) {
111+
dispatchAnimationsFinished()
112+
}
113+
}
114+
115+
private fun clearAnimatedValues(view: View) {
116+
view.alpha = 1f
117+
view.translationY = 0f
118+
}
119+
120+
}

app/src/main/res/layout/fragment_search.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@
3434
<androidx.appcompat.widget.Toolbar
3535
android:layout_width="match_parent"
3636
android:layout_height="wrap_content"
37-
android:minHeight="?attr/actionBarSize"
3837
app:layout_scrollFlags="scroll|enterAlways|snap"
39-
app:menu="@menu/account"
40-
app:navigationIcon="@drawable/ic_search">
38+
android:minHeight="?attr/actionBarSize"
39+
app:navigationIcon="@drawable/ic_search"
40+
app:contentInsetStartWithNavigation="0dp"
41+
app:menu="@menu/account">
4142

4243
<EditText
4344
android:layout_width="match_parent"
@@ -48,7 +49,7 @@
4849
android:inputType="textCapWords"
4950
android:imeOptions="actionSearch"
5051
android:hint="@string/search_owl"
51-
android:textAppearance="?attr/textAppearanceSubtitle1" />
52+
android:textAppearance="?attr/textAppearanceSubtitle1"/>
5253

5354
</androidx.appcompat.widget.Toolbar>
5455

0 commit comments

Comments
 (0)