Skip to content

Commit 0264816

Browse files
committed
Merge remote-tracking branch 'upstream/master' into 2561-fix-sqlite-crash
2 parents c73037d + 5dfb936 commit 0264816

File tree

11 files changed

+669
-244
lines changed

11 files changed

+669
-244
lines changed

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ internal sealed interface QuestionnaireAdapterItem {
3434
val responses: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>,
3535
val title: String,
3636
) : QuestionnaireAdapterItem
37+
38+
data class Navigation(val questionnaireNavigationUIState: QuestionnaireNavigationUIState) :
39+
QuestionnaireAdapterItem
3740
}

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
2424
import com.google.android.fhir.datacapture.contrib.views.PhoneNumberViewHolderFactory
2525
import com.google.android.fhir.datacapture.extensions.inflate
2626
import com.google.android.fhir.datacapture.extensions.itemControl
27+
import com.google.android.fhir.datacapture.views.NavigationViewHolder
2728
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
2829
import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory
2930
import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory
@@ -69,6 +70,13 @@ internal class QuestionnaireEditAdapter(
6970
),
7071
)
7172
}
73+
ViewType.Type.NAVIGATION -> {
74+
ViewHolder.NavigationHolder(
75+
NavigationViewHolder(
76+
parent.inflate(R.layout.pagination_navigation_view),
77+
),
78+
)
79+
}
7280
}
7381
}
7482

@@ -122,6 +130,10 @@ internal class QuestionnaireEditAdapter(
122130
holder as ViewHolder.RepeatedGroupHeaderHolder
123131
holder.viewHolder.bind(item)
124132
}
133+
is QuestionnaireAdapterItem.Navigation -> {
134+
holder as ViewHolder.NavigationHolder
135+
holder.viewHolder.bind(item.questionnaireNavigationUIState)
136+
}
125137
}
126138
}
127139

@@ -143,6 +155,10 @@ internal class QuestionnaireEditAdapter(
143155
// All of the repeated group headers will be rendered identically
144156
subtype = 0
145157
}
158+
is QuestionnaireAdapterItem.Navigation -> {
159+
type = ViewType.Type.NAVIGATION
160+
subtype = 0xFFFFFF
161+
}
146162
}
147163
return ViewType.from(type = type, subtype = subtype).viewType
148164
}
@@ -174,6 +190,7 @@ internal class QuestionnaireEditAdapter(
174190
enum class Type {
175191
QUESTION,
176192
REPEATED_GROUP_HEADER,
193+
NAVIGATION,
177194
}
178195
}
179196

@@ -269,6 +286,8 @@ internal class QuestionnaireEditAdapter(
269286

270287
class RepeatedGroupHeaderHolder(val viewHolder: RepeatedGroupHeaderItemViewHolder) :
271288
ViewHolder(viewHolder.itemView)
289+
290+
class NavigationHolder(val viewHolder: NavigationViewHolder) : ViewHolder(viewHolder.itemView)
272291
}
273292

274293
internal companion object {
@@ -296,6 +315,7 @@ internal object DiffCallbacks {
296315
newItem is QuestionnaireAdapterItem.RepeatedGroupHeader &&
297316
oldItem.index == newItem.index
298317
}
318+
is QuestionnaireAdapterItem.Navigation -> newItem is QuestionnaireAdapterItem.Navigation
299319
}
300320

301321
override fun areContentsTheSame(
@@ -311,6 +331,10 @@ internal object DiffCallbacks {
311331
newItem is QuestionnaireAdapterItem.RepeatedGroupHeader &&
312332
oldItem.responses == newItem.responses
313333
}
334+
is QuestionnaireAdapterItem.Navigation -> {
335+
newItem is QuestionnaireAdapterItem.Navigation &&
336+
oldItem.questionnaireNavigationUIState == newItem.questionnaireNavigationUIState
337+
}
314338
}
315339
}
316340

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt

Lines changed: 44 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import android.os.Bundle
2121
import android.view.LayoutInflater
2222
import android.view.View
2323
import android.view.ViewGroup
24-
import android.widget.Button
2524
import androidx.annotation.VisibleForTesting
2625
import androidx.appcompat.view.ContextThemeWrapper
2726
import androidx.core.content.res.use
@@ -34,6 +33,7 @@ import androidx.lifecycle.lifecycleScope
3433
import androidx.recyclerview.widget.LinearLayoutManager
3534
import androidx.recyclerview.widget.RecyclerView
3635
import com.google.android.fhir.datacapture.validation.Invalid
36+
import com.google.android.fhir.datacapture.views.NavigationViewHolder
3737
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory
3838
import com.google.android.material.progressindicator.LinearProgressIndicator
3939
import kotlinx.coroutines.launch
@@ -94,58 +94,44 @@ class QuestionnaireFragment : Fragment() {
9494
view.findViewById<RecyclerView>(R.id.questionnaire_edit_recycler_view)
9595
val questionnaireReviewRecyclerView =
9696
view.findViewById<RecyclerView>(R.id.questionnaire_review_recycler_view)
97-
val paginationPreviousButton = view.findViewById<View>(R.id.pagination_previous_button)
98-
paginationPreviousButton.setOnClickListener { viewModel.goToPreviousPage() }
99-
val paginationNextButton = view.findViewById<View>(R.id.pagination_next_button)
100-
paginationNextButton.setOnClickListener { viewModel.goToNextPage() }
101-
view.findViewById<Button>(R.id.cancel_questionnaire).setOnClickListener {
97+
98+
// This container frame floats at the bottom of the view to make navigation controls visible at
99+
// all times when the user scrolls. Use
100+
// [QuestionnaireFragment.Builder.setShowNavigationInDefaultLongScroll] to disable this.
101+
val bottomNavContainerFrame = view.findViewById<View>(R.id.bottom_nav_container_frame)
102+
103+
viewModel.setOnCancelButtonClickListener {
102104
QuestionnaireCancelDialogFragment()
103105
.show(requireActivity().supportFragmentManager, QuestionnaireCancelDialogFragment.TAG)
104106
}
105-
106-
view
107-
.findViewById<Button>(R.id.submit_questionnaire)
108-
.apply {
109-
text =
110-
requireArguments()
111-
.getString(EXTRA_SUBMIT_BUTTON_TEXT, getString(R.string.submit_questionnaire))
112-
}
113-
.setOnClickListener {
114-
lifecycleScope.launch {
115-
viewModel.validateQuestionnaireAndUpdateUI().let { validationMap ->
116-
if (validationMap.values.flatten().filterIsInstance<Invalid>().isEmpty()) {
117-
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
118-
} else {
119-
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
120-
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
121-
QuestionnaireValidationErrorMessageDialogFragment()
122-
.show(
123-
requireActivity().supportFragmentManager,
124-
QuestionnaireValidationErrorMessageDialogFragment.TAG,
125-
)
126-
}
107+
viewModel.setOnSubmitButtonClickListener {
108+
lifecycleScope.launch {
109+
viewModel.validateQuestionnaireAndUpdateUI().let { validationMap ->
110+
if (validationMap.values.flatten().filterIsInstance<Invalid>().isEmpty()) {
111+
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
112+
} else {
113+
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
114+
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
115+
QuestionnaireValidationErrorMessageDialogFragment()
116+
.show(
117+
requireActivity().supportFragmentManager,
118+
QuestionnaireValidationErrorMessageDialogFragment.TAG,
119+
)
127120
}
128121
}
129122
}
123+
}
130124
val questionnaireProgressIndicator: LinearProgressIndicator =
131125
view.findViewById(R.id.questionnaire_progress_indicator)
132126
val questionnaireEditAdapter =
133127
QuestionnaireEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get())
134128
val questionnaireReviewAdapter = QuestionnaireReviewAdapter()
135129

136-
val submitButton = requireView().findViewById<Button>(R.id.submit_questionnaire)
137-
val cancelButton = requireView().findViewById<Button>(R.id.cancel_questionnaire)
138-
139130
val reviewModeEditButton =
140131
view.findViewById<View>(R.id.review_mode_edit_button).apply {
141132
setOnClickListener { viewModel.setReviewMode(false) }
142133
}
143134

144-
val reviewModeButton =
145-
view.findViewById<View>(R.id.review_mode_button).apply {
146-
setOnClickListener { viewModel.setReviewMode(true) }
147-
}
148-
149135
questionnaireEditRecyclerView.adapter = questionnaireEditAdapter
150136
val linearLayoutManager = LinearLayoutManager(view.context)
151137
questionnaireEditRecyclerView.layoutManager = linearLayoutManager
@@ -163,23 +149,20 @@ class QuestionnaireFragment : Fragment() {
163149
// Set items
164150
questionnaireEditRecyclerView.visibility = View.GONE
165151
questionnaireReviewAdapter.submitList(
166-
state.items.filterIsInstance<QuestionnaireAdapterItem.Question>(),
152+
state.items,
167153
)
168154
questionnaireReviewRecyclerView.visibility = View.VISIBLE
169-
170-
// Set button visibility
171-
submitButton.visibility = if (displayMode.showSubmitButton) View.VISIBLE else View.GONE
172-
cancelButton.visibility = if (displayMode.showCancelButton) View.VISIBLE else View.GONE
173-
174-
reviewModeButton.visibility = View.GONE
175155
reviewModeEditButton.visibility =
176156
if (displayMode.showEditButton) {
177157
View.VISIBLE
178158
} else {
179159
View.GONE
180160
}
181-
paginationPreviousButton.visibility = View.GONE
182-
paginationNextButton.visibility = View.GONE
161+
162+
// Set bottom navigation
163+
bottomNavContainerFrame.visibility = View.VISIBLE
164+
NavigationViewHolder(bottomNavContainerFrame)
165+
.bind(state.bottomNavItems.single().questionnaireNavigationUIState)
183166

184167
// Hide progress indicator
185168
questionnaireProgressIndicator.visibility = View.GONE
@@ -189,25 +172,12 @@ class QuestionnaireFragment : Fragment() {
189172
questionnaireReviewRecyclerView.visibility = View.GONE
190173
questionnaireEditAdapter.submitList(state.items)
191174
questionnaireEditRecyclerView.visibility = View.VISIBLE
192-
193-
// Set button visibility
194-
submitButton.visibility =
195-
if (displayMode.pagination.showSubmitButton) View.VISIBLE else View.GONE
196-
cancelButton.visibility =
197-
if (displayMode.pagination.showCancelButton) View.VISIBLE else View.GONE
198-
reviewModeButton.visibility =
199-
if (displayMode.pagination.showReviewButton) View.VISIBLE else View.GONE
200175
reviewModeEditButton.visibility = View.GONE
201176

202-
if (displayMode.pagination.isPaginated) {
203-
paginationPreviousButton.visibility =
204-
if (displayMode.pagination.hasPreviousPage) View.VISIBLE else View.GONE
205-
paginationNextButton.visibility =
206-
if (displayMode.pagination.hasNextPage) View.VISIBLE else View.GONE
207-
} else {
208-
paginationPreviousButton.visibility = View.GONE
209-
paginationNextButton.visibility = View.GONE
210-
}
177+
// Set bottom navigation
178+
bottomNavContainerFrame.visibility = View.VISIBLE
179+
NavigationViewHolder(bottomNavContainerFrame)
180+
.bind(state.bottomNavItems.single().questionnaireNavigationUIState)
211181

212182
// Set progress indicator
213183
questionnaireProgressIndicator.visibility = View.VISIBLE
@@ -241,13 +211,9 @@ class QuestionnaireFragment : Fragment() {
241211
is DisplayMode.InitMode -> {
242212
questionnaireReviewRecyclerView.visibility = View.GONE
243213
questionnaireEditRecyclerView.visibility = View.GONE
244-
paginationPreviousButton.visibility = View.GONE
245-
paginationNextButton.visibility = View.GONE
246214
questionnaireProgressIndicator.visibility = View.GONE
247-
submitButton.visibility = View.GONE
248-
cancelButton.visibility = View.GONE
249-
reviewModeButton.visibility = View.GONE
250215
reviewModeEditButton.visibility = View.GONE
216+
bottomNavContainerFrame.visibility = View.GONE
251217
}
252218
}
253219
}
@@ -425,6 +391,14 @@ class QuestionnaireFragment : Fragment() {
425391
*/
426392
fun setShowCancelButton(value: Boolean) = apply { args.add(EXTRA_SHOW_CANCEL_BUTTON to value) }
427393

394+
/**
395+
* A [Boolean] extra to show questionnaire page as a default/long scroll with the
396+
* previous/next/submit buttons anchored to bottom/end of page. Default is false.
397+
*/
398+
fun setShowNavigationInDefaultLongScroll(value: Boolean) = apply {
399+
args.add(EXTRA_SHOW_NAVIGATION_IN_DEFAULT_LONG_SCROLL to value)
400+
}
401+
428402
@VisibleForTesting fun buildArgs() = bundleOf(*args.toTypedArray())
429403

430404
/** @return A [QuestionnaireFragment] with provided [Bundle] arguments. */
@@ -524,6 +498,9 @@ class QuestionnaireFragment : Fragment() {
524498

525499
internal const val EXTRA_SUBMIT_BUTTON_TEXT = "submit-button-text"
526500

501+
internal const val EXTRA_SHOW_NAVIGATION_IN_DEFAULT_LONG_SCROLL =
502+
"show-navigation-in-default-long-scroll"
503+
527504
fun builder() = Builder()
528505
}
529506

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2023-2024 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+
* 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.google.android.fhir.datacapture
18+
19+
sealed class QuestionnaireNavigationViewUIState(val isShown: Boolean, val isEnabled: Boolean) {
20+
data object Hidden : QuestionnaireNavigationViewUIState(isShown = false, isEnabled = false)
21+
22+
data class Enabled(val labelText: String? = null, val onClickAction: () -> Unit) :
23+
QuestionnaireNavigationViewUIState(isShown = true, isEnabled = true)
24+
}
25+
26+
data class QuestionnaireNavigationUIState(
27+
val navPrevious: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
28+
val navNext: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
29+
val navSubmit: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
30+
val navCancel: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
31+
val navReview: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
32+
)

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireReviewAdapter.kt

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2023 Google LLC
2+
* Copyright 2022-2024 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,21 +16,65 @@
1616

1717
package com.google.android.fhir.datacapture
1818

19+
import android.view.LayoutInflater
1920
import android.view.ViewGroup
2021
import androidx.recyclerview.widget.ListAdapter
22+
import androidx.recyclerview.widget.RecyclerView
23+
import com.google.android.fhir.datacapture.views.NavigationViewHolder
2124
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
2225
import com.google.android.fhir.datacapture.views.factories.ReviewViewHolderFactory
2326

2427
/** List Adapter used to bind answers to [QuestionnaireItemViewHolder] in review mode. */
2528
internal class QuestionnaireReviewAdapter :
26-
ListAdapter<QuestionnaireAdapterItem.Question, QuestionnaireItemViewHolder>(
27-
DiffCallbacks.QUESTIONS,
29+
ListAdapter<QuestionnaireAdapterItem, RecyclerView.ViewHolder>(
30+
DiffCallbacks.ITEMS,
2831
) {
29-
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder {
30-
return ReviewViewHolderFactory.create(parent)
32+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
33+
val typedViewType = QuestionnaireEditAdapter.ViewType.parse(viewType)
34+
return when (typedViewType.type) {
35+
QuestionnaireEditAdapter.ViewType.Type.QUESTION -> ReviewViewHolderFactory.create(parent)
36+
QuestionnaireEditAdapter.ViewType.Type.NAVIGATION ->
37+
NavigationViewHolder(
38+
LayoutInflater.from(parent.context)
39+
.inflate(R.layout.pagination_navigation_view, parent, false),
40+
)
41+
QuestionnaireEditAdapter.ViewType.Type.REPEATED_GROUP_HEADER -> TODO()
42+
}
3143
}
3244

33-
override fun onBindViewHolder(holder: QuestionnaireItemViewHolder, position: Int) {
34-
holder.bind(getItem(position).item)
45+
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
46+
when (val item = getItem(position)) {
47+
is QuestionnaireAdapterItem.Question -> {
48+
holder as QuestionnaireItemViewHolder
49+
holder.bind(item.item)
50+
}
51+
is QuestionnaireAdapterItem.Navigation -> {
52+
holder as NavigationViewHolder
53+
holder.bind(item.questionnaireNavigationUIState)
54+
}
55+
is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO()
56+
}
57+
}
58+
59+
override fun getItemViewType(position: Int): Int {
60+
// Because we have multiple Item subtypes, we will pack two ints into the item view type.
61+
62+
// The first 8 bits will be represented by this type, which is unique for each Item subclass.
63+
val type: QuestionnaireEditAdapter.ViewType.Type
64+
// The last 24 bits will be represented by this subtype, which will further divide each Item
65+
// subclass into more view types.
66+
val subtype: Int
67+
when (getItem(position)) {
68+
is QuestionnaireAdapterItem.Question -> {
69+
type = QuestionnaireEditAdapter.ViewType.Type.QUESTION
70+
subtype = 0xFFFFFF
71+
}
72+
is QuestionnaireAdapterItem.Navigation -> {
73+
type = QuestionnaireEditAdapter.ViewType.Type.NAVIGATION
74+
subtype = 0xFFFFFF
75+
}
76+
is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO()
77+
}
78+
return QuestionnaireEditAdapter.ViewType.from(type = type, subtype = subtype).viewType
3579
}
3680
}

0 commit comments

Comments
 (0)