auto set type image to images with enabled actions

d3b4bff6a684e0771aedfe5bd0ecfcc21f3bdeb7
This commit is contained in:
edubinskaya
2024-03-20 15:02:39 +03:00
parent 939ad5e2f8
commit 217430e894
16 changed files with 115 additions and 54 deletions
@@ -11,8 +11,11 @@ import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
internal class AccessibilityDelegateWrapper(
private val originalDelegate: AccessibilityDelegateCompat?,
private val initializeAccessibilityNodeInfo:
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> }
var initializeAccessibilityNodeInfo:
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> },
var actionsAccessibilityNodeInfo:
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> },
) : AccessibilityDelegateCompat() {
@@ -52,6 +55,8 @@ internal class AccessibilityDelegateWrapper(
?: super.onInitializeAccessibilityNodeInfo(host, info)
initializeAccessibilityNodeInfo(host, info)
actionsAccessibilityNodeInfo(host, info)
}
override fun onRequestSendAccessibilityEvent(
@@ -1,6 +1,7 @@
package com.yandex.div.core.view2
import android.view.View
import android.widget.ImageView
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import com.yandex.div.core.annotations.Mockable
@@ -10,9 +11,12 @@ import com.yandex.div.core.experiments.Experiment.ACCESSIBILITY_ENABLED
import com.yandex.div.core.util.AccessibilityStateProvider
import com.yandex.div.core.view2.backbutton.BackHandlingRecyclerView
import com.yandex.div.core.view2.divs.widgets.DivSliderView
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivAccessibility
import com.yandex.div2.DivBase
import com.yandex.div2.DivContainer
import com.yandex.div2.DivGallery
import com.yandex.div2.DivGifImage
import com.yandex.div2.DivImage
import com.yandex.div2.DivInput
import com.yandex.div2.DivSelect
@@ -56,17 +60,23 @@ internal class DivAccessibilityBinder @Inject constructor(
}
}
fun bindType(view: View, divBase: DivBase, type: DivAccessibility.Type) {
fun bindType(view: View, divBase: DivBase, type: DivAccessibility.Type, resolver: ExpressionResolver) {
if (!accessibilityStateProvider.isAccessibilityEnabled(view.context)) {
return
}
val originalDelegate = ViewCompat.getAccessibilityDelegate(view)
val accessibilityType = type.toAccessibilityType(divBase)
val accessibilityType = type.toAccessibilityType(divBase, resolver)
val accessibilityDelegate =
if (accessibilityType == AccessibilityType.LIST && view is BackHandlingRecyclerView) {
AccessibilityListDelegate(view)
} else if (originalDelegate is AccessibilityDelegateWrapper) {
originalDelegate.apply {
initializeAccessibilityNodeInfo = { _, info ->
info?.bindType(accessibilityType)
}
}
} else {
AccessibilityDelegateWrapper(
originalDelegate,
@@ -168,15 +178,23 @@ internal class DivAccessibilityBinder @Inject constructor(
isFocusable = actionable
}
private fun DivAccessibility.Type.toAccessibilityType(div: DivBase): AccessibilityType =
when (this) {
private fun DivImage.isClickable(resolver: ExpressionResolver): Boolean =
if (action != null && action?.isEnabled?.evaluate(resolver) == true) {
true
} else if (actions != null && actions?.any { it.isEnabled.evaluate(resolver) } == true) {
true
} else longtapActions != null && longtapActions?.any { it.isEnabled.evaluate(resolver) } == true
private fun DivAccessibility.Type.toAccessibilityType(
div: DivBase, resolver: ExpressionResolver
): AccessibilityType = when (this) {
DivAccessibility.Type.AUTO -> when {
div is DivInput -> AccessibilityType.EDIT_TEXT
div is DivText -> AccessibilityType.TEXT
div is DivTabs -> AccessibilityType.TAB_WIDGET
div is DivSelect -> AccessibilityType.SELECT
div is DivSlider -> AccessibilityType.SLIDER
div is DivImage && div.accessibility != null -> AccessibilityType.IMAGE
div is DivImage && (div.accessibility != null || div.isClickable(resolver))-> AccessibilityType.IMAGE
div is DivGallery && div.accessibility?.description != null -> AccessibilityType.PAGER
else -> AccessibilityType.NONE
}
@@ -50,6 +50,7 @@ import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div.json.expressions.equalsToConstant
import com.yandex.div.json.expressions.isConstantOrNull
import com.yandex.div2.Div
import com.yandex.div2.DivAccessibility
import com.yandex.div2.DivAction
import com.yandex.div2.DivAlignmentHorizontal
import com.yandex.div2.DivAlignmentVertical
@@ -511,7 +512,8 @@ internal fun View.applyDivActions(
actions: List<DivAction>?,
longTapActions: List<DivAction>?,
doubleTapActions: List<DivAction>?,
actionAnimation: DivAnimation
actionAnimation: DivAnimation,
accessibility: DivAccessibility?,
) {
val actionBinder = divView.div2Component.actionBinder
val tapActions = if (actions.isNullOrEmpty()) {
@@ -519,7 +521,8 @@ internal fun View.applyDivActions(
} else {
actions
}
actionBinder.bindDivActions(divView, this, tapActions, longTapActions, doubleTapActions, actionAnimation)
actionBinder.bindDivActions(divView, this, tapActions, longTapActions, doubleTapActions,
actionAnimation, accessibility)
}
internal fun View.setAnimatedTouchListener(
@@ -3,9 +3,9 @@ package com.yandex.div.core.view2.divs
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.StringDef
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import com.yandex.div.R
@@ -20,11 +20,8 @@ import com.yandex.div.core.experiments.Experiment.ACCESSIBILITY_ENABLED
import com.yandex.div.core.experiments.Experiment.IGNORE_ACTION_MENU_ITEMS_ENABLED
import com.yandex.div.core.experiments.Experiment.LONGTAP_ACTIONS_PASS_TO_CHILD_ENABLED
import com.yandex.div.core.view2.AccessibilityDelegateWrapper
import com.yandex.div.core.view2.AccessibilityListDelegate
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.DivAccessibilityBinder
import com.yandex.div.core.view2.DivGestureListener
import com.yandex.div.core.view2.backbutton.BackHandlingRecyclerView
import com.yandex.div.core.view2.divs.DivActionBinder.LogType.Companion.LOG_BLUR
import com.yandex.div.core.view2.divs.DivActionBinder.LogType.Companion.LOG_CLICK
import com.yandex.div.core.view2.divs.DivActionBinder.LogType.Companion.LOG_DOUBLE_CLICK
@@ -70,10 +67,9 @@ internal class DivActionBinder @Inject constructor(
actions: List<DivAction>?,
longTapActions: List<DivAction>?,
doubleTapActions: List<DivAction>?,
actionAnimation: DivAnimation
actionAnimation: DivAnimation,
accessibility: DivAccessibility?,
) {
val originalDelegate = ViewCompat.getAccessibilityDelegate(target)
val onApply = {
applyDivActions(
divView = divView,
@@ -82,7 +78,7 @@ internal class DivActionBinder @Inject constructor(
doubleTapActions = doubleTapActions.onlyEnabled(divView.expressionResolver),
longTapActions = longTapActions.onlyEnabled(divView.expressionResolver),
actionAnimation = actionAnimation,
originalDelegate = originalDelegate,
accessibility = accessibility,
)
}
val resolver = divView.expressionResolver
@@ -101,7 +97,7 @@ internal class DivActionBinder @Inject constructor(
longTapActions: List<DivAction>,
doubleTapActions: List<DivAction>,
actionAnimation: DivAnimation,
originalDelegate: AccessibilityDelegateCompat?,
accessibility: DivAccessibility?,
) {
val clickableState = target.isClickable
val longClickableState = target.isLongClickable
@@ -120,30 +116,55 @@ internal class DivActionBinder @Inject constructor(
divGestureListener
)
if (accessibilityEnabled &&
DivAccessibility.Mode.MERGE == divView.getPropagatedAccessibilityMode(target) &&
divView.isDescendantAccessibilityMode(target)
) {
target.isClickable = clickableState
target.isLongClickable = longClickableState
if (accessibilityEnabled) {
if (DivAccessibility.Mode.MERGE == divView.getPropagatedAccessibilityMode(target) &&
divView.isDescendantAccessibilityMode(target)) {
target.isClickable = clickableState
target.isLongClickable = longClickableState
}
val accessibilityWrapper = AccessibilityDelegateWrapper(
originalDelegate,
initializeAccessibilityNodeInfo = { _, info ->
if (actions.isNotEmpty()) {
info?.addAction(AccessibilityNodeInfoCompat
.AccessibilityActionCompat.ACTION_CLICK)
}
if (longTapActions.isNotEmpty()) {
info?.addAction(AccessibilityNodeInfoCompat
.AccessibilityActionCompat.ACTION_LONG_CLICK)
}
})
ViewCompat.setAccessibilityDelegate(target, accessibilityWrapper)
bindAccessibilityDelegate(target, actions, longTapActions, accessibility)
}
}
private fun bindAccessibilityDelegate(
target: View,
actions: List<DivAction>,
longTapActions: List<DivAction>,
accessibility: DivAccessibility?,
) {
val originalDelegate = ViewCompat.getAccessibilityDelegate(target)
val action = { _: View?, info: AccessibilityNodeInfoCompat? ->
if (actions.isNotEmpty()) {
info?.addAction(AccessibilityNodeInfoCompat
.AccessibilityActionCompat.ACTION_CLICK)
}
if (longTapActions.isNotEmpty()) {
info?.addAction(AccessibilityNodeInfoCompat
.AccessibilityActionCompat.ACTION_LONG_CLICK)
}
if (target is ImageView && (accessibility?.type == DivAccessibility.Type.AUTO || accessibility == null)) {
if (longTapActions.isNotEmpty() || actions.isNotEmpty() || accessibility?.description != null) {
info?.className = "android.widget.ImageView"
} else {
info?.className = ""
}
}
}
val accessibilityWrapper = if (originalDelegate is AccessibilityDelegateWrapper) {
originalDelegate.actionsAccessibilityNodeInfo = action
originalDelegate
} else {
AccessibilityDelegateWrapper(
originalDelegate,
actionsAccessibilityNodeInfo = action)
}
ViewCompat.setAccessibilityDelegate(target, accessibilityWrapper)
}
private fun bindTapActions(
divView: Div2View,
target: View,
@@ -273,11 +273,11 @@ internal class DivBaseBinder @Inject constructor(
if (newDiv.accessibility == null && oldDiv?.accessibility == null) {
// Shortcut for empty accessibility binding
applyAccessibilityMode(divView, newDiv, mode = null)
divAccessibilityBinder.bindType(this, newDiv, DivAccessibility.Type.AUTO)
divAccessibilityBinder.bindType(this, newDiv, DivAccessibility.Type.AUTO, resolver)
return
}
bindAccessibilityType(newDiv, oldDiv)
bindAccessibilityType(newDiv, oldDiv, resolver)
bindAccessibilityDescriptionAndHint(newDiv, oldDiv, resolver, subscriber)
bindAccessibilityMode(divView, newDiv, resolver, subscriber)
bindAccessibilityStateDescription(newDiv, oldDiv, resolver, subscriber)
@@ -286,14 +286,15 @@ internal class DivBaseBinder @Inject constructor(
private fun View.bindAccessibilityType(
newDiv: DivBase,
oldDiv: DivBase?
oldDiv: DivBase?,
resolver: ExpressionResolver,
) {
if (oldDiv != null && newDiv.accessibility?.type == oldDiv.accessibility?.type) {
return
}
divAccessibilityBinder.bindType(this, newDiv, newDiv.accessibility?.type
?: DivAccessibility.Type.AUTO)
?: DivAccessibility.Type.AUTO, resolver)
}
private fun View.bindAccessibilityDescriptionAndHint(
@@ -75,7 +75,8 @@ internal class DivContainerBinder @Inject constructor(
val oldDiv = divHolderView.div
baseBinder.bindView(view, div, oldDiv, divView)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
div.doubletapActions, div.actionAnimation, div.accessibility)
val resolver = divView.expressionResolver
val oldResolver = divView.oldExpressionResolver
@@ -52,7 +52,8 @@ internal class DivGifImageBinder @Inject constructor(
baseBinder.bindView(view, div, oldDiv, divView)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
div.doubletapActions, div.actionAnimation, div.accessibility)
view.bindAspectRatio(div.aspect, oldDiv?.aspect, expressionResolver)
@@ -47,7 +47,8 @@ internal class DivGridBinder @Inject constructor(
view.releaseViewVisitor = divView.releaseViewVisitor
baseBinder.bindView(view, div, oldDiv, divView)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
div.doubletapActions, div.actionAnimation, div.accessibility)
view.addSubscription(
div.columnCount.observeAndGet(resolver) { columnCount -> view.columnCount = columnCount.toIntSafely() }
@@ -42,7 +42,8 @@ internal class DivImageBinder @Inject constructor(
if (div === oldDiv) return
baseBinder.bindView(view, div, oldDiv, divView)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
div.doubletapActions, div.actionAnimation, div.accessibility)
val expressionResolver = divView.expressionResolver
val errorCollector = errorCollectors.getOrCreate(divView.dataTag, divView.divData)
@@ -10,6 +10,7 @@ import androidx.core.view.doOnLayout
import androidx.core.widget.doAfterTextChanged
import com.yandex.div.core.dagger.DivScope
import com.yandex.div.core.expression.variables.TwoWayStringVariableBinder
import com.yandex.div.core.util.AccessibilityStateProvider
import com.yandex.div.core.util.expressionSubscriber
import com.yandex.div.core.util.mask.BaseInputMask
import com.yandex.div.core.util.mask.CurrencyInputMask
@@ -47,6 +48,7 @@ internal class DivInputBinder @Inject constructor(
private val baseBinder: DivBaseBinder,
private val typefaceResolver: DivTypefaceResolver,
private val variableBinder: TwoWayStringVariableBinder,
private val accessibilityStateProvider: AccessibilityStateProvider,
private val errorCollectors: ErrorCollectors
) : DivViewBinder<DivInput, DivInputView> {
@@ -62,6 +64,7 @@ internal class DivInputBinder @Inject constructor(
isFocusable = true
isFocusableInTouchMode = true
textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
accessibilityEnabled = accessibilityStateProvider.isAccessibilityEnabled(view.context)
observeBackground(divView, div, oldDiv, expressionResolver)
@@ -22,7 +22,8 @@ internal class DivSeparatorBinder @Inject constructor(
if (div === oldDiv) return
baseBinder.bindView(view, div, oldDiv, divView)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
div.doubletapActions, div.actionAnimation, div.accessibility)
view.bindStyle(div.delimiterStyle, oldDiv?.delimiterStyle, divView.expressionResolver)
@@ -91,7 +91,8 @@ internal class DivTextBinder @Inject constructor(
if (div === oldDiv) return
baseBinder.bindView(view, div, oldDiv, divView)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
div.doubletapActions, div.actionAnimation, div.accessibility)
val expressionResolver = divView.expressionResolver
view.bindTypeface(div, oldDiv, expressionResolver)
@@ -26,11 +26,14 @@ internal class DivInputView @JvmOverloads constructor(
private var textChangeWatcher: TextWatcher? = null
private val accessibilityManager = context.getSystemService(
Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
private var _hint: String? = null
internal var accessibilityEnabled: Boolean = false
set(value) {
field = value
setInputHint(_hint)
}
init {
setPadding(0, 0, 0, 0)
}
@@ -38,7 +41,7 @@ internal class DivInputView @JvmOverloads constructor(
fun setInputHint(hint: String?) {
_hint = hint
this.hint = when {
accessibilityManager?.isTouchExplorationEnabled != true -> hint
!accessibilityEnabled -> hint
hint.isNullOrEmpty() && contentDescription.isNullOrEmpty() -> null
hint.isNullOrEmpty() -> contentDescription
contentDescription.isNullOrEmpty() -> hint
@@ -21,6 +21,7 @@ internal fun assertActionApplied(divView: Div2View, target: View, actionUri: Uri
longTapActions = anyOrNull(),
doubleTapActions = anyOrNull(),
actionAnimation = any(),
accessibility = anyOrNull(),
)
val action = actionCaptor.firstValue.find {
@@ -30,7 +30,7 @@ import org.robolectric.RuntimeEnvironment
open class DivBinderTest {
internal val actionBinder = mock<DivActionBinder> {
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any()) }.thenCallRealMethod()
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any(), anyOrNull()) }.thenCallRealMethod()
}
private val mockComponent = mock<Div2Component>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {
on { actionBinder } doReturn actionBinder
@@ -37,7 +37,7 @@ internal fun divView(
)
val actionBinder = mock<DivActionBinder> {
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any()) }.thenCallRealMethod()
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any(), anyOrNull()) }.thenCallRealMethod()
}
val component = mock<Div2Component>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {