Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.

Commit da3a514

Browse files
jdkorenGerrit Code Review
authored andcommitted
Merge "Codelabs UI for tablets" into main
2 parents 74fad4b + 31c68c5 commit da3a514

File tree

24 files changed

+304
-172
lines changed

24 files changed

+304
-172
lines changed

mobile/src/main/java/com/google/samples/apps/iosched/ui/codelabs/CodelabsAdapter.kt

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,16 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
3232
import com.google.android.flexbox.FlexboxLayoutManager
3333
import com.google.samples.apps.iosched.R
3434
import com.google.samples.apps.iosched.databinding.ItemCodelabBinding
35-
import com.google.samples.apps.iosched.databinding.ItemCodelabsHeaderBinding
3635
import com.google.samples.apps.iosched.databinding.ItemCodelabsInformationCardBinding
3736
import com.google.samples.apps.iosched.model.Codelab
3837
import com.google.samples.apps.iosched.ui.codelabs.CodelabsViewHolder.CodelabItemHolder
39-
import com.google.samples.apps.iosched.ui.codelabs.CodelabsViewHolder.CodelabsHeaderHolder
4038
import com.google.samples.apps.iosched.ui.codelabs.CodelabsViewHolder.CodelabsInformationCardHolder
4139
import com.google.samples.apps.iosched.util.compatRemoveIf
4240
import com.google.samples.apps.iosched.util.executeAfter
4341

4442
internal class CodelabsAdapter(
4543
private val codelabsActionsHandler: CodelabsActionsHandler,
4644
private val tagViewPool: RecycledViewPool,
47-
private val isMapEnabled: Boolean,
4845
savedState: Bundle?
4946
) : ListAdapter<Any, CodelabsViewHolder>(CodelabsDiffCallback) {
5047

@@ -76,11 +73,9 @@ internal class CodelabsAdapter(
7673
}
7774

7875
override fun getItemViewType(position: Int): Int {
79-
val item = getItem(position)
80-
return when (item) {
76+
return when (val item = getItem(position)) {
8177
is Codelab -> R.layout.item_codelab
8278
is CodelabsInformationCard -> R.layout.item_codelabs_information_card
83-
is CodelabsHeaderItem -> R.layout.item_codelabs_header
8479
else -> throw IllegalStateException("Unknown type: ${item::class.java.simpleName}")
8580
}
8681
}
@@ -104,12 +99,6 @@ internal class CodelabsAdapter(
10499
actionHandler = codelabsActionsHandler
105100
}
106101
)
107-
R.layout.item_codelabs_header -> CodelabsHeaderHolder(
108-
ItemCodelabsHeaderBinding.inflate(inflater, parent, false).apply {
109-
actionHandler = codelabsActionsHandler
110-
mapEnabled = isMapEnabled
111-
}
112-
)
113102
else -> throw IllegalArgumentException("Invalid viewType")
114103
}
115104
}
@@ -126,19 +115,22 @@ internal class CodelabsAdapter(
126115
codelab = item
127116
isExpanded = expandedIds.contains(item.id)
128117
}
129-
holder.itemView.setOnClickListener {
130-
val parent = holder.itemView.parent as? ViewGroup ?: return@setOnClickListener
131-
val expanded = holder.binding.isExpanded ?: false
132-
if (expanded) {
133-
expandedIds.remove(item.id)
134-
} else {
135-
expandedIds.add(item.id)
136-
}
137-
val transition = TransitionInflater.from(holder.itemView.context)
138-
.inflateTransition(R.transition.codelab_toggle)
139-
TransitionManager.beginDelayedTransition(parent, transition)
140-
holder.binding.executeAfter {
141-
isExpanded = !expanded
118+
// In certain configurations the view already has a click listener to start the codelab.
119+
if (!holder.itemView.hasOnClickListeners()) {
120+
holder.itemView.setOnClickListener {
121+
val parent = holder.itemView.parent as? ViewGroup ?: return@setOnClickListener
122+
val expanded = holder.binding.isExpanded ?: false
123+
if (expanded) {
124+
expandedIds.remove(item.id)
125+
} else {
126+
expandedIds.add(item.id)
127+
}
128+
val transition = TransitionInflater.from(holder.itemView.context)
129+
.inflateTransition(R.transition.codelab_toggle)
130+
TransitionManager.beginDelayedTransition(parent, transition)
131+
holder.binding.executeAfter {
132+
isExpanded = !expanded
133+
}
142134
}
143135
}
144136
}
@@ -147,8 +139,6 @@ internal class CodelabsAdapter(
147139
// Marker objects for singleton items
148140
object CodelabsInformationCard
149141

150-
object CodelabsHeaderItem
151-
152142
internal sealed class CodelabsViewHolder(itemView: View) : ViewHolder(itemView) {
153143

154144
class CodelabItemHolder(
@@ -158,18 +148,13 @@ internal sealed class CodelabsViewHolder(itemView: View) : ViewHolder(itemView)
158148
class CodelabsInformationCardHolder(
159149
val binding: ItemCodelabsInformationCardBinding
160150
) : CodelabsViewHolder(binding.root)
161-
162-
class CodelabsHeaderHolder(
163-
val binding: ItemCodelabsHeaderBinding
164-
) : CodelabsViewHolder(binding.root)
165151
}
166152

167153
internal object CodelabsDiffCallback : DiffUtil.ItemCallback<Any>() {
168154

169155
override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
170156
return when {
171157
oldItem === CodelabsInformationCard && newItem === CodelabsInformationCard -> true
172-
oldItem === CodelabsHeaderItem && newItem === CodelabsHeaderItem -> true
173158
oldItem is Codelab && newItem is Codelab -> oldItem.id == newItem.id
174159
else -> false
175160
}

mobile/src/main/java/com/google/samples/apps/iosched/ui/codelabs/CodelabsFragment.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import androidx.fragment.app.activityViewModels
2727
import androidx.fragment.app.viewModels
2828
import androidx.navigation.fragment.findNavController
2929
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
30+
import com.google.samples.apps.iosched.R
3031
import com.google.samples.apps.iosched.databinding.FragmentCodelabsBinding
3132
import com.google.samples.apps.iosched.model.Codelab
3233
import com.google.samples.apps.iosched.shared.analytics.AnalyticsActions
3334
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
3435
import com.google.samples.apps.iosched.shared.di.MapFeatureEnabledFlag
36+
import com.google.samples.apps.iosched.shared.util.consume
3537
import com.google.samples.apps.iosched.ui.MainActivityViewModel
3638
import com.google.samples.apps.iosched.ui.MainNavigationFragment
3739
import com.google.samples.apps.iosched.ui.signin.setupProfileMenuItem
@@ -81,15 +83,27 @@ class CodelabsFragment : MainNavigationFragment(), CodelabsActionsHandler {
8183

8284
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
8385
super.onViewCreated(view, savedInstanceState)
84-
binding.toolbar.setupProfileMenuItem(mainActivityViewModel, viewLifecycleOwner)
86+
binding.toolbar.apply {
87+
setupProfileMenuItem(mainActivityViewModel, viewLifecycleOwner)
88+
menu.findItem(R.id.action_see_on_map)?.isVisible = mapFeatureEnabled
89+
setOnMenuItemClickListener {
90+
when (it.itemId) {
91+
R.id.action_see_on_map -> consume { openCodelabsOnMap() }
92+
R.id.action_codelabs_website -> consume { launchCodelabsWebsite() }
93+
}
94+
false
95+
}
96+
}
8597

8698
codelabsAdapter = CodelabsAdapter(
8799
this,
88100
tagRecycledViewPool,
89-
mapFeatureEnabled,
90101
savedInstanceState
91102
)
92-
binding.codelabsList.adapter = codelabsAdapter
103+
binding.codelabsList.apply {
104+
adapter = codelabsAdapter
105+
setHasFixedSize(true)
106+
}
93107

94108
// Pad the bottom of the RecyclerView so that the content scrolls up above the nav bar
95109
binding.codelabsList.doOnApplyWindowInsets { v, insets, padding ->
@@ -100,7 +114,7 @@ class CodelabsFragment : MainNavigationFragment(), CodelabsActionsHandler {
100114
}
101115

102116
launchAndRepeatWithViewLifecycle {
103-
codelabsViewModel.codelabs.collect {
117+
codelabsViewModel.screenContent.collect {
104118
codelabsAdapter.submitList(it)
105119
}
106120
}
@@ -129,8 +143,10 @@ class CodelabsFragment : MainNavigationFragment(), CodelabsActionsHandler {
129143
}
130144

131145
override fun startCodelab(codelab: Codelab) {
132-
openWebsiteUri(requireContext(), addCodelabsAnalyticsQueryParams(codelab.codelabUrl))
133-
analyticsHelper.logUiEvent("Start codelab \"${codelab.title}\"", AnalyticsActions.CLICK)
146+
if (codelab.hasUrl()) {
147+
openWebsiteUri(requireContext(), addCodelabsAnalyticsQueryParams(codelab.codelabUrl))
148+
analyticsHelper.logUiEvent("Start codelab \"${codelab.title}\"", AnalyticsActions.CLICK)
149+
}
134150
}
135151

136152
private fun addCodelabsAnalyticsQueryParams(url: String): Uri {

mobile/src/main/java/com/google/samples/apps/iosched/ui/codelabs/CodelabsViewModel.kt

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ package com.google.samples.apps.iosched.ui.codelabs
1818

1919
import androidx.lifecycle.ViewModel
2020
import androidx.lifecycle.viewModelScope
21+
import com.google.samples.apps.iosched.model.Codelab
2122
import com.google.samples.apps.iosched.shared.domain.codelabs.GetCodelabsInfoCardShownUseCase
2223
import com.google.samples.apps.iosched.shared.domain.codelabs.LoadCodelabsUseCase
2324
import com.google.samples.apps.iosched.shared.domain.codelabs.SetCodelabsInfoCardShownUseCase
24-
import com.google.samples.apps.iosched.shared.result.Result
2525
import com.google.samples.apps.iosched.shared.result.successOr
2626
import com.google.samples.apps.iosched.util.WhileViewSubscribed
2727
import dagger.hilt.android.lifecycle.HiltViewModel
28-
import kotlinx.coroutines.flow.StateFlow
28+
import kotlinx.coroutines.flow.combine
29+
import kotlinx.coroutines.flow.flow
2930
import kotlinx.coroutines.flow.map
3031
import kotlinx.coroutines.flow.stateIn
3132
import kotlinx.coroutines.launch
@@ -34,25 +35,29 @@ import javax.inject.Inject
3435

3536
@HiltViewModel
3637
class CodelabsViewModel @Inject constructor(
37-
private val loadCodelabsUseCase: LoadCodelabsUseCase,
38-
private val getCodelabsInfoCardShownUseCase: GetCodelabsInfoCardShownUseCase,
38+
loadCodelabsUseCase: LoadCodelabsUseCase,
39+
getCodelabsInfoCardShownUseCase: GetCodelabsInfoCardShownUseCase,
3940
private val setCodelabsInfoCardShownUseCase: SetCodelabsInfoCardShownUseCase
4041
) : ViewModel() {
4142

42-
val codelabs: StateFlow<List<Any>> = getCodelabsInfoCardShownUseCase(Unit).map {
43-
// Refresh codelabs when infoCardShownResult changes.
44-
refreshCodelabs(it)
45-
}.stateIn(viewModelScope, WhileViewSubscribed, emptyList())
43+
private val infoCardDismissed = getCodelabsInfoCardShownUseCase(Unit).map {
44+
it.successOr(false)
45+
}
4646

47-
private suspend fun refreshCodelabs(cardShown: Result<Boolean>): List<Any> {
48-
val codelabs = loadCodelabsUseCase(Unit)
47+
private val codelabs = flow {
48+
emit(loadCodelabsUseCase(Unit).successOr(emptyList()))
49+
}
50+
51+
val screenContent = combine(codelabs, infoCardDismissed) { list, cardDismissed ->
52+
buildScreenContent(list, cardDismissed)
53+
}.stateIn(viewModelScope, WhileViewSubscribed, emptyList())
4954

55+
private fun buildScreenContent(codelabs: List<Codelab>, cardDismissed: Boolean): List<Any> {
5056
val items = mutableListOf<Any>()
51-
if (!cardShown.successOr(false)) {
57+
if (!cardDismissed) {
5258
items.add(CodelabsInformationCard)
5359
}
54-
items.add(CodelabsHeaderItem)
55-
items.addAll(codelabs.successOr(emptyList()))
60+
items.addAll(codelabs)
5661
return items
5762
}
5863

mobile/src/main/java/com/google/samples/apps/iosched/util/ViewBindingAdapters.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import androidx.browser.customtabs.CustomTabsService
3131
import androidx.core.net.toUri
3232
import androidx.core.view.isVisible
3333
import androidx.databinding.BindingAdapter
34+
import androidx.recyclerview.widget.RecyclerView
3435
import androidx.viewpager2.widget.MarginPageTransformer
3536
import androidx.viewpager2.widget.ViewPager2
3637
import com.bumptech.glide.Glide
@@ -41,6 +42,7 @@ import com.google.samples.apps.iosched.R
4142
import com.google.samples.apps.iosched.model.Theme
4243
import com.google.samples.apps.iosched.model.Theme.DARK
4344
import com.google.samples.apps.iosched.widget.CustomSwipeRefreshLayout
45+
import com.google.samples.apps.iosched.widget.SpaceDecoration
4446
import timber.log.Timber
4547

4648
@BindingAdapter("goneUnless")
@@ -125,6 +127,14 @@ fun setText(view: TextView, @StringRes resId: Int) {
125127
}
126128
}
127129

130+
@BindingAdapter("itemSpacing")
131+
fun itemSpacing(view: RecyclerView, dimen: Float) {
132+
val space = dimen.toInt()
133+
if (space > 0) {
134+
view.addItemDecoration(SpaceDecoration(space, space, space, space))
135+
}
136+
}
137+
128138
private const val CHROME_PACKAGE = "com.android.chrome"
129139

130140
@BindingAdapter("websiteLink", "hideWhenEmpty", requireAll = false)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright 2021 Google LLC
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+
~ https://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+
18+
<layout
19+
xmlns:android="http://schemas.android.com/apk/res/android"
20+
xmlns:app="http://schemas.android.com/apk/res-auto"
21+
xmlns:tools="http://schemas.android.com/tools">
22+
23+
<data>
24+
25+
<variable
26+
name="codelab"
27+
type="com.google.samples.apps.iosched.model.Codelab" />
28+
29+
<variable
30+
name="actionHandler"
31+
type="com.google.samples.apps.iosched.ui.codelabs.CodelabsActionsHandler" />
32+
</data>
33+
34+
<com.google.android.material.card.MaterialCardView
35+
android:layout_width="match_parent"
36+
android:layout_height="wrap_content"
37+
android:onClick="@{() -> actionHandler.startCodelab(codelab)}"
38+
app:contentPadding="@dimen/margin_normal">
39+
40+
<androidx.constraintlayout.widget.ConstraintLayout
41+
android:layout_width="match_parent"
42+
android:layout_height="wrap_content"
43+
android:minHeight="?listPreferredItemHeight">
44+
45+
<ImageView
46+
android:id="@+id/codelab_icon"
47+
android:layout_width="32dp"
48+
android:layout_height="32dp"
49+
android:contentDescription="@null"
50+
android:layout_marginEnd="@dimen/margin_normal"
51+
app:layout_constraintStart_toStartOf="parent"
52+
app:layout_constraintEnd_toStartOf="@id/codelab_title"
53+
app:layout_constraintTop_toTopOf="parent"
54+
app:imageUrl="@{codelab.iconUrl}"
55+
app:placeholder="@{@drawable/ic_nav_codelabs}"
56+
tools:src="@drawable/ic_nav_codelabs" />
57+
58+
<TextView
59+
android:id="@+id/codelab_title"
60+
android:layout_width="0dp"
61+
android:layout_height="wrap_content"
62+
android:text="@{codelab.title}"
63+
android:textAlignment="viewStart"
64+
android:textAppearance="?textAppearanceListItem"
65+
app:layout_constraintEnd_toEndOf="parent"
66+
app:layout_constraintStart_toEndOf="@id/codelab_icon"
67+
app:layout_constraintTop_toTopOf="parent"
68+
tools:text="@sample/codelabs.json/codelabs/title" />
69+
70+
<TextView
71+
android:id="@+id/codelab_duration"
72+
android:layout_width="wrap_content"
73+
android:layout_height="wrap_content"
74+
android:layout_marginTop="@dimen/spacing_micro"
75+
app:layout_constraintStart_toStartOf="@id/codelab_title"
76+
app:layout_constraintTop_toBottomOf="@id/codelab_title"
77+
app:codelabDuration="@{codelab.durationMinutes}"
78+
tools:text="@sample/codelabs.json/codelabs/duration"
79+
tools:visibility="visible" />
80+
81+
<com.google.samples.apps.iosched.widget.NoTouchRecyclerView
82+
android:id="@+id/codelab_tags"
83+
android:layout_width="0dp"
84+
android:layout_height="wrap_content"
85+
android:orientation="horizontal"
86+
app:layout_constraintEnd_toEndOf="parent"
87+
app:layout_constraintStart_toStartOf="@id/codelab_title"
88+
app:layout_constraintTop_toBottomOf="@id/codelab_duration"
89+
app:topicTags="@{codelab.tags}"
90+
tools:itemCount="3"
91+
tools:layoutManager="LinearLayoutManager"
92+
tools:listitem="@layout/item_inline_tag" />
93+
94+
<TextView
95+
android:id="@+id/codelab_description"
96+
style="@style/Widget.IOSched.MultilineBody"
97+
android:layout_width="0dp"
98+
android:layout_height="wrap_content"
99+
android:layout_marginTop="@dimen/margin_normal"
100+
android:text="@{codelab.description}"
101+
android:textAlignment="viewStart"
102+
app:layout_constraintEnd_toEndOf="parent"
103+
app:layout_constraintStart_toStartOf="@id/codelab_title"
104+
app:layout_constraintTop_toBottomOf="@id/codelab_tags"
105+
tools:text="@sample/codelabs.json/codelabs/description"
106+
tools:visibility="visible" />
107+
108+
</androidx.constraintlayout.widget.ConstraintLayout>
109+
</com.google.android.material.card.MaterialCardView>
110+
</layout>

0 commit comments

Comments
 (0)