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

Commit 7b338e9

Browse files
jdkorenGerrit Code Review
authored andcommitted
Merge "Toolbar fixes in Schedule" into main
2 parents 2ddf3f2 + 4510eec commit 7b338e9

16 files changed

+303
-305
lines changed

mobile/src/main/java/com/google/samples/apps/iosched/ui/schedule/ScheduleFragment.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.view.View
2222
import android.view.ViewGroup
2323
import android.widget.Toast
2424
import androidx.core.view.updatePaddingRelative
25+
import androidx.fragment.app.Fragment
2526
import androidx.fragment.app.activityViewModels
2627
import androidx.fragment.app.viewModels
2728
import androidx.lifecycle.lifecycleScope
@@ -40,7 +41,6 @@ import com.google.samples.apps.iosched.shared.di.SearchScheduleEnabledFlag
4041
import com.google.samples.apps.iosched.shared.domain.sessions.ConferenceDayIndexer
4142
import com.google.samples.apps.iosched.shared.util.TimeUtils
4243
import com.google.samples.apps.iosched.ui.MainActivityViewModel
43-
import com.google.samples.apps.iosched.ui.MainNavigationFragment
4444
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
4545
import com.google.samples.apps.iosched.ui.schedule.ScheduleNavigationAction.NavigateToSignInDialogAction
4646
import com.google.samples.apps.iosched.ui.schedule.ScheduleNavigationAction.NavigateToSignOutDialogAction
@@ -69,7 +69,7 @@ import javax.inject.Named
6969
* The Schedule page of the top-level Activity.
7070
*/
7171
@AndroidEntryPoint
72-
class ScheduleFragment : MainNavigationFragment() {
72+
class ScheduleFragment : Fragment() {
7373

7474
companion object {
7575
private const val DIALOG_NEED_TO_SIGN_IN = "dialog_need_to_sign_in"
@@ -123,15 +123,15 @@ class ScheduleFragment : MainNavigationFragment() {
123123

124124
snackbar = binding.snackbar
125125
scheduleRecyclerView = binding.recyclerviewSchedule
126-
dayIndicatorRecyclerView = binding.includeScheduleAppbar.dayIndicators
126+
dayIndicatorRecyclerView = binding.dayIndicators
127127
return binding.root
128128
}
129129

130130
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
131131
super.onViewCreated(view, savedInstanceState)
132132

133133
// Set up search menu item
134-
binding.includeScheduleAppbar.toolbar.run {
134+
binding.toolbar.run {
135135
inflateMenu(R.menu.schedule_menu)
136136
menu.findItem(R.id.search).isVisible = searchScheduleFeatureEnabled
137137
setOnMenuItemClickListener { item ->
@@ -145,7 +145,7 @@ class ScheduleFragment : MainNavigationFragment() {
145145
}
146146
}
147147

148-
binding.includeScheduleAppbar.toolbar.setupProfileMenuItem(mainActivityViewModel, this)
148+
binding.toolbar.setupProfileMenuItem(mainActivityViewModel, this)
149149

150150
// Pad the bottom of the RecyclerView so that the content scrolls up above the nav bar
151151
binding.recyclerviewSchedule.doOnApplyWindowInsets { v, insets, padding ->
@@ -174,6 +174,10 @@ class ScheduleFragment : MainNavigationFragment() {
174174
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
175175
onScheduleScrolled()
176176
}
177+
178+
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
179+
scheduleViewModel.userHasInteracted = true
180+
}
177181
})
178182
}
179183
scheduleScroller = JumpSmoothScroller(view.context)
@@ -322,10 +326,6 @@ class ScheduleFragment : MainNavigationFragment() {
322326
}
323327
}
324328

325-
override fun onUserInteraction() {
326-
scheduleViewModel.userHasInteracted = true
327-
}
328-
329329
private fun openSearch() {
330330
findNavController().navigate(ScheduleFragmentDirections.toSearch())
331331
}

mobile/src/main/java/com/google/samples/apps/iosched/ui/schedule/ScheduleTwoPaneFragment.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import android.view.LayoutInflater
2121
import android.view.View
2222
import android.view.ViewGroup
2323
import androidx.activity.OnBackPressedCallback
24+
import androidx.core.view.doOnNextLayout
2425
import androidx.fragment.app.activityViewModels
26+
import androidx.lifecycle.lifecycleScope
2527
import androidx.navigation.NavController
2628
import androidx.navigation.NavDestination
2729
import androidx.navigation.fragment.NavHostFragment
@@ -35,6 +37,7 @@ import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
3537
import com.google.samples.apps.iosched.ui.messages.setupSnackbarManager
3638
import com.google.samples.apps.iosched.ui.signin.SignInDialogFragment
3739
import dagger.hilt.android.AndroidEntryPoint
40+
import kotlinx.coroutines.flow.collect
3841
import javax.inject.Inject
3942

4043
@AndroidEntryPoint
@@ -82,6 +85,10 @@ class ScheduleTwoPaneFragment : MainNavigationFragment() {
8285
detailPaneNavController.addOnDestinationChangedListener(backPressHandler)
8386
}
8487

88+
binding.slidingPaneLayout.doOnNextLayout {
89+
scheduleTwoPaneViewModel.setIsTwoPane(!binding.slidingPaneLayout.isSlideable)
90+
}
91+
8592
scheduleTwoPaneViewModel.navigateToSessionAction.observe(
8693
viewLifecycleOwner,
8794
EventObserver { sessionId ->
@@ -94,6 +101,12 @@ class ScheduleTwoPaneFragment : MainNavigationFragment() {
94101
}
95102
)
96103

104+
lifecycleScope.launchWhenStarted {
105+
scheduleTwoPaneViewModel.returnToListPaneEvents.collect {
106+
binding.slidingPaneLayout.close()
107+
}
108+
}
109+
97110
scheduleTwoPaneViewModel.navigateToSignInDialogAction.observe(
98111
viewLifecycleOwner,
99112
EventObserver {

mobile/src/main/java/com/google/samples/apps/iosched/ui/schedule/ScheduleTwoPaneViewModel.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,32 @@
1717
package com.google.samples.apps.iosched.ui.schedule
1818

1919
import androidx.lifecycle.ViewModel
20+
import com.google.samples.apps.iosched.shared.util.tryOffer
2021
import com.google.samples.apps.iosched.ui.sessioncommon.EventActionsViewModelDelegate
2122
import dagger.hilt.android.lifecycle.HiltViewModel
23+
import kotlinx.coroutines.channels.Channel
24+
import kotlinx.coroutines.flow.MutableStateFlow
25+
import kotlinx.coroutines.flow.StateFlow
26+
import kotlinx.coroutines.flow.receiveAsFlow
2227
import javax.inject.Inject
2328

2429
// Note: clients should obtain this from the Activity.
2530
@HiltViewModel
2631
class ScheduleTwoPaneViewModel @Inject constructor(
2732
eventActionsViewModelDelegate: EventActionsViewModelDelegate
28-
) : ViewModel(), EventActionsViewModelDelegate by eventActionsViewModelDelegate
33+
) : ViewModel(), EventActionsViewModelDelegate by eventActionsViewModelDelegate {
34+
35+
private val _isTwoPane = MutableStateFlow(false)
36+
val isTwoPane: StateFlow<Boolean> = _isTwoPane
37+
38+
private val _returnToListPaneEvents = Channel<Unit>(capacity = Channel.CONFLATED)
39+
val returnToListPaneEvents = _returnToListPaneEvents.receiveAsFlow()
40+
41+
fun setIsTwoPane(isTwoPane: Boolean) {
42+
_isTwoPane.value = isTwoPane
43+
}
44+
45+
fun returnToListPane() {
46+
_returnToListPaneEvents.tryOffer(Unit)
47+
}
48+
}

mobile/src/main/java/com/google/samples/apps/iosched/ui/search/SearchFragment.kt

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ import android.view.View
2424
import android.view.ViewGroup
2525
import android.view.inputmethod.InputMethodManager
2626
import android.widget.SearchView
27+
import androidx.core.view.doOnNextLayout
2728
import androidx.core.view.updatePadding
29+
import androidx.fragment.app.Fragment
2830
import androidx.fragment.app.activityViewModels
2931
import androidx.fragment.app.viewModels
3032
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
3133
import com.google.samples.apps.iosched.R
3234
import com.google.samples.apps.iosched.databinding.FragmentSearchBinding
35+
import com.google.samples.apps.iosched.databinding.SearchActiveFiltersNarrowBinding
36+
import com.google.samples.apps.iosched.databinding.SearchActiveFiltersWideBinding
3337
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
34-
import com.google.samples.apps.iosched.ui.MainNavigationFragment
3538
import com.google.samples.apps.iosched.ui.schedule.ScheduleTwoPaneViewModel
3639
import com.google.samples.apps.iosched.ui.sessioncommon.SessionsAdapter
3740
import com.google.samples.apps.iosched.util.doOnApplyWindowInsets
@@ -42,7 +45,7 @@ import javax.inject.Inject
4245
import javax.inject.Named
4346

4447
@AndroidEntryPoint
45-
class SearchFragment : MainNavigationFragment() {
48+
class SearchFragment : Fragment() {
4649

4750
@Inject
4851
lateinit var analyticsHelper: AnalyticsHelper
@@ -75,7 +78,7 @@ class SearchFragment : MainNavigationFragment() {
7578
super.onViewCreated(view, savedInstanceState)
7679
binding.viewModel = viewModel
7780

78-
binding.includeSearchAppbar.toolbar.apply {
81+
binding.toolbar.apply {
7982
inflateMenu(R.menu.search_menu)
8083
setOnMenuItemClickListener {
8184
if (it.itemId == R.id.action_open_filters) {
@@ -87,7 +90,7 @@ class SearchFragment : MainNavigationFragment() {
8790
}
8891
}
8992

90-
binding.includeSearchAppbar.searchView.apply {
93+
binding.searchView.apply {
9194
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
9295
override fun onQueryTextSubmit(query: String): Boolean {
9396
dismissKeyboard(this@apply)
@@ -129,6 +132,42 @@ class SearchFragment : MainNavigationFragment() {
129132
}
130133
}
131134

135+
/* The active filters on Search can appear in one of two places:
136+
* - In the toolbar next to the search field (on wide screens)
137+
* - In the app bar below the toolbar (on narrow screens)
138+
*
139+
* Normally this could be handled by a resource with a width qualifier, e.g. layout-w720dp.
140+
* However, Search can appear in the list pane of a two pane layout. When both panes are
141+
* visible, a resource qualifier like the above will give us the "wide" state (based on the
142+
* device width) when we actually want the "narrow" state (based on the list pane width).
143+
* Instead we check the toolbar width after first layout and inflate one of two ViewStubs.
144+
*/
145+
binding.toolbar.doOnNextLayout { toolbar ->
146+
val threshold =
147+
resources.getDimensionPixelSize(R.dimen.active_filters_in_toolbar_threshold)
148+
if (toolbar.width >= threshold) {
149+
binding.activeFiltersWideStub.viewStub?.apply {
150+
setOnInflateListener { _, inflated ->
151+
SearchActiveFiltersWideBinding.bind(inflated).apply {
152+
viewModel = this@SearchFragment.viewModel
153+
lifecycleOwner = viewLifecycleOwner
154+
}
155+
}
156+
inflate()
157+
}
158+
} else {
159+
binding.activeFiltersNarrowStub.viewStub?.apply {
160+
setOnInflateListener { _, inflated ->
161+
SearchActiveFiltersNarrowBinding.bind(inflated).apply {
162+
viewModel = this@SearchFragment.viewModel
163+
lifecycleOwner = viewLifecycleOwner
164+
}
165+
}
166+
inflate()
167+
}
168+
}
169+
}
170+
132171
if (savedInstanceState == null) {
133172
// On first entry, show the filters.
134173
findFiltersFragment().showFiltersSheet()
@@ -137,7 +176,7 @@ class SearchFragment : MainNavigationFragment() {
137176
}
138177

139178
override fun onPause() {
140-
dismissKeyboard(binding.includeSearchAppbar.searchView)
179+
dismissKeyboard(binding.searchView)
141180
super.onPause()
142181
}
143182

mobile/src/main/java/com/google/samples/apps/iosched/ui/sessiondetail/SessionDetailFragment.kt

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import android.view.ContextThemeWrapper
2323
import android.view.LayoutInflater
2424
import android.view.View
2525
import android.view.ViewGroup
26+
import androidx.appcompat.content.res.AppCompatResources
2627
import androidx.core.app.ShareCompat
2728
import androidx.core.net.toUri
2829
import androidx.core.view.doOnLayout
2930
import androidx.core.view.forEach
3031
import androidx.core.view.updatePadding
32+
import androidx.fragment.app.Fragment
3133
import androidx.fragment.app.FragmentActivity
3234
import androidx.fragment.app.activityViewModels
3335
import androidx.fragment.app.viewModels
@@ -48,7 +50,6 @@ import com.google.samples.apps.iosched.shared.domain.users.SwapRequestParameters
4850
import com.google.samples.apps.iosched.shared.notifications.AlarmBroadcastReceiver
4951
import com.google.samples.apps.iosched.shared.result.successOr
5052
import com.google.samples.apps.iosched.shared.util.toEpochMilli
51-
import com.google.samples.apps.iosched.ui.MainNavigationFragment
5253
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
5354
import com.google.samples.apps.iosched.ui.reservation.RemoveReservationDialogFragment
5455
import com.google.samples.apps.iosched.ui.reservation.RemoveReservationDialogFragment.Companion.DIALOG_REMOVE_RESERVATION
@@ -78,7 +79,7 @@ import javax.inject.Inject
7879
import javax.inject.Named
7980

8081
@AndroidEntryPoint
81-
class SessionDetailFragment : MainNavigationFragment(), SessionFeedbackFragment.Listener {
82+
class SessionDetailFragment : Fragment(), SessionFeedbackFragment.Listener {
8283

8384
private var shareString = ""
8485

@@ -118,10 +119,14 @@ class SessionDetailFragment : MainNavigationFragment(), SessionFeedbackFragment.
118119

119120
val themedInflater =
120121
inflater.cloneInContext(ContextThemeWrapper(requireActivity(), style.AppTheme_Detail))
121-
binding = FragmentSessionDetailBinding.inflate(themedInflater, container, false).apply {
122-
viewModel = sessionDetailViewModel
123-
lifecycleOwner = viewLifecycleOwner
124-
}
122+
binding = FragmentSessionDetailBinding.inflate(themedInflater, container, false)
123+
return binding.root
124+
}
125+
126+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
127+
super.onViewCreated(view, savedInstanceState)
128+
binding.viewModel = sessionDetailViewModel
129+
binding.lifecycleOwner = viewLifecycleOwner
125130

126131
binding.sessionDetailBottomAppBar.run {
127132
inflateMenu(R.menu.session_detail_menu)
@@ -244,6 +249,22 @@ class SessionDetailFragment : MainNavigationFragment(), SessionFeedbackFragment.
244249
}
245250
}
246251

252+
lifecycleScope.launchWhenStarted {
253+
// Only show the back/up arrow in the toolbar in single-pane configurations.
254+
scheduleTwoPaneViewModel.isTwoPane.collect { isTwoPane ->
255+
if (isTwoPane) {
256+
binding.toolbar.navigationIcon = null
257+
binding.toolbar.setNavigationOnClickListener(null)
258+
} else {
259+
binding.toolbar.navigationIcon =
260+
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_arrow_back)
261+
binding.toolbar.setNavigationOnClickListener {
262+
scheduleTwoPaneViewModel.returnToListPane()
263+
}
264+
}
265+
}
266+
}
267+
247268
// When opened from the post session notification, open the feedback dialog
248269
requireNotNull(arguments).apply {
249270
val sessionId = SessionDetailFragmentArgs.fromBundle(this).sessionId
@@ -257,11 +278,6 @@ class SessionDetailFragment : MainNavigationFragment(), SessionFeedbackFragment.
257278
}
258279
}
259280
}
260-
return binding.root
261-
}
262-
263-
override fun onActivityCreated(savedInstanceState: Bundle?) {
264-
super.onActivityCreated(savedInstanceState)
265281

266282
// Observing the changes from Fragment because data binding doesn't work with menu items.
267283
val menu = binding.sessionDetailBottomAppBar.menu

mobile/src/main/java/com/google/samples/apps/iosched/ui/speaker/SpeakerFragment.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,18 @@ import android.view.LayoutInflater
2222
import android.view.View
2323
import android.view.ViewGroup
2424
import androidx.core.view.updatePadding
25+
import androidx.fragment.app.Fragment
2526
import androidx.fragment.app.activityViewModels
2627
import androidx.fragment.app.viewModels
2728
import androidx.lifecycle.lifecycleScope
29+
import androidx.navigation.fragment.findNavController
2830
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
2931
import androidx.transition.TransitionInflater
3032
import com.google.android.material.appbar.AppBarLayout
3133
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
3234
import com.google.samples.apps.iosched.R
3335
import com.google.samples.apps.iosched.databinding.FragmentSpeakerBinding
3436
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
35-
import com.google.samples.apps.iosched.ui.MainNavigationFragment
3637
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
3738
import com.google.samples.apps.iosched.ui.schedule.ScheduleTwoPaneViewModel
3839
import com.google.samples.apps.iosched.util.doOnApplyWindowInsets
@@ -46,7 +47,7 @@ import javax.inject.Named
4647
* Fragment displaying speaker details and their events.
4748
*/
4849
@AndroidEntryPoint
49-
class SpeakerFragment : MainNavigationFragment(), OnOffsetChangedListener {
50+
class SpeakerFragment : Fragment(), OnOffsetChangedListener {
5051

5152
@Inject lateinit var snackbarMessageManager: SnackbarMessageManager
5253

@@ -67,7 +68,7 @@ class SpeakerFragment : MainNavigationFragment(), OnOffsetChangedListener {
6768
inflater: LayoutInflater,
6869
container: ViewGroup?,
6970
savedInstanceState: Bundle?
70-
): View? {
71+
): View {
7172

7273
sharedElementEnterTransition =
7374
TransitionInflater.from(context).inflateTransition(R.transition.speaker_shared_enter)
@@ -123,9 +124,14 @@ class SpeakerFragment : MainNavigationFragment(), OnOffsetChangedListener {
123124
insets.systemWindowInsetTop * 2
124125
}
125126
}
127+
128+
binding.toolbar.setNavigationOnClickListener {
129+
findNavController().navigateUp()
130+
}
131+
126132
lifecycleScope.launchWhenStarted {
127133
speakerViewModel.speakerUserSessions.collect {
128-
speakerAdapter.speakerSessions = it ?: emptyList()
134+
speakerAdapter.speakerSessions = it
129135
}
130136
}
131137

0 commit comments

Comments
 (0)