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

Commit 5a11938

Browse files
committed
List > Oscillation
1 parent 32ee2df commit 5a11938

File tree

9 files changed

+408
-0
lines changed

9 files changed

+408
-0
lines changed

Motion/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ package.
2424
- [Layout > FAB transformation](app/src/main/java/com/example/android/motion/demo/fabtransformation)
2525
- [List > Reorder](app/src/main/java/com/example/android/motion/demo/reorder)
2626
- [List > Stagger](app/src/main/java/com/example/android/motion/demo/stagger)
27+
- [List > Oscillation](app/src/main/java/com/example/android/motion/demo/oscillation)
2728
- [Navigation > Shared element](app/src/main/java/com/example/android/motion/demo/sharedelement)
2829
- [Navigation > Fade through](app/src/main/java/com/example/android/motion/demo/navfadethrough)

Motion/app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
implementation 'androidx.fragment:fragment-ktx:1.2.0-alpha02'
5757
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
5858
implementation 'androidx.transition:transition:1.2.0-beta01'
59+
implementation 'androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02'
5960
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta02'
6061

6162
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'

Motion/app/src/main/AndroidManifest.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@
117117
android:resource="@array/stagger_apis" />
118118
</activity>
119119

120+
<activity
121+
android:name=".demo.oscillation.OscillationActivity"
122+
android:label="@string/oscillation_label"
123+
android:theme="@style/Theme.Motion.CardUi">
124+
<intent-filter>
125+
<action android:name="android.intent.action.MAIN" />
126+
<category android:name="com.example.android.motion.intent.category.DEMO" />
127+
</intent-filter>
128+
129+
<meta-data
130+
android:name="com.example.android.motion.demo.DESCRIPTION"
131+
android:value="@string/oscillation_description" />
132+
<meta-data
133+
android:name="com.example.android.motion.demo.APIS"
134+
android:resource="@array/oscillation_apis" />
135+
</activity>
136+
120137
<activity
121138
android:name=".demo.sharedelement.SharedElementActivity"
122139
android:label="@string/shared_element_label"
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright 2019 The Android Open Source Project
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+
* http://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.example.android.motion.demo.oscillation
18+
19+
import android.view.LayoutInflater
20+
import android.view.ViewGroup
21+
import android.widget.EdgeEffect
22+
import android.widget.ImageView
23+
import android.widget.TextView
24+
import androidx.core.view.doOnNextLayout
25+
import androidx.dynamicanimation.animation.SpringAnimation
26+
import androidx.dynamicanimation.animation.SpringForce
27+
import androidx.recyclerview.widget.ListAdapter
28+
import androidx.recyclerview.widget.RecyclerView
29+
import com.bumptech.glide.Glide
30+
import com.example.android.motion.R
31+
import com.example.android.motion.model.Cheese
32+
33+
internal class CheeseAdapter : ListAdapter<Cheese, CheeseViewHolder>(Cheese.DIFF_CALLBACK) {
34+
35+
/**
36+
* A [RecyclerView.OnScrollListener] to be set to the RecyclerView. This tilts the visible
37+
* items while the list is scrolled.
38+
*
39+
* @see RecyclerView.addOnScrollListener
40+
*/
41+
val onScrollListener = object : RecyclerView.OnScrollListener() {
42+
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
43+
recyclerView.forEachVisibleHolder { holder: CheeseViewHolder ->
44+
holder.rotation
45+
// Update the velocity.
46+
// The velocity is calculated by the horizontal scroll offset.
47+
.setStartVelocity(holder.currentVelocity - dx * SCROLL_ROTATION_MAGNITUDE)
48+
// Start the animation. This does nothing if the animation is already running.
49+
.start()
50+
}
51+
}
52+
}
53+
54+
/**
55+
* A [RecyclerView.EdgeEffectFactory] to be set to the RecyclerView. This adds bounce effect
56+
* when the list is over-scrolled.
57+
*
58+
* @see RecyclerView.setEdgeEffectFactory
59+
*/
60+
val edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() {
61+
override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect {
62+
return object : EdgeEffect(recyclerView.context) {
63+
64+
override fun onPull(deltaDistance: Float) {
65+
super.onPull(deltaDistance)
66+
handlePull(deltaDistance)
67+
}
68+
69+
override fun onPull(deltaDistance: Float, displacement: Float) {
70+
super.onPull(deltaDistance, displacement)
71+
handlePull(deltaDistance)
72+
}
73+
74+
private fun handlePull(deltaDistance: Float) {
75+
// This is called on every touch event while the list is scrolled with a finger.
76+
// We simply update the view properties without animation.
77+
val sign = if (direction == DIRECTION_RIGHT) -1 else 1
78+
val rotationDelta = sign * deltaDistance * OVERSCROLL_ROTATION_MAGNITUDE
79+
val translationXDelta =
80+
sign * recyclerView.width * deltaDistance * OVERSCROLL_TRANSLATION_MAGNITUDE
81+
recyclerView.forEachVisibleHolder { holder: CheeseViewHolder ->
82+
holder.rotation.cancel()
83+
holder.translationX.cancel()
84+
holder.itemView.rotation += rotationDelta
85+
holder.itemView.translationX += translationXDelta
86+
}
87+
}
88+
89+
override fun onRelease() {
90+
super.onRelease()
91+
// The finger is lifted. This is when we should start the animations to bring
92+
// the view property values back to their resting states.
93+
recyclerView.forEachVisibleHolder { holder: CheeseViewHolder ->
94+
holder.rotation.start()
95+
holder.translationX.start()
96+
}
97+
}
98+
99+
override fun onAbsorb(velocity: Int) {
100+
super.onAbsorb(velocity)
101+
val sign = if (direction == DIRECTION_RIGHT) -1 else 1
102+
// The list has reached the edge on fling.
103+
val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE
104+
recyclerView.forEachVisibleHolder { holder: CheeseViewHolder ->
105+
holder.translationX
106+
.setStartVelocity(translationVelocity)
107+
.start()
108+
}
109+
}
110+
}
111+
}
112+
}
113+
114+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder {
115+
return CheeseViewHolder(parent)
116+
}
117+
118+
override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
119+
val cheese = getItem(position)
120+
Glide.with(holder.image).load(cheese.image).into(holder.image)
121+
holder.name.text = cheese.name
122+
// The rotation pivot should be at the center of the top edge.
123+
holder.itemView.doOnNextLayout { v -> v.pivotX = v.width / 2f }
124+
holder.itemView.pivotY = 0f
125+
}
126+
}
127+
128+
129+
internal class CheeseViewHolder(
130+
val parent: ViewGroup
131+
) : RecyclerView.ViewHolder(
132+
LayoutInflater.from(parent.context)
133+
.inflate(R.layout.cheese_board_item, parent, false)
134+
) {
135+
val image: ImageView = itemView.findViewById(R.id.image)
136+
val name: TextView = itemView.findViewById(R.id.name)
137+
138+
var currentVelocity = 0f
139+
140+
/**
141+
* A [SpringAnimation] for this RecyclerView item. This animation rotates the view with a bouncy
142+
* spring configuration, resulting in the oscillation effect.
143+
*
144+
* The animation is started in [CheeseAdapter.onScrollListener].
145+
*/
146+
val rotation: SpringAnimation = SpringAnimation(itemView, SpringAnimation.ROTATION)
147+
.setSpring(
148+
SpringForce()
149+
.setFinalPosition(0f)
150+
.setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY)
151+
.setStiffness(SpringForce.STIFFNESS_LOW)
152+
)
153+
.addUpdateListener { _, _, velocity ->
154+
currentVelocity = velocity
155+
}
156+
157+
/**
158+
* A [SpringAnimation] for this RecyclerView item. This animation is used to bring the item back
159+
* after the over-scroll effect.
160+
*/
161+
val translationX: SpringAnimation = SpringAnimation(itemView, SpringAnimation.TRANSLATION_X)
162+
.setSpring(
163+
SpringForce()
164+
.setFinalPosition(0f)
165+
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
166+
.setStiffness(SpringForce.STIFFNESS_LOW)
167+
)
168+
}
169+
170+
/**
171+
* Runs [action] on every visible [RecyclerView.ViewHolder] in this [RecyclerView].
172+
*/
173+
private inline fun <reified T : RecyclerView.ViewHolder> RecyclerView.forEachVisibleHolder(
174+
action: (T) -> Unit
175+
) {
176+
for (i in 0 until childCount) {
177+
action(getChildViewHolder(getChildAt(i)) as T)
178+
}
179+
}
180+
181+
// The constants below are used to calculate the animation magnitude from values taken from UI
182+
// events. Their values are determined empirically and can be modified to change the animation
183+
// flavor.
184+
185+
/** The magnitude of rotation while the list is scrolled. */
186+
private const val SCROLL_ROTATION_MAGNITUDE = 0.25f
187+
/** The magnitude of rotation while the list is over-scrolled. */
188+
private const val OVERSCROLL_ROTATION_MAGNITUDE = -10
189+
/** The magnitude of translation distance while the list is over-scrolled. */
190+
private const val OVERSCROLL_TRANSLATION_MAGNITUDE = 0.2f
191+
/** The magnitude of translation distance when the list reaches the edge on fling. */
192+
private const val FLING_TRANSLATION_MAGNITUDE = 0.5f
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2019 The Android Open Source Project
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+
* http://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.example.android.motion.demo.oscillation
18+
19+
import android.os.Bundle
20+
import androidx.activity.viewModels
21+
import androidx.appcompat.app.AppCompatActivity
22+
import androidx.appcompat.widget.Toolbar
23+
import androidx.lifecycle.observe
24+
import androidx.recyclerview.widget.RecyclerView
25+
import com.example.android.motion.R
26+
import com.example.android.motion.ui.EdgeToEdge
27+
28+
class OscillationActivity : AppCompatActivity() {
29+
30+
private val viewModel: OscillationViewModel by viewModels()
31+
32+
override fun onCreate(savedInstanceState: Bundle?) {
33+
super.onCreate(savedInstanceState)
34+
setContentView(R.layout.oscillation_activity)
35+
36+
val toolbar: Toolbar = findViewById(R.id.toolbar)
37+
val list: RecyclerView = findViewById(R.id.list)
38+
setSupportActionBar(toolbar)
39+
40+
EdgeToEdge.setUpRoot(findViewById(R.id.root))
41+
EdgeToEdge.setUpAppBar(findViewById(R.id.app_bar), toolbar)
42+
EdgeToEdge.setUpScrollingContent(list)
43+
44+
val adapter = CheeseAdapter()
45+
list.adapter = adapter
46+
// The adapter knows how to animate its items while the list is scrolled.
47+
list.addOnScrollListener(adapter.onScrollListener)
48+
list.edgeEffectFactory = adapter.edgeEffectFactory
49+
50+
viewModel.cheeses.observe(this) { cheeses ->
51+
adapter.submitList(cheeses)
52+
}
53+
}
54+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2019 The Android Open Source Project
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+
* http://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.example.android.motion.demo.oscillation
18+
19+
import androidx.lifecycle.LiveData
20+
import androidx.lifecycle.MutableLiveData
21+
import androidx.lifecycle.ViewModel
22+
import com.example.android.motion.model.Cheese
23+
24+
class OscillationViewModel : ViewModel() {
25+
26+
val cheeses: LiveData<List<Cheese>> = MutableLiveData(Cheese.ALL.filter {
27+
it.name.length < 10
28+
}.shuffled().take(15))
29+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright 2019 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<com.google.android.material.card.MaterialCardView
18+
xmlns:android="http://schemas.android.com/apk/res/android"
19+
xmlns:app="http://schemas.android.com/apk/res-auto"
20+
xmlns:tools="http://schemas.android.com/tools"
21+
android:layout_width="wrap_content"
22+
android:layout_height="wrap_content"
23+
android:layout_margin="@dimen/spacing_medium"
24+
app:cardCornerRadius="1dp">
25+
26+
<LinearLayout
27+
android:layout_width="wrap_content"
28+
android:layout_height="wrap_content"
29+
android:orientation="horizontal">
30+
31+
<ImageView
32+
android:id="@+id/image"
33+
android:layout_width="64dp"
34+
android:layout_height="64dp"
35+
android:contentDescription="@null"
36+
android:scaleType="centerCrop"
37+
tools:src="@drawable/cheese_1" />
38+
39+
<TextView
40+
android:id="@+id/name"
41+
android:layout_width="0dp"
42+
android:layout_height="match_parent"
43+
android:layout_margin="@dimen/spacing_medium"
44+
android:layout_weight="1"
45+
android:textAppearance="?attr/textAppearanceHeadline6"
46+
tools:text="Cheese" />
47+
48+
</LinearLayout>
49+
50+
</com.google.android.material.card.MaterialCardView>

0 commit comments

Comments
 (0)