Attempt to give accessibility focus to the first item of the RecyclerView when the recycler view is updated (screen change), to improve screen reader behavior.

This commit is contained in:
Benoit Marty 2023-05-10 18:45:25 +02:00
parent 20fedc87fe
commit 7a65a51ee1
2 changed files with 113 additions and 2 deletions

View file

@ -21,12 +21,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
@ -36,15 +42,26 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding
import im.vector.app.features.crypto.verification.VerificationAction
import im.vector.app.features.qrcode.QrCodeScannerActivity
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(),
class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(),
SelfVerificationController.InteractionListener {
@Inject lateinit var controller: SelfVerificationController
private var requestAccessibilityFocus: Boolean = false
private val modelBuildListener: OnModelBuildFinishedListener = OnModelBuildFinishedListener {
if (requestAccessibilityFocus) {
// Do not use giveAccessibilityFocusOnce() here.
views.bottomSheetVerificationRecyclerView.layoutManager?.findViewByPosition(0)?.giveAccessibilityFocus()
requestAccessibilityFocus = false
// Note: it does not work when the recycler view is displayed for the first time, because findViewByPosition(0) is null
}
}
private val viewModel by parentFragmentViewModel(SelfVerificationViewModel::class)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding {
@ -58,17 +75,22 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil
override fun onDestroyView() {
views.bottomSheetVerificationRecyclerView.cleanup()
controller.removeModelBuildListener(modelBuildListener)
controller.listener = null
super.onDestroyView()
}
private fun setupRecyclerView() {
views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.addModelBuildListener(modelBuildListener)
controller.listener = this
}
override fun invalidate() = withState(viewModel) { state ->
// Timber.w("VALR: invalidate with State: ${state.pendingRequest}")
if (state.isNewScreen()) {
requestAccessibilityFocus = true
}
controller.update(state)
}
@ -176,4 +198,41 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil
override fun declineRequest() {
viewModel.handle(VerificationAction.CancelPendingVerification)
}
private var currentScreenIndex = -1
private fun SelfVerificationViewState.isNewScreen(): Boolean {
val newIndex = toScreenIndex()
if (currentScreenIndex == newIndex) {
return false
}
currentScreenIndex = newIndex
return true
}
private fun SelfVerificationViewState.toScreenIndex(): Int {
return if (activeAction !is UserAction.None) {
when (activeAction) {
UserAction.ConfirmCancel -> 30
UserAction.None -> 31
}
} else {
when (pendingRequest) {
is Fail -> 0
is Loading -> 1
is Success -> when (pendingRequest.invoke().state) {
EVerificationState.WaitingForReady -> 10
EVerificationState.Requested -> 11
EVerificationState.Ready -> 12
EVerificationState.Started -> 13
EVerificationState.WeStarted -> 14
EVerificationState.WaitingForDone -> 15
EVerificationState.Done -> 16
EVerificationState.Cancelled -> 17
EVerificationState.HandledByOtherSession -> 18
}
Uninitialized -> 2
}
}
}
}

View file

@ -21,12 +21,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.giveAccessibilityFocus
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
@ -36,6 +42,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding
import im.vector.app.features.crypto.verification.VerificationAction
import im.vector.app.features.qrcode.QrCodeScannerActivity
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import timber.log.Timber
import javax.inject.Inject
@ -45,6 +52,16 @@ class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChild
@Inject lateinit var controller: UserVerificationController
private var requestAccessibilityFocus: Boolean = false
private val modelBuildListener: OnModelBuildFinishedListener = OnModelBuildFinishedListener {
if (requestAccessibilityFocus) {
// Do not use giveAccessibilityFocusOnce() here.
views.bottomSheetVerificationRecyclerView.layoutManager?.findViewByPosition(0)?.giveAccessibilityFocus()
requestAccessibilityFocus = false
// Note: it does not work when the recycler view is displayed for the first time, because findViewByPosition(0) is null
}
}
private val viewModel by parentFragmentViewModel(UserVerificationViewModel::class)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding {
@ -58,17 +75,22 @@ class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChild
override fun onDestroyView() {
views.bottomSheetVerificationRecyclerView.cleanup()
controller.removeModelBuildListener(modelBuildListener)
controller.listener = null
super.onDestroyView()
}
private fun setupRecyclerView() {
views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.addModelBuildListener(modelBuildListener)
controller.listener = this
}
override fun invalidate() = withState(viewModel) { state ->
// Timber.w("VALR: invalidate with State: ${state.pendingRequest}")
if (state.isNewScreen()) {
requestAccessibilityFocus = true
}
controller.update(state)
}
@ -142,10 +164,40 @@ class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChild
}
override fun onUserDeniesQrCodeScanned() {
viewModel.handle(VerificationAction.OtherUserDidNotScanned)
viewModel.handle(VerificationAction.OtherUserDidNotScanned)
}
override fun onUserConfirmsQrCodeScanned() {
viewModel.handle(VerificationAction.OtherUserScannedSuccessfully)
}
private var currentScreenIndex = -1
private fun UserVerificationViewState.isNewScreen(): Boolean {
val newIndex = toScreenIndex()
if (currentScreenIndex == newIndex) {
return false
}
currentScreenIndex = newIndex
return true
}
private fun UserVerificationViewState.toScreenIndex(): Int {
return when (pendingRequest) {
is Fail -> 0
is Loading -> 1
is Success -> when (pendingRequest.invoke().state) {
EVerificationState.WaitingForReady -> 10
EVerificationState.Requested -> 11
EVerificationState.Ready -> 12
EVerificationState.Started -> 13
EVerificationState.WeStarted -> 14
EVerificationState.WaitingForDone -> 15
EVerificationState.Done -> 16
EVerificationState.Cancelled -> 17
EVerificationState.HandledByOtherSession -> 18
}
Uninitialized -> 2
}
}
}