Skip to content

Commit cd6017f

Browse files
authored
Merge pull request android#5 from android/loading
List > Loading
2 parents 29e7509 + 5f2843c commit cd6017f

File tree

13 files changed

+463
-5
lines changed

13 files changed

+463
-5
lines changed

Motion/app/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ dependencies {
5656
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
5757
implementation 'androidx.transition:transition:1.2.0-beta01'
5858
implementation 'androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02'
59-
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta02'
59+
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta03'
6060

6161
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
6262

@@ -65,6 +65,9 @@ dependencies {
6565
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
6666
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
6767

68+
def paging_version = '2.1.0'
69+
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
70+
6871
implementation 'com.google.android.material:material:1.1.0-alpha09'
6972

7073
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"

Motion/app/src/main/AndroidManifest.xml

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

120+
<activity
121+
android:name=".demo.loading.LoadingActivity"
122+
android:label="@string/loading_label">
123+
<intent-filter>
124+
<action android:name="android.intent.action.MAIN" />
125+
<category android:name="com.example.android.motion.intent.category.DEMO" />
126+
</intent-filter>
127+
128+
<meta-data
129+
android:name="com.example.android.motion.demo.DESCRIPTION"
130+
android:value="@string/loading_description" />
131+
<meta-data
132+
android:name="com.example.android.motion.demo.APIS"
133+
android:resource="@array/loading_apis" />
134+
</activity>
135+
120136
<activity
121137
android:name=".demo.oscillation.OscillationActivity"
122138
android:label="@string/oscillation_label"
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.loading
18+
19+
import android.animation.ObjectAnimator
20+
import android.os.SystemClock
21+
import android.view.LayoutInflater
22+
import android.view.View
23+
import android.view.ViewGroup
24+
import android.widget.ImageView
25+
import android.widget.TextView
26+
import androidx.core.animation.doOnEnd
27+
import androidx.paging.PagedListAdapter
28+
import androidx.recyclerview.widget.RecyclerView
29+
import com.bumptech.glide.Glide
30+
import com.bumptech.glide.load.resource.bitmap.CircleCrop
31+
import com.example.android.motion.R
32+
import com.example.android.motion.model.Cheese
33+
34+
internal class CheeseAdapter : PagedListAdapter<Cheese, CheeseViewHolder>(Cheese.DIFF_CALLBACK) {
35+
36+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder {
37+
return CheeseViewHolder(parent)
38+
}
39+
40+
override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
41+
val cheese: Cheese? = getItem(position)
42+
if (cheese == null) {
43+
holder.showPlaceholder()
44+
} else {
45+
holder.bind(cheese)
46+
}
47+
}
48+
}
49+
50+
/**
51+
* A dummy adapter that shows placeholders.
52+
*/
53+
internal class PlaceholderAdapter : RecyclerView.Adapter<CheeseViewHolder>() {
54+
55+
override fun getItemCount(): Int {
56+
return Int.MAX_VALUE
57+
}
58+
59+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder {
60+
return CheeseViewHolder(parent)
61+
}
62+
63+
override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
64+
// We have to call this method in onBindVH rather than onCreateVH because it uses the
65+
// adapterPosition of the ViewHolder.
66+
holder.showPlaceholder()
67+
}
68+
}
69+
70+
private const val FADE_DURATION = 1000L
71+
72+
internal class CheeseViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
73+
LayoutInflater.from(parent.context).inflate(R.layout.cheese_list_item, parent, false)
74+
) {
75+
val image: ImageView = itemView.findViewById(R.id.image)
76+
val name: TextView = itemView.findViewById(R.id.name)
77+
78+
/**
79+
* This is the animation we apply to each of the list items. It animates the alpha value from 1
80+
* to 0, then back to 1. The animation repeats infinitely until it is manually ended.
81+
*/
82+
private val animation = ObjectAnimator.ofFloat(itemView, View.ALPHA, 1f, 0f, 1f).apply {
83+
repeatCount = ObjectAnimator.INFINITE
84+
duration = FADE_DURATION
85+
// Reset the alpha on animation end.
86+
doOnEnd { itemView.alpha = 1f }
87+
}
88+
89+
fun showPlaceholder() {
90+
// Shift the timing of fade-in/out for each item by its adapter position. We use the
91+
// elapsed real time to make this independent from the timing of method call.
92+
animation.currentPlayTime =
93+
(SystemClock.elapsedRealtime() - adapterPosition * 30L) % FADE_DURATION
94+
animation.start()
95+
// Show the placeholder UI.
96+
image.setImageResource(R.drawable.image_placeholder)
97+
name.text = null
98+
name.setBackgroundResource(R.drawable.text_placeholder)
99+
}
100+
101+
fun bind(cheese: Cheese) {
102+
animation.end()
103+
Glide.with(image).load(cheese.image).transform(CircleCrop()).into(image)
104+
name.text = cheese.name
105+
name.setBackgroundResource(0)
106+
}
107+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.loading
18+
19+
import android.os.SystemClock
20+
import androidx.paging.DataSource
21+
import androidx.paging.PositionalDataSource
22+
import com.example.android.motion.model.Cheese
23+
24+
class CheeseDataSource : PositionalDataSource<Cheese>() {
25+
26+
companion object Factory : DataSource.Factory<Int, Cheese>() {
27+
override fun create(): DataSource<Int, Cheese> = CheeseDataSource()
28+
}
29+
30+
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Cheese>) {
31+
// Simulate a slow network.
32+
SystemClock.sleep(3000L)
33+
callback.onResult(
34+
Cheese.ALL.subList(
35+
params.requestedStartPosition,
36+
params.requestedStartPosition + params.requestedLoadSize
37+
),
38+
params.requestedStartPosition,
39+
Cheese.ALL.size
40+
)
41+
}
42+
43+
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Cheese>) {
44+
// Simulate a slow network.
45+
SystemClock.sleep(3000L)
46+
callback.onResult(
47+
Cheese.ALL.subList(
48+
params.startPosition, params.startPosition + params.loadSize
49+
)
50+
)
51+
}
52+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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.loading
18+
19+
import android.os.Bundle
20+
import android.view.Menu
21+
import android.view.MenuItem
22+
import androidx.activity.viewModels
23+
import androidx.appcompat.app.AppCompatActivity
24+
import androidx.appcompat.widget.Toolbar
25+
import androidx.lifecycle.observe
26+
import androidx.recyclerview.widget.RecyclerView
27+
import androidx.transition.Fade
28+
import androidx.transition.Transition
29+
import androidx.transition.TransitionListenerAdapter
30+
import androidx.transition.TransitionManager
31+
import com.example.android.motion.R
32+
import com.example.android.motion.demo.FAST_OUT_SLOW_IN
33+
import com.example.android.motion.demo.LARGE_EXPAND_DURATION
34+
import com.example.android.motion.demo.plusAssign
35+
import com.example.android.motion.demo.transitionSequential
36+
import com.example.android.motion.ui.EdgeToEdge
37+
38+
/**
39+
* Shows a list of cheeses. We use the Paging Library to load the list.
40+
*/
41+
class LoadingActivity : AppCompatActivity() {
42+
43+
private val viewModel: LoadingViewModel by viewModels()
44+
45+
private lateinit var list: RecyclerView
46+
private val fade = transitionSequential {
47+
duration = LARGE_EXPAND_DURATION
48+
interpolator = FAST_OUT_SLOW_IN
49+
this += Fade(Fade.OUT)
50+
this += Fade(Fade.IN)
51+
addListener(object : TransitionListenerAdapter() {
52+
override fun onTransitionEnd(transition: Transition) {
53+
if (savedItemAnimator != null) {
54+
list.itemAnimator = savedItemAnimator
55+
}
56+
}
57+
})
58+
}
59+
60+
private val placeholderAdapter = PlaceholderAdapter()
61+
private val cheeseAdapter = CheeseAdapter()
62+
63+
private var savedItemAnimator: RecyclerView.ItemAnimator? = null
64+
65+
override fun onCreate(savedInstanceState: Bundle?) {
66+
super.onCreate(savedInstanceState)
67+
setContentView(R.layout.loading_activity)
68+
69+
val toolbar: Toolbar = findViewById(R.id.toolbar)
70+
list = findViewById(R.id.list)
71+
setSupportActionBar(toolbar)
72+
EdgeToEdge.setUpRoot(findViewById(R.id.coordinator))
73+
EdgeToEdge.setUpAppBar(findViewById(R.id.app_bar), toolbar)
74+
EdgeToEdge.setUpScrollingContent(list)
75+
76+
// Show the initial placeholders.
77+
// See the ViewHolder implementation for how to create the loading animation.
78+
list.adapter = placeholderAdapter
79+
viewModel.cheeses.observe(this) { cheeses ->
80+
if (list.adapter != cheeseAdapter) {
81+
list.adapter = cheeseAdapter
82+
savedItemAnimator = list.itemAnimator
83+
list.itemAnimator = null
84+
TransitionManager.beginDelayedTransition(list, fade)
85+
}
86+
cheeseAdapter.submitList(cheeses)
87+
}
88+
}
89+
90+
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
91+
menuInflater.inflate(R.menu.loading, menu)
92+
return super.onCreateOptionsMenu(menu)
93+
}
94+
95+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
96+
return when (item.itemId) {
97+
R.id.action_refresh -> {
98+
TransitionManager.beginDelayedTransition(list, fade)
99+
list.adapter = placeholderAdapter
100+
viewModel.refresh()
101+
true
102+
}
103+
else -> super.onOptionsItemSelected(item)
104+
}
105+
}
106+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.loading
18+
19+
import androidx.lifecycle.LiveData
20+
import androidx.lifecycle.MediatorLiveData
21+
import androidx.lifecycle.ViewModel
22+
import androidx.paging.PagedList
23+
import androidx.paging.toLiveData
24+
import com.example.android.motion.model.Cheese
25+
26+
class LoadingViewModel : ViewModel() {
27+
28+
private var source: LiveData<PagedList<Cheese>>? = null
29+
private val _cheeses = MediatorLiveData<PagedList<Cheese>>()
30+
val cheeses: LiveData<PagedList<Cheese>> = _cheeses
31+
32+
init {
33+
refresh()
34+
}
35+
36+
fun refresh() {
37+
source?.let { _cheeses.removeSource(it) }
38+
val s = CheeseDataSource.toLiveData(pageSize = 15)
39+
source = s
40+
_cheeses.addSource(s) { _cheeses.postValue(it) }
41+
}
42+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
<shape
18+
xmlns:android="http://schemas.android.com/apk/res/android"
19+
android:shape="oval">
20+
<solid android:color="@color/card_background" />
21+
</shape>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
<shape
18+
xmlns:android="http://schemas.android.com/apk/res/android"
19+
android:shape="rectangle">
20+
<size android:width="160dp" />
21+
<solid android:color="@color/card_background" />
22+
</shape>

0 commit comments

Comments
 (0)