Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the next button navigation view to a default long scroll #2134

Merged
merged 20 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0ccb597
Configurable bottom navigation to be part of page scroll
LZRS Sep 21, 2023
538055b
Merge branch 'master' into 1764-scroll-to-bottom-to-enable-next
LZRS Sep 22, 2023
bb4b5a2
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS Sep 25, 2023
42bcee5
Merge branch 'master' into 1764-scroll-to-bottom-to-enable-next
f-odhiambo Sep 27, 2023
3742e58
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS Oct 18, 2023
a2e410c
Merge branch 'master' into 1764-scroll-to-bottom-to-enable-next
LZRS Oct 18, 2023
eaec177
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS Oct 25, 2023
5ed31e7
Shared layout for bottom navigation items, for scroll and sticky
LZRS Oct 26, 2023
d291427
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS Oct 31, 2023
fbb6ba3
Use separate recyclerview for bottom navigation items
LZRS Nov 1, 2023
d7ab77e
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS Feb 28, 2024
b2db3d9
Merge branch 'master' into 1764-scroll-to-bottom-to-enable-next
LZRS Mar 12, 2024
e62105f
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS Mar 21, 2024
a5e0df3
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS Apr 23, 2024
09a0010
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
LZRS May 1, 2024
53242bb
Merge branch 'master' into 1764-scroll-to-bottom-to-enable-next
jingtang10 Jun 21, 2024
3fbb72d
Fix build errors
jingtang10 Jun 21, 2024
716c855
Remove unnecessary disabled state
jingtang10 Jun 21, 2024
a0c61d9
Delete the unnecessary navigation recycler view
jingtang10 Jun 21, 2024
23d038d
Simplify unnecessary .apply
jingtang10 Jun 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,4 +22,7 @@ import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
internal sealed interface QuestionnaireAdapterItem {
/** A row for a question in a Questionnaire RecyclerView. */
data class Question(val item: QuestionnaireViewItem) : QuestionnaireAdapterItem

data class Navigation(val questionnaireNavigationUIState: QuestionnaireNavigationUIState) :
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
QuestionnaireAdapterItem
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 Google LLC
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,11 +16,14 @@

package com.google.android.fhir.datacapture

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.contrib.views.PhoneNumberViewHolderFactory
import com.google.android.fhir.datacapture.extensions.itemControl
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory
Expand All @@ -46,16 +49,21 @@ internal class QuestionnaireEditAdapter(
private val questionnaireItemViewHolderMatchers:
List<QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatcher> =
emptyList(),
) : ListAdapter<QuestionnaireAdapterItem, QuestionnaireItemViewHolder>(DiffCallbacks.ITEMS) {
) : ListAdapter<QuestionnaireAdapterItem, RecyclerView.ViewHolder>(DiffCallbacks.ITEMS) {
/**
* @param viewType the integer value of the [QuestionnaireViewHolderType] used to render the
* [QuestionnaireViewItem].
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val typedViewType = ViewType.parse(viewType)
val subtype = typedViewType.subtype
return when (typedViewType.type) {
ViewType.Type.QUESTION -> onCreateViewHolderQuestion(parent = parent, subtype = subtype)
ViewType.Type.NAVIGATION ->
NavigationViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.pagination_navigation_view, parent, false),
)
}
}

Expand Down Expand Up @@ -99,11 +107,16 @@ internal class QuestionnaireEditAdapter(
return viewHolderFactory.create(parent)
}

override fun onBindViewHolder(holder: QuestionnaireItemViewHolder, position: Int) {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is QuestionnaireAdapterItem.Question -> {
holder as QuestionnaireItemViewHolder
holder.bind(item.item)
}
is QuestionnaireAdapterItem.Navigation -> {
holder as NavigationViewHolder
holder.bind(item.questionnaireNavigationUIState)
}
}
}

Expand All @@ -120,6 +133,10 @@ internal class QuestionnaireEditAdapter(
type = ViewType.Type.QUESTION
subtype = getItemViewTypeForQuestion(item.item)
}
is QuestionnaireAdapterItem.Navigation -> {
type = ViewType.Type.NAVIGATION
subtype = 0xFFFFFF
}
}
return ViewType.from(type = type, subtype = subtype).viewType
}
Expand Down Expand Up @@ -150,6 +167,7 @@ internal class QuestionnaireEditAdapter(

enum class Type {
QUESTION,
NAVIGATION,
}
}

Expand Down Expand Up @@ -261,6 +279,7 @@ internal object DiffCallbacks {
newItem is QuestionnaireAdapterItem.Question &&
QUESTIONS.areItemsTheSame(oldItem, newItem)
}
is QuestionnaireAdapterItem.Navigation -> newItem is QuestionnaireAdapterItem.Navigation
}

override fun areContentsTheSame(
Expand All @@ -272,6 +291,10 @@ internal object DiffCallbacks {
newItem is QuestionnaireAdapterItem.Question &&
QUESTIONS.areContentsTheSame(oldItem, newItem)
}
is QuestionnaireAdapterItem.Navigation -> {
newItem is QuestionnaireAdapterItem.Navigation &&
oldItem.questionnaireNavigationUIState == newItem.questionnaireNavigationUIState
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.VisibleForTesting
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.res.use
Expand Down Expand Up @@ -94,58 +93,41 @@ class QuestionnaireFragment : Fragment() {
view.findViewById<RecyclerView>(R.id.questionnaire_edit_recycler_view)
val questionnaireReviewRecyclerView =
view.findViewById<RecyclerView>(R.id.questionnaire_review_recycler_view)
val paginationPreviousButton = view.findViewById<View>(R.id.pagination_previous_button)
paginationPreviousButton.setOnClickListener { viewModel.goToPreviousPage() }
val paginationNextButton = view.findViewById<View>(R.id.pagination_next_button)
paginationNextButton.setOnClickListener { viewModel.goToNextPage() }
view.findViewById<Button>(R.id.cancel_questionnaire).setOnClickListener {
val bottomNavigationRecyclerView =
view.findViewById<RecyclerView>(R.id.questionnaire_bottom_navigation_recycler_view)

viewModel.setOnCancelButtonClickListener {
QuestionnaireCancelDialogFragment()
.show(requireActivity().supportFragmentManager, QuestionnaireCancelDialogFragment.TAG)
}

view
.findViewById<Button>(R.id.submit_questionnaire)
.apply {
text =
requireArguments()
.getString(EXTRA_SUBMIT_BUTTON_TEXT, getString(R.string.submit_questionnaire))
}
.setOnClickListener {
lifecycleScope.launch {
viewModel.validateQuestionnaireAndUpdateUI().let { validationMap ->
if (validationMap.values.flatten().filterIsInstance<Invalid>().isEmpty()) {
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
} else {
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
QuestionnaireValidationErrorMessageDialogFragment()
.show(
requireActivity().supportFragmentManager,
QuestionnaireValidationErrorMessageDialogFragment.TAG,
)
}
viewModel.setOnSubmitButtonClickListener {
lifecycleScope.launch {
viewModel.validateQuestionnaireAndUpdateUI().let { validationMap ->
if (validationMap.values.flatten().filterIsInstance<Invalid>().isEmpty()) {
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
} else {
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
QuestionnaireValidationErrorMessageDialogFragment()
.show(
requireActivity().supportFragmentManager,
QuestionnaireValidationErrorMessageDialogFragment.TAG,
)
}
}
}
}
val questionnaireProgressIndicator: LinearProgressIndicator =
view.findViewById(R.id.questionnaire_progress_indicator)
val questionnaireEditAdapter =
QuestionnaireEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get())
val questionnaireReviewAdapter = QuestionnaireReviewAdapter()

val submitButton = requireView().findViewById<Button>(R.id.submit_questionnaire)
val cancelButton = requireView().findViewById<Button>(R.id.cancel_questionnaire)

val reviewModeEditButton =
view.findViewById<View>(R.id.review_mode_edit_button).apply {
setOnClickListener { viewModel.setReviewMode(false) }
}

val reviewModeButton =
view.findViewById<View>(R.id.review_mode_button).apply {
setOnClickListener { viewModel.setReviewMode(true) }
}

questionnaireEditRecyclerView.adapter = questionnaireEditAdapter
val linearLayoutManager = LinearLayoutManager(view.context)
questionnaireEditRecyclerView.layoutManager = linearLayoutManager
Expand All @@ -155,6 +137,10 @@ class QuestionnaireFragment : Fragment() {
questionnaireReviewRecyclerView.adapter = questionnaireReviewAdapter
questionnaireReviewRecyclerView.layoutManager = LinearLayoutManager(view.context)

val bottomNavigationAdapter = QuestionnaireNavigationAdapter()
bottomNavigationRecyclerView.adapter = bottomNavigationAdapter
bottomNavigationRecyclerView.layoutManager = LinearLayoutManager(view.context)

// Listen to updates from the view model.
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
viewModel.questionnaireStateFlow.collect { state ->
Expand All @@ -163,23 +149,17 @@ class QuestionnaireFragment : Fragment() {
// Set items
questionnaireEditRecyclerView.visibility = View.GONE
questionnaireReviewAdapter.submitList(
state.items.filterIsInstance<QuestionnaireAdapterItem.Question>(),
state.items,
)
bottomNavigationAdapter.submitList(state.bottomNavItems)
questionnaireReviewRecyclerView.visibility = View.VISIBLE

// Set button visibility
submitButton.visibility = if (displayMode.showSubmitButton) View.VISIBLE else View.GONE
cancelButton.visibility = if (displayMode.showCancelButton) View.VISIBLE else View.GONE

reviewModeButton.visibility = View.GONE
reviewModeEditButton.visibility =
if (displayMode.showEditButton) {
View.VISIBLE
} else {
View.GONE
}
paginationPreviousButton.visibility = View.GONE
paginationNextButton.visibility = View.GONE

// Hide progress indicator
questionnaireProgressIndicator.visibility = View.GONE
Expand All @@ -188,27 +168,10 @@ class QuestionnaireFragment : Fragment() {
// Set items
questionnaireReviewRecyclerView.visibility = View.GONE
questionnaireEditAdapter.submitList(state.items)
bottomNavigationAdapter.submitList(state.bottomNavItems)
questionnaireEditRecyclerView.visibility = View.VISIBLE

// Set button visibility
submitButton.visibility =
if (displayMode.pagination.showSubmitButton) View.VISIBLE else View.GONE
cancelButton.visibility =
if (displayMode.pagination.showCancelButton) View.VISIBLE else View.GONE
reviewModeButton.visibility =
if (displayMode.pagination.showReviewButton) View.VISIBLE else View.GONE
reviewModeEditButton.visibility = View.GONE

if (displayMode.pagination.isPaginated) {
paginationPreviousButton.visibility =
if (displayMode.pagination.hasPreviousPage) View.VISIBLE else View.GONE
paginationNextButton.visibility =
if (displayMode.pagination.hasNextPage) View.VISIBLE else View.GONE
} else {
paginationPreviousButton.visibility = View.GONE
paginationNextButton.visibility = View.GONE
}

// Set progress indicator
questionnaireProgressIndicator.visibility = View.VISIBLE
if (displayMode.pagination.isPaginated) {
Expand Down Expand Up @@ -241,13 +204,9 @@ class QuestionnaireFragment : Fragment() {
is DisplayMode.InitMode -> {
questionnaireReviewRecyclerView.visibility = View.GONE
questionnaireEditRecyclerView.visibility = View.GONE
paginationPreviousButton.visibility = View.GONE
paginationNextButton.visibility = View.GONE
questionnaireProgressIndicator.visibility = View.GONE
submitButton.visibility = View.GONE
cancelButton.visibility = View.GONE
reviewModeButton.visibility = View.GONE
reviewModeEditButton.visibility = View.GONE
bottomNavigationRecyclerView.visibility = View.GONE
}
}
}
Expand Down Expand Up @@ -425,6 +384,14 @@ class QuestionnaireFragment : Fragment() {
*/
fun setShowCancelButton(value: Boolean) = apply { args.add(EXTRA_SHOW_CANCEL_BUTTON to value) }

/**
* A [Boolean] extra to show questionnaire page as a default/long scroll with the
* previous/next/submit buttons anchored to bottom/end of page. Default is false.
*/
fun setShowNavigationInDefaultLongScroll(value: Boolean) = apply {
args.add(EXTRA_SHOW_NAVIGATION_IN_DEFAULT_LONG_SCROLL to value)
}

@VisibleForTesting fun buildArgs() = bundleOf(*args.toTypedArray())

/** @return A [QuestionnaireFragment] with provided [Bundle] arguments. */
Expand Down Expand Up @@ -524,6 +491,9 @@ class QuestionnaireFragment : Fragment() {

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

internal const val EXTRA_SHOW_NAVIGATION_IN_DEFAULT_LONG_SCROLL =
"show-navigation-in-default-long-scroll"

fun builder() = Builder()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
import com.google.android.fhir.datacapture.views.factories.ReviewViewHolderFactory

internal class QuestionnaireNavigationAdapter :
ListAdapter<QuestionnaireAdapterItem, RecyclerView.ViewHolder>(DiffCallbacks.ITEMS) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val typedViewType = QuestionnaireEditAdapter.ViewType.parse(viewType)
return when (typedViewType.type) {
QuestionnaireEditAdapter.ViewType.Type.QUESTION -> ReviewViewHolderFactory.create(parent)
QuestionnaireEditAdapter.ViewType.Type.NAVIGATION ->
NavigationViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.pagination_navigation_view, parent, false),
)
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is QuestionnaireAdapterItem.Question -> {
holder as QuestionnaireItemViewHolder
holder.bind(item.item)
}
is QuestionnaireAdapterItem.Navigation -> {
holder as NavigationViewHolder
holder.bind(item.questionnaireNavigationUIState)
}
}
}

override fun getItemViewType(position: Int): Int {
// Because we have multiple Item subtypes, we will pack two ints into the item view type.

// The first 8 bits will be represented by this type, which is unique for each Item subclass.
val type: QuestionnaireEditAdapter.ViewType.Type
// The last 24 bits will be represented by this subtype, which will further divide each Item
// subclass into more view types.
val subtype: Int
when (getItem(position)) {
is QuestionnaireAdapterItem.Question -> {
type = QuestionnaireEditAdapter.ViewType.Type.QUESTION
subtype = 0xFFFFFF
}
is QuestionnaireAdapterItem.Navigation -> {
type = QuestionnaireEditAdapter.ViewType.Type.NAVIGATION
subtype = 0xFFFFFF
}
}
return QuestionnaireEditAdapter.ViewType.from(type = type, subtype = subtype).viewType
}
}
Loading
Loading