Skip to content
Snippets Groups Projects
Unverified Commit 7c56e97e authored by Lukas Lechner's avatar Lukas Lechner Committed by GitHub
Browse files

Screen: Vaccination List (EXPOSUREAPP-6734) (#3082)


* Create first version of incomplete vaccination list screen with mock data

* Add UiState for VaccinationListFragment.kt

* Set Toolbar Overlay

* Fine tune fragment_vaccination_list.xml

* Use ModularAdapter

* Add click listener to navigate to detail fragment

* Fix nav_graph issue

* Adjust layouts for night mode

* Fix vaccination card colors

* Fix mock data

* Revert changes in Project.xml

* Add TODO

* Show blue background on complete vaccination status

* Show subtitle on complete vaccination status

* Add VaccinationListCertificateCardItem.kt

* Show Refresh button on complete vaccination status

* Add fragment in vaccination_nav_graph.xml

* Connect Vaccination List ViewModel to Vaccination Repository

* Create new mock data

* Adapt VaccinationTestFragment.kt

* Try to make sonar happy

* Use VaccinationCertificate and ProofCertificate interfaces instead of VaccinationCertificateV1 and ProofCertificateV1

* Adjust title+subtitle alignment

* Adjust text size in vaccination_list_incomplete_top_card.xml

* Use isEligibleForProofCertificate boolean to show 'refresh' or 'register new vaccination' button

* Adjust subtitle color of vaccination_list_certificate_card.xml and vaccination_list_name_card.xml

* Fix package declaration of VaccinationListAdapter

* Update VaccinationListAdapter instead of re-creating a new one each time we observe a uiState change

* Refactor Navigation

* Show sample QR-code

* Add some layout improvements

* Adjust background height for COMPLETE and INCOMPLETE state

* Show both buttons in VaccinationListFragment - Refresh button and register Vaccination button

* Use separate flow to generate qrCode

* Refactor CardItems to be inlined into their ViewHolders

* Ignore onClick listener in equals() and hashCode() of VaccinationListVaccinationCardItem.kt to avoid re-drawings

* Fix Lint

* Fix Detekt

Co-authored-by: default avatarMohamed <mohamed.metwalli@sap.com>
Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
parent 575036cc
No related branches found
No related tags found
No related merge requests found
Showing
with 850 additions and 1 deletion
......@@ -24,6 +24,15 @@ class VaccinationTestFragment : Fragment(R.layout.fragment_test_vaccination), Au
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.openVaccinationList.setOnClickListener {
doNavigate(
VaccinationTestFragmentDirections.actionVaccinationTestFragmentToVaccinationListFragment(
"vaccinated-person-identifier"
)
)
}
binding.openVaccinationDetailsIncomplete.setOnClickListener {
doNavigate(
VaccinationTestFragmentDirections
......
......@@ -31,6 +31,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/open_vaccination_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Vaccination List"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/open_vaccination_details_complete"
android:layout_width="wrap_content"
......@@ -39,7 +49,7 @@
android:text="Vaccination details - complete"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
app:layout_constraintTop_toBottomOf="@+id/open_vaccination_list" />
<com.google.android.material.button.MaterialButton
android:id="@+id/open_vaccination_details_incomplete"
......
......@@ -170,9 +170,26 @@
android:name="de.rki.coronawarnapp.vaccination.ui.VaccinationTestFragment"
android:label="VaccinationTestFragment"
tools:layout="@layout/fragment_test_vaccination" >
<action
android:id="@+id/action_vaccinationTestFragment_to_vaccinationDetailsFragment"
app:destination="@id/vaccinationDetailsFragment" />
<action
android:id="@+id/action_vaccinationTestFragment_to_vaccinationListFragment"
app:destination="@id/vaccinationListFragment" />
</fragment>
<fragment
android:id="@+id/vaccinationListFragment"
android:name="de.rki.coronawarnapp.vaccination.ui.list.VaccinationListFragment"
android:label="fragment_vaccination_list"
tools:layout="@layout/fragment_vaccination_list">
<argument
android:name="vaccinatedPersonId"
app:argType="string" />
<action
android:id="@+id/action_vaccinationListFragment_to_vaccinationDetailsFragment"
app:destination="@id/vaccinationDetailsFragment" />
</fragment>
<fragment
......
......@@ -16,12 +16,15 @@ import java.util.Date
import java.util.TimeZone
import java.util.concurrent.TimeUnit
@Suppress("TooManyFunctions")
object TimeAndDateExtensions {
private const val MS_TO_DAYS = (1000 * 60 * 60 * 24)
private const val MS_TO_HOURS = (1000 * 60 * 60)
private const val MS_TO_SECONDS = 1000
private val dayFormatter = DateTimeFormat.forPattern("dd.MM.yyyy")
fun getCurrentHourUTC(): Int = DateTime(Instant.now(), DateTimeZone.UTC).hourOfDay().get()
fun Date.toServerFormat(): String =
......@@ -104,6 +107,16 @@ object TimeAndDateExtensions {
fun Instant.toUserTimeZone() = this.toDateTime(DateTimeZone.forTimeZone(TimeZone.getDefault()))
fun Instant.toLocalDateUserTz(): LocalDate = this.toUserTimeZone().toLocalDate()
/**
* Returns a readable date String with the format "dd.MM.yyyy" like 23.05.1989 of an Instant
*/
fun Instant.toDayFormat() = toString(dayFormatter)
/**
* Returns a readable date String with the format "dd.MM.yyyy" like 23.05.1989 of a LocalDate
*/
fun LocalDate.toDayFormat() = toString(dayFormatter)
}
typealias HourInterval = Long
......@@ -4,9 +4,15 @@ import dagger.Module
import dagger.android.ContributesAndroidInjector
import de.rki.coronawarnapp.vaccination.ui.details.VaccinationDetailsFragment
import de.rki.coronawarnapp.vaccination.ui.details.VaccinationDetailsFragmentModule
import de.rki.coronawarnapp.vaccination.ui.list.VaccinationListFragment
import de.rki.coronawarnapp.vaccination.ui.list.VaccinationListFragmentModule
@Module
abstract class VaccinationUIModule {
@ContributesAndroidInjector(modules = [VaccinationListFragmentModule::class])
abstract fun vaccinationListFragment(): VaccinationListFragment
@ContributesAndroidInjector(modules = [VaccinationDetailsFragmentModule::class])
abstract fun vaccinationDetailsFragment(): VaccinationDetailsFragment
}
package de.rki.coronawarnapp.vaccination.ui.list
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import com.google.android.material.appbar.AppBarLayout
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.FragmentVaccinationListBinding
import de.rki.coronawarnapp.ui.view.onOffsetChange
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.lists.diffutil.update
import de.rki.coronawarnapp.util.ui.doNavigate
import de.rki.coronawarnapp.util.ui.popBackStack
import de.rki.coronawarnapp.util.ui.viewBindingLazy
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
import de.rki.coronawarnapp.vaccination.ui.list.VaccinationListViewModel.Event.NavigateToVaccinationCertificateDetails
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListAdapter
import javax.inject.Inject
class VaccinationListFragment : Fragment(R.layout.fragment_vaccination_list), AutoInject {
@Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
private val args by navArgs<VaccinationListFragmentArgs>()
private val binding: FragmentVaccinationListBinding by viewBindingLazy()
private val viewModel: VaccinationListViewModel by cwaViewModelsAssisted(
factoryProducer = { viewModelFactory },
constructorCall = { factory, _ ->
factory as VaccinationListViewModel.Factory
factory.create(
vaccinatedPersonIdentifier = args.vaccinatedPersonId
)
}
)
private val adapter = VaccinationListAdapter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
toolbar.setNavigationOnClickListener {
popBackStack()
}
recyclerViewVaccinationList.adapter = adapter
viewModel.uiState.observe(viewLifecycleOwner) { uiState ->
bindViews(uiState)
}
viewModel.events.observe(viewLifecycleOwner) { event ->
when (event) {
is NavigateToVaccinationCertificateDetails -> doNavigate(
VaccinationListFragmentDirections.actionVaccinationListFragmentToVaccinationDetailsFragment(
event.vaccinationCertificateId
)
)
}
}
registerNewVaccinationButton.setOnClickListener {
Toast.makeText(requireContext(), "TODO \uD83D\uDEA7", Toast.LENGTH_LONG).show()
}
refreshButton.setOnClickListener {
Toast.makeText(requireContext(), "TODO \uD83D\uDEA7", Toast.LENGTH_LONG).show()
}
}
}
private fun FragmentVaccinationListBinding.bindViews(uiState: VaccinationListViewModel.UiState) = with(uiState) {
adapter.update(listItems)
val isVaccinationComplete = vaccinationStatus == VaccinatedPerson.Status.COMPLETE
val background = if (isVaccinationComplete) {
R.drawable.vaccination_compelete_gradient
} else {
R.drawable.vaccination_incomplete
}
expandedImage.setImageResource(background)
subtitle.isVisible = isVaccinationComplete
appBarLayout.onOffsetChange { titleAlpha, subtitleAlpha ->
title.alpha = titleAlpha
subtitle.alpha = subtitleAlpha
}
setToolbarOverlay(isVaccinationComplete)
}
private fun setToolbarOverlay(isVaccinationComplete: Boolean) {
// subtitle is only visible when vaccination is complete
val bottomTextView = if (isVaccinationComplete) binding.subtitle else binding.title
val deviceWidth = requireContext().resources.displayMetrics.widthPixels
val params: CoordinatorLayout.LayoutParams = binding.recyclerViewVaccinationList.layoutParams
as (CoordinatorLayout.LayoutParams)
val textParams = bottomTextView.layoutParams as (LinearLayout.LayoutParams)
val divider = if (isVaccinationComplete) 2 else 3
textParams.bottomMargin = (deviceWidth / divider) - 24 /* 24 is space between screen border and Card */
bottomTextView.requestLayout()
val behavior: AppBarLayout.ScrollingViewBehavior = params.behavior as (AppBarLayout.ScrollingViewBehavior)
behavior.overlayTop = (deviceWidth / divider) - 24
}
}
package de.rki.coronawarnapp.vaccination.ui.list
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
@Module
abstract class VaccinationListFragmentModule {
@Binds
@IntoMap
@CWAViewModelKey(VaccinationListViewModel::class)
abstract fun vaccinationListFragment(
factory: VaccinationListViewModel.Factory
): CWAViewModelFactory<out CWAViewModel>
}
package de.rki.coronawarnapp.vaccination.ui.list
import android.graphics.Bitmap
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.presencetracing.checkins.qrcode.QrCodeGenerator
import de.rki.coronawarnapp.util.TimeAndDateExtensions.toDayFormat
import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUserTz
import de.rki.coronawarnapp.util.TimeStamper
import de.rki.coronawarnapp.util.ui.SingleLiveEvent
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
import de.rki.coronawarnapp.vaccination.core.ProofCertificate
import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson.Status.COMPLETE
import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListCertificateCardItemVH.VaccinationListCertificateCardItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListIncompleteTopCardItemVH.VaccinationListIncompleteTopCardItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListNameCardItemVH.VaccinationListNameCardItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListVaccinationCardItemVH.VaccinationListVaccinationCardItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import org.joda.time.Days
import org.joda.time.LocalDate
class VaccinationListViewModel @AssistedInject constructor(
private val vaccinationRepository: VaccinationRepository,
private val timeStamper: TimeStamper,
private val qrCodeGenerator: QrCodeGenerator,
@Assisted private val vaccinatedPersonIdentifier: String
) : CWAViewModel() {
val events = SingleLiveEvent<Event>()
val vaccinationInfoFlow = vaccinationRepository.vaccinationInfos.map { vaccinatedPersonSet ->
// TODO: use the line below once the repository returns actual values
// val vaccinatedPerson = vaccinatedPersonSet.single { it.identifier.code == vaccinatedPersonIdentifier }
}
private val proofQrCode: Flow<Bitmap?> = vaccinationRepository.vaccinationInfos.transform { vaccinationInfos ->
emit(null)
// TODO: use actual values from repository instead of these mocked ones
val proofCertificates = setOf(
getMockProofCertificate()
)
if (proofCertificates.isNotEmpty()) {
emit(qrCodeGenerator.createQrCode("TODO create qrCode from actual value"))
}
}
val uiState: LiveData<UiState> = combine(vaccinationInfoFlow, proofQrCode) { vaccinatedPerson, proofQrCode ->
// For now, use mock data
val vaccinationStatus = COMPLETE
// val vaccinationStatus = COMPLETE
val vaccinationCertificates = setOf(
getMockVaccinationCertificate(),
getMockVaccinationCertificate().copy(
doseNumber = 2
)
)
val proofCertificates = setOf(
getMockProofCertificate()
)
val listItems = assembleItemList(
vaccinationCertificates = vaccinationCertificates,
proofCertificates = proofCertificates,
firstName = "François-Joan",
lastName = "d'Arsøns - van Halen",
dateOfBirth = LocalDate.parse("2009-02-28"),
vaccinationStatus,
proofQrCode
)
UiState(
listItems,
vaccinationStatus = vaccinationStatus
)
}.catch {
// TODO Error Handling in an upcoming subtask
}.asLiveData()
// TODO: after using actual values from the repository, we only pass VaccinatedPerson here instead of all these
// arguments
@Suppress("LongParameterList")
private fun assembleItemList(
vaccinationCertificates: Set<VaccinationCertificate>,
proofCertificates: Set<ProofCertificate>,
firstName: String,
lastName: String,
dateOfBirth: LocalDate,
vaccinationStatus: VaccinatedPerson.Status,
proofQrCode: Bitmap?
) = mutableListOf<VaccinationListItem>().apply {
if (vaccinationStatus == COMPLETE) {
if (proofCertificates.isNotEmpty()) {
val proofCertificate = proofCertificates.first()
val expiresAt = proofCertificate.expiresAt.toLocalDateUserTz()
val today = timeStamper.nowUTC.toLocalDateUserTz()
val remainingValidityInDays = Days.daysBetween(today, expiresAt).days
add(
VaccinationListCertificateCardItem(
qrCode = proofQrCode,
remainingValidityInDays = remainingValidityInDays
)
)
}
} else {
add(VaccinationListIncompleteTopCardItem)
}
add(
VaccinationListNameCardItem(
fullName = "$firstName $lastName",
dayOfBirth = dateOfBirth.toDayFormat()
)
)
vaccinationCertificates.forEach { vaccinationCertificate ->
with(vaccinationCertificate) {
add(
VaccinationListVaccinationCardItem(
vaccinationCertificateId = certificateId,
doseNumber = doseNumber.toString(),
totalSeriesOfDoses = totalSeriesOfDoses.toString(),
vaccinatedAt = vaccinatedAt.toDayFormat(),
vaccinationStatus = vaccinationStatus,
isFinalVaccination =
doseNumber == totalSeriesOfDoses,
onCardClick = { certificateId ->
events.postValue(Event.NavigateToVaccinationCertificateDetails(certificateId))
}
)
)
}
}
}.toList()
data class UiState(
val listItems: List<VaccinationListItem>,
val vaccinationStatus: VaccinatedPerson.Status
)
sealed class Event {
data class NavigateToVaccinationCertificateDetails(val vaccinationCertificateId: String) : Event()
}
@AssistedFactory
interface Factory : CWAViewModelFactory<VaccinationListViewModel> {
fun create(
vaccinatedPersonIdentifier: String
): VaccinationListViewModel
}
}
package de.rki.coronawarnapp.vaccination.ui.list
import de.rki.coronawarnapp.ui.Country
import de.rki.coronawarnapp.vaccination.core.ProofCertificate
import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
import org.joda.time.Duration
import org.joda.time.Instant
import org.joda.time.LocalDate
/**
* Mock Data needed for UI development while backend connection is not yet available
* Can be removed later
*/
internal fun getMockVaccinationCertificate() = MockVaccinationCertificate(
firstName = "François-Joan",
lastName = "FRANCOIS<JOAN",
dateOfBirth = LocalDate.parse("2009-02-28"),
vaccinatedAt = LocalDate.parse("2021-04-22"),
vaccineName = "vaccineName",
vaccineManufacturer = "vaccineManufactorer",
medicalProductName = "medicalProductName",
doseNumber = 1,
totalSeriesOfDoses = 2,
certificateIssuer = "certificateIssuer",
certificateCountry = Country.AT,
certificateId = "certificate Id",
personIdentifier = getPersonIdentifier()
)
internal fun getMockProofCertificate() = MockProofCertificate(
personIdentifier = getPersonIdentifier(),
expiresAt = Instant.now().plus(Duration.standardDays(3)),
firstName = "François-Joan",
lastName = "FRANCOIS<JOAN",
dateOfBirth = LocalDate.parse("2009-02-28"),
vaccinatedAt = LocalDate.parse("2021-04-22"),
vaccineName = "vaccineName",
vaccineManufacturer = "vaccineManufactorer",
medicalProductName = "medicalProductName",
doseNumber = 1,
totalSeriesOfDoses = 2,
certificateIssuer = "certificateIssuer",
certificateId = "certificate Id"
)
fun getPersonIdentifier() = VaccinatedPersonIdentifier(
dateOfBirth = LocalDate.parse("2009-02-28"),
lastNameStandardized = "DARSONS<VAN<HALEN",
firstNameStandardized = "FRANCOIS<JOAN"
)
internal data class MockProofCertificate(
override val personIdentifier: VaccinatedPersonIdentifier,
override val expiresAt: Instant,
override val firstName: String?,
override val lastName: String,
override val dateOfBirth: LocalDate,
override val vaccineName: String,
override val medicalProductName: String,
override val vaccineManufacturer: String,
override val doseNumber: Int,
override val totalSeriesOfDoses: Int,
override val vaccinatedAt: LocalDate,
override val certificateIssuer: String,
override val certificateId: String
) : ProofCertificate
internal data class MockVaccinationCertificate(
override val firstName: String?,
override val lastName: String,
override val dateOfBirth: LocalDate,
override val vaccinatedAt: LocalDate,
override val vaccineName: String,
override val vaccineManufacturer: String,
override val medicalProductName: String,
override val doseNumber: Int,
override val totalSeriesOfDoses: Int,
override val certificateIssuer: String,
override val certificateCountry: Country,
override val certificateId: String,
override val personIdentifier: VaccinatedPersonIdentifier
) : VaccinationCertificate
package de.rki.coronawarnapp.vaccination.ui.list.adapter
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding
import de.rki.coronawarnapp.util.lists.BindableVH
import de.rki.coronawarnapp.util.lists.HasStableId
import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffUtilAdapter
import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffer
import de.rki.coronawarnapp.util.lists.modular.ModularAdapter
import de.rki.coronawarnapp.util.lists.modular.mods.DataBinderMod
import de.rki.coronawarnapp.util.lists.modular.mods.StableIdMod
import de.rki.coronawarnapp.util.lists.modular.mods.TypedVHCreatorMod
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListCertificateCardItemVH
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListCertificateCardItemVH.VaccinationListCertificateCardItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListIncompleteTopCardItemVH
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListIncompleteTopCardItemVH.VaccinationListIncompleteTopCardItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListNameCardItemVH
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListNameCardItemVH.VaccinationListNameCardItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListVaccinationCardItemVH
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListVaccinationCardItemVH.VaccinationListVaccinationCardItem
class VaccinationListAdapter :
ModularAdapter<VaccinationListAdapter.ItemVH<VaccinationListItem, ViewBinding>>(),
AsyncDiffUtilAdapter<VaccinationListItem> {
override val asyncDiffer: AsyncDiffer<VaccinationListItem> = AsyncDiffer(adapter = this)
init {
modules.addAll(
listOf(
StableIdMod(data),
DataBinderMod<VaccinationListItem, ItemVH<VaccinationListItem, ViewBinding>>(data),
TypedVHCreatorMod({ data[it] is VaccinationListIncompleteTopCardItem }) {
VaccinationListIncompleteTopCardItemVH(it)
},
TypedVHCreatorMod({ data[it] is VaccinationListNameCardItem }) {
VaccinationListNameCardItemVH(it)
},
TypedVHCreatorMod({ data[it] is VaccinationListVaccinationCardItem }) {
VaccinationListVaccinationCardItemVH(it)
},
TypedVHCreatorMod({ data[it] is VaccinationListCertificateCardItem }) {
VaccinationListCertificateCardItemVH(it)
}
)
)
}
override fun getItemCount(): Int {
return data.size
}
abstract class ItemVH<Item : VaccinationListItem, VB : ViewBinding>(
@LayoutRes layoutRes: Int,
parent: ViewGroup
) : ModularAdapter.VH(layoutRes, parent), BindableVH<Item, VB>
}
interface VaccinationListItem : HasStableId
package de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder
import android.graphics.Bitmap
import android.view.ViewGroup
import androidx.core.view.isVisible
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.VaccinationListCertificateCardBinding
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListAdapter
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListCertificateCardItemVH.VaccinationListCertificateCardItem
class VaccinationListCertificateCardItemVH(parent: ViewGroup) :
VaccinationListAdapter.ItemVH<VaccinationListCertificateCardItem, VaccinationListCertificateCardBinding>(
layoutRes = R.layout.vaccination_list_certificate_card,
parent = parent
) {
override val viewBinding: Lazy<VaccinationListCertificateCardBinding> = lazy {
VaccinationListCertificateCardBinding.bind(itemView)
}
override val onBindData: VaccinationListCertificateCardBinding
.(item: VaccinationListCertificateCardItem, payloads: List<Any>) -> Unit =
{ item, _ ->
when (item.qrCode) {
null -> progressBar.isVisible = true
else -> {
qrCodeImage.setImageBitmap(item.qrCode)
progressBar.isVisible = false
}
}
certificateCardSubtitle.text =
context.getString(R.string.vaccination_list_certificate_card_subtitle, item.remainingValidityInDays)
}
data class VaccinationListCertificateCardItem(
val qrCode: Bitmap?,
val remainingValidityInDays: Int
) :
VaccinationListItem {
override val stableId = this.hashCode().toLong()
}
}
package de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder
import android.view.ViewGroup
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.VaccinationListIncompleteTopCardBinding
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListAdapter
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListIncompleteTopCardItemVH.VaccinationListIncompleteTopCardItem
class VaccinationListIncompleteTopCardItemVH(parent: ViewGroup) :
VaccinationListAdapter.ItemVH<VaccinationListIncompleteTopCardItem, VaccinationListIncompleteTopCardBinding>(
layoutRes = R.layout.vaccination_list_incomplete_top_card,
parent = parent
) {
override val viewBinding: Lazy<VaccinationListIncompleteTopCardBinding> = lazy {
VaccinationListIncompleteTopCardBinding.bind(itemView)
}
override val onBindData: VaccinationListIncompleteTopCardBinding
.(item: VaccinationListIncompleteTopCardItem, payloads: List<Any>) -> Unit = { _, _ -> // NOOP
}
object VaccinationListIncompleteTopCardItem : VaccinationListItem {
override val stableId = this.hashCode().toLong()
}
}
package de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder
import android.view.ViewGroup
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.VaccinationListNameCardBinding
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListAdapter
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListNameCardItemVH.VaccinationListNameCardItem
class VaccinationListNameCardItemVH(parent: ViewGroup) :
VaccinationListAdapter.ItemVH<VaccinationListNameCardItem, VaccinationListNameCardBinding>(
layoutRes = R.layout.vaccination_list_name_card,
parent = parent
) {
override val viewBinding: Lazy<VaccinationListNameCardBinding> = lazy {
VaccinationListNameCardBinding.bind(itemView)
}
override val onBindData: VaccinationListNameCardBinding
.(item: VaccinationListNameCardItem, payloads: List<Any>) -> Unit =
{ item, _ ->
nameCardTitle.text = item.fullName
nameCardSubtitle.text = context.getString(
R.string.vaccination_list_name_card_subtitle,
item.dayOfBirth
)
}
data class VaccinationListNameCardItem(val fullName: String, val dayOfBirth: String) : VaccinationListItem {
override val stableId = this.hashCode().toLong()
}
}
package de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder
import android.view.ViewGroup
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.VaccinationListVaccinationCardBinding
import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson.Status.COMPLETE
import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson.Status.INCOMPLETE
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListAdapter
import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListItem
import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListVaccinationCardItemVH.VaccinationListVaccinationCardItem
import java.util.Objects
class VaccinationListVaccinationCardItemVH(
parent: ViewGroup,
) :
VaccinationListAdapter.ItemVH<VaccinationListVaccinationCardItem, VaccinationListVaccinationCardBinding>(
layoutRes = R.layout.vaccination_list_vaccination_card,
parent = parent
) {
override val viewBinding: Lazy<VaccinationListVaccinationCardBinding> = lazy {
VaccinationListVaccinationCardBinding.bind(itemView)
}
override val onBindData:
VaccinationListVaccinationCardBinding.(item: VaccinationListVaccinationCardItem, payloads: List<Any>) -> Unit =
{ item, _ ->
with(item) {
root.setOnClickListener {
onCardClick.invoke(vaccinationCertificateId)
}
vaccinationCardTitle.text = context.getString(
R.string.vaccination_list_vaccination_card_title,
doseNumber,
totalSeriesOfDoses
)
vaccinationCardSubtitle.text = context.getString(
R.string.vaccination_list_vaccination_card_subtitle,
vaccinatedAt
)
val iconRes = when (item.vaccinationStatus) {
INCOMPLETE -> {
if (isFinalVaccination) {
R.drawable.ic_vaccination_incomplete_final
} else {
R.drawable.ic_vaccination_incomplete
}
}
COMPLETE -> {
if (isFinalVaccination) {
R.drawable.ic_vaccination_complete_final
} else {
R.drawable.ic_vaccination_complete
}
}
}
vaccinationIcon.setImageResource(iconRes)
}
}
data class VaccinationListVaccinationCardItem(
val vaccinationCertificateId: String,
val doseNumber: String,
val totalSeriesOfDoses: String,
val vaccinatedAt: String,
val vaccinationStatus: VaccinatedPerson.Status,
val isFinalVaccination: Boolean,
val onCardClick: (String) -> Unit
) : VaccinationListItem {
override val stableId: Long = Objects.hash(
vaccinationCertificateId,
doseNumber,
totalSeriesOfDoses,
vaccinatedAt,
vaccinationStatus,
isFinalVaccination
).toLong()
// Ignore onCardClick Listener in equals() to avoid re-drawing when only the click listener is updated
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as VaccinationListVaccinationCardItem
if (vaccinationCertificateId != other.vaccinationCertificateId) return false
if (doseNumber != other.doseNumber) return false
if (totalSeriesOfDoses != other.totalSeriesOfDoses) return false
if (vaccinatedAt != other.vaccinatedAt) return false
if (vaccinationStatus != other.vaccinationStatus) return false
if (isFinalVaccination != other.isFinalVaccination) return false
if (stableId != other.stableId) return false
return true
}
// Ignore onCardClick Listener in equals() to avoid re-drawing when only the click listener is updated
override fun hashCode(): Int {
var result = vaccinationCertificateId.hashCode()
result = 31 * result + doseNumber.hashCode()
result = 31 * result + totalSeriesOfDoses.hashCode()
result = 31 * result + vaccinatedAt.hashCode()
result = 31 * result + vaccinationStatus.hashCode()
result = 31 * result + isFinalVaccination.hashCode()
result = 31 * result + stableId.hashCode()
return result
}
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="19dp"
android:height="16dp"
android:viewportWidth="19"
android:viewportHeight="16">
<path
android:pathData="M11.9186,0.7474L10.4286,2.3179L14.9685,7.103H0.9918V9.197H14.9685L10.4286,13.9821L11.9186,15.5525L18.9419,8.15L11.9186,0.7474Z"
android:fillColor="#A7A7A7"
android:fillType="evenOdd"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="19dp"
android:height="16dp"
android:viewportWidth="19"
android:viewportHeight="16">
<path
android:pathData="M11.9186,0.7475L10.4286,2.318L14.9685,7.1031H0.9918V9.197H14.9685L10.4286,13.9821L11.9186,15.5526L18.9419,8.15L11.9186,0.7475Z"
android:fillColor="#17191A"
android:fillAlpha="0.6"
android:fillType="evenOdd"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="88dp"
android:height="95dp"
android:viewportWidth="88"
android:viewportHeight="95">
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z"
android:fillColor="#FDD207"/>
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="35.3456"
android:startX="-1.95773E-6"
android:endY="57.3292"
android:endX="82.1951"
android:type="linear">
<item android:offset="0" android:color="#FF0093C8"/>
<item android:offset="1" android:color="#FF007FAD"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z"
android:fillColor="#FDD207"/>
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="35.3456"
android:startX="-1.95773E-6"
android:endY="57.3292"
android:endX="82.1951"
android:type="linear">
<item android:offset="0" android:color="#FF0093C8"/>
<item android:offset="1" android:color="#FF007FAD"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M44.6667,21L23,29.125V45.6188C23,59.2958 32.2354,72.0521 44.6667,75.1667C57.0979,72.0521 66.3333,59.2958 66.3333,45.6188V29.125L44.6667,21ZM60.9167,45.6188C60.9167,56.4521 54.0104,66.4729 44.6667,69.5333C35.3229,66.4729 28.4167,56.4792 28.4167,45.6188V32.8896L44.6667,26.7958L60.9167,32.8896V45.6188Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M44.6665,71.0834V26.5834V24.0834L26.1665,32.0834V49.0834L31.1665,63.0834L44.6665,71.0834Z"
android:fillColor="#ffffff"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="88dp"
android:height="95dp"
android:viewportWidth="88"
android:viewportHeight="95">
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z"
android:fillColor="#FDD207"/>
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="35.3456"
android:startX="-1.95773E-6"
android:endY="57.3292"
android:endX="82.1951"
android:type="linear">
<item android:offset="0" android:color="#FF0093C8"/>
<item android:offset="1" android:color="#FF007FAD"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M44.5,21L23,30.8182V45.5455C23,59.1682 32.1733,71.9073 44.5,75C56.8267,71.9073 66,59.1682 66,45.5455V30.8182L44.5,21Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M32.525,46.7909L29,50.2335L39,60L59,40.4671L55.475,37L39,53.0902L32.525,46.7909Z"
android:fillColor="#0186B6"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="88dp"
android:height="95dp"
android:viewportWidth="88"
android:viewportHeight="95">
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z"
android:fillColor="#616F7E"/>
<path
android:pathData="M44.6667,21L23,29.125V45.6188C23,59.2958 32.2354,72.0521 44.6667,75.1667C57.0979,72.0521 66.3333,59.2958 66.3333,45.6188V29.125L44.6667,21ZM60.9167,45.6188C60.9167,56.4521 54.0104,66.4729 44.6667,69.5333C35.3229,66.4729 28.4167,56.4792 28.4167,45.6188V32.8896L44.6667,26.7958L60.9167,32.8896V45.6188Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M44.6665,71.0834V26.5834V24.0834L26.1665,32.0834V49.0834L31.1665,63.0834L44.6665,71.0834Z"
android:fillColor="#ffffff"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="88dp"
android:height="95dp"
android:viewportWidth="88"
android:viewportHeight="95">
<path
android:pathData="M0,4C0,1.7909 1.7909,0 4,0H84C86.2091,0 88,1.7909 88,4V91C88,93.2091 86.2091,95 84,95H4C1.7909,95 0,93.2091 0,91V4Z"
android:fillColor="#616F7E"/>
<path
android:pathData="M44.5,21L23,30.8182V45.5455C23,59.1682 32.1733,71.9073 44.5,75C56.8267,71.9073 66,59.1682 66,45.5455V30.8182L44.5,21Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M32.525,46.7909L29,50.2335L39,60L59,40.4671L55.475,37L39,53.0902L32.525,46.7909Z"
android:fillColor="#616F7E"/>
</vector>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment