Accessibility refactoring

commit_hash:22623192cdabbd2dcedf862b9a6d9aa8219e422b
This commit is contained in:
grechka62
2025-07-15 19:47:36 +03:00
parent 40b1cc2d94
commit e35dc0a5e9
101 changed files with 666 additions and 996 deletions
+3 -21
View File
@@ -1205,7 +1205,6 @@
"client/android/div/src/main/java/com/yandex/div/core/util/validator/ExpressionValidator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/validator/ExpressionValidator.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/validator/RegexValidator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/validator/RegexValidator.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/validator/ValidatorItemData.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/validator/ValidatorItemData.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/AccessibilityDelegateWrapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/AccessibilityDelegateWrapper.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/AccessibilityListDelegate.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/AccessibilityListDelegate.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/BindingContext.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/BindingContext.kt",
"client/android/div/src/main/java/com/yandex/div/core/view2/CompositeLogId.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/CompositeLogId.kt",
@@ -1480,8 +1479,10 @@
"client/android/div/src/main/java/com/yandex/div/internal/widget/menu/NonScrollImageView.java":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/menu/NonScrollImageView.java",
"client/android/div/src/main/java/com/yandex/div/internal/widget/menu/OverflowMenuSubscriber.java":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/menu/OverflowMenuSubscriber.java",
"client/android/div/src/main/java/com/yandex/div/internal/widget/menu/OverflowMenuWrapper.java":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/menu/OverflowMenuWrapper.java",
"client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderAccessibilityHelper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderAccessibilityHelper.kt",
"client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderDrawDelegate.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderDrawDelegate.kt",
"client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderTextStyle.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderTextStyle.kt",
"client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderThumbAnimatorListener.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderThumbAnimatorListener.kt",
"client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderView.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/slider/SliderView.kt",
"client/android/div/src/main/java/com/yandex/div/internal/widget/slider/shapes/TextDrawDelegate.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/slider/shapes/TextDrawDelegate.kt",
"client/android/div/src/main/java/com/yandex/div/internal/widget/slider/shapes/TextDrawable.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/internal/widget/slider/shapes/TextDrawable.kt",
@@ -1574,6 +1575,7 @@
"client/android/div/src/test/java/com/yandex/div/core/view2/GlobalVariableScopesTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/view2/GlobalVariableScopesTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/view2/SetVariableValueTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/view2/SetVariableValueTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/view2/TestHelpers.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/view2/TestHelpers.kt",
"client/android/div/src/test/java/com/yandex/div/core/view2/TypeAutoAccessibilityTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/view2/TypeAutoAccessibilityTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/view2/VariableUpdatesTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/view2/VariableUpdatesTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/view2/animations/DivComparatorTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/view2/animations/DivComparatorTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/view2/animations/DivStateComparatorTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/view2/animations/DivStateComparatorTest.kt",
@@ -1649,7 +1651,6 @@
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/SliderTests.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/SliderTests.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/SuperLineHeightTextViewTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/SuperLineHeightTextViewTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/TabsSwipeTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/TabsSwipeTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/TypeAutoAccessibilityTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/TypeAutoAccessibilityTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/TypedFocusActionsTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/TypedFocusActionsTest.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/rule/ActivityParamsTestRule.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/rule/ActivityParamsTestRule.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/rule/Rules.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/rule/Rules.kt",
@@ -1679,7 +1680,6 @@
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/SliderSteps.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/SliderSteps.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/SliderViews.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/SliderViews.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/SuperLineHeightTextViewSteps.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/SuperLineHeightTextViewSteps.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/TypeAutoAccessibilitySteps.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/TypeAutoAccessibilitySteps.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/VisibilityActionsSteps.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/VisibilityActionsSteps.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/CharSequences.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/CharSequences.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/OutsideActions.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/OutsideActions.kt",
@@ -2389,8 +2389,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API24_HDPI_540x1200/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -3532,8 +3530,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -4675,8 +4671,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API26_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -5818,8 +5812,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -6961,8 +6953,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API28_XHDPI_720x1600/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -8104,8 +8094,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -9247,8 +9235,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API30_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -10390,8 +10376,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewPixelCopy/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -11533,8 +11517,6 @@
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step6.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step7.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text-properties/step8.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_focused_text_color/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step0.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-text/text_range_without_bound/step1.png",
"client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png":"divkit/public/client/android/divkit-demo-app/src/screenshotTest/screenshots/API32_XXHDPI_1080x2400/viewRender/com.yandex.div.Div2InteractiveScreenshotTest/div-timer/timer-end-actions-tick-actions/step0.png",
@@ -2,7 +2,6 @@ package com.yandex.div.core.actions
import android.view.View
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.divs.gainAccessibilityFocus
import com.yandex.div.core.view2.divs.widgets.DivInputView
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivActionFocusElement
@@ -36,7 +35,6 @@ internal class DivActionTypedFocusElementHandler @Inject constructor() : DivActi
?: return
requestedView.requestFocus()
requestedView.gainAccessibilityFocus()
when (requestedView) {
is DivInputView -> requestedView.openKeyboard()
}
@@ -9,6 +9,7 @@ import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
import android.widget.PopupWindow
import androidx.activity.OnBackPressedCallback
@@ -30,7 +31,6 @@ import com.yandex.div.core.util.isActuallyLaidOut
import com.yandex.div.core.view2.BindingContext
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.DivVisibilityActionTracker
import com.yandex.div.core.view2.divs.sendAccessibilityEventUnchecked
import com.yandex.div.core.view2.divs.toLayoutParamsSize
import com.yandex.div.core.view2.divs.toPx
import com.yandex.div.core.view2.errors.ErrorCollectors
@@ -432,3 +432,19 @@ private fun Div2View.getWindowFrame(): Rect {
getWindowVisibleDisplayFrame(windowFrame)
return windowFrame
}
private fun sendAccessibilityEventUnchecked(
event: Int,
view: View?,
accessibilityStateProvider: AccessibilityStateProvider
) {
view ?: return
if (!accessibilityStateProvider.isAccessibilityEnabled(view.context)) return
val accessibilityEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AccessibilityEvent(event)
} else {
@Suppress("DEPRECATION")
AccessibilityEvent.obtain(event)
}
view.sendAccessibilityEventUnchecked(accessibilityEvent)
}
@@ -14,22 +14,22 @@ internal class AccessibilityStateProvider @Inject constructor(
fun isAccessibilityEnabled(context: Context): Boolean {
return when {
!a11yConfigurationEnabled -> false
touchModeEnabled != null -> touchModeEnabled!!
touchExplorationEnabled != null -> touchExplorationEnabled!!
else -> {
evaluateTouchModeEnabled(context)
touchModeEnabled!!
touchExplorationEnabled!!
}
}
}
companion object {
var touchModeEnabled: Boolean? = null
var touchExplorationEnabled: Boolean? = null
fun evaluateTouchModeEnabled(context: Context) {
if (touchModeEnabled != null) return
if (touchExplorationEnabled != null) return
val accessibilityManager = context.getSystemService(
Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
touchModeEnabled = accessibilityManager?.isTouchExplorationEnabled ?: false
touchExplorationEnabled = accessibilityManager?.isTouchExplorationEnabled ?: false
}
}
}
@@ -1,6 +1,5 @@
package com.yandex.div.core.util
import android.os.Build
import android.view.View
import android.view.View.OnAttachStateChangeListener
import androidx.core.view.ViewCompat
@@ -39,14 +38,6 @@ inline fun View.doOnActualLayout(crossinline action: (view: View) -> Unit) {
}
}
internal fun View.makeFocusable() {
isFocusable = true
isFocusableInTouchMode = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
defaultFocusHighlightEnabled = false
}
}
internal inline fun View.doOnHierarchyLayout(
crossinline action: (view: View) -> Unit,
onEnqueuedAction: () -> Unit
@@ -1,77 +0,0 @@
package com.yandex.div.core.view2
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
internal class AccessibilityDelegateWrapper(
private val originalDelegate: AccessibilityDelegateCompat?,
var initializeAccessibilityNodeInfo:
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> },
var actionsAccessibilityNodeInfo:
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> },
) : AccessibilityDelegateCompat() {
override fun sendAccessibilityEvent(host: View, eventType: Int) {
originalDelegate?.sendAccessibilityEvent(host, eventType)
?: super.sendAccessibilityEvent(host, eventType)
}
override fun sendAccessibilityEventUnchecked(host: View, event: AccessibilityEvent) {
originalDelegate?.sendAccessibilityEventUnchecked(host, event)
?: super.sendAccessibilityEventUnchecked(host, event)
}
override fun dispatchPopulateAccessibilityEvent(
host: View,
event: AccessibilityEvent
): Boolean {
return originalDelegate?.dispatchPopulateAccessibilityEvent(host, event)
?: super.dispatchPopulateAccessibilityEvent(host, event)
}
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
originalDelegate?.onPopulateAccessibilityEvent(host, event)
?: super.onPopulateAccessibilityEvent(host, event)
}
override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {
originalDelegate?.onInitializeAccessibilityEvent(host, event)
?: super.onInitializeAccessibilityEvent(host, event)
}
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
originalDelegate?.onInitializeAccessibilityNodeInfo(host, info)
?: super.onInitializeAccessibilityNodeInfo(host, info)
initializeAccessibilityNodeInfo(host, info)
actionsAccessibilityNodeInfo(host, info)
}
override fun onRequestSendAccessibilityEvent(
host: ViewGroup,
child: View,
event: AccessibilityEvent
): Boolean {
return originalDelegate?.onRequestSendAccessibilityEvent(host, child, event)
?: super.onRequestSendAccessibilityEvent(host, child, event)
}
override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProviderCompat? {
return originalDelegate?.getAccessibilityNodeProvider(host)
?: super.getAccessibilityNodeProvider(host)
}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
return originalDelegate?.performAccessibilityAction(host, action, args)
?: super.performAccessibilityAction(host, action, args)
}
}
@@ -4,8 +4,6 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Button
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@@ -89,7 +89,6 @@ import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div.util.INVALID_STATE_ID
import com.yandex.div.util.getInitialStateId
import com.yandex.div2.Div
import com.yandex.div2.DivAccessibility
import com.yandex.div2.DivAction
import com.yandex.div2.DivData
import com.yandex.div2.DivPatch
@@ -131,7 +130,6 @@ class Div2View private constructor(
private val divDataChangedObservers = mutableListOf<DivDataChangedObserver>()
private val persistentDivDataObservers = ObserverList<PersistentDivDataObserver>()
private val viewToDivBindings = WeakHashMap<View, Div>()
private val propagatedAccessibilityModes = WeakHashMap<View, DivAccessibility.Mode>()
private val bulkActionsHandler = BulkActionHandler()
private val divVideoActionHandler: DivVideoActionHandler
get() = div2Component.divVideoActionHandler
@@ -709,7 +707,6 @@ class Div2View private constructor(
private fun stopLoadAndSubscriptions() {
viewToDivBindings.clear()
propagatedAccessibilityModes.clear()
cancelTooltips() // Depends on children, should be called before removing them
clearSubscriptions()
divDataChangedObservers.clear()
@@ -1160,20 +1157,6 @@ class Div2View private constructor(
internal fun takeBindingDiv(view: View) = viewToDivBindings[view]
internal fun setPropagatedAccessibilityMode(view: View, mode: DivAccessibility.Mode) {
propagatedAccessibilityModes[view] = mode
}
internal fun getPropagatedAccessibilityMode(view: View): DivAccessibility.Mode? {
return propagatedAccessibilityModes[view]
}
internal fun isDescendantAccessibilityMode(view: View): Boolean {
return (view.parent as? View)?.let { parent ->
propagatedAccessibilityModes[parent] == propagatedAccessibilityModes[view]
} ?: false
}
/**
* @return exception if setting variable failed, null otherwise.
*/
@@ -1,26 +1,35 @@
package com.yandex.div.core.view2
import android.os.Build
import android.view.View
import android.widget.CheckBox
import android.widget.RadioButton
import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.children
import androidx.core.view.isVisible
import com.yandex.div.core.Disposable
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.dagger.DivScope
import com.yandex.div.core.dagger.ExperimentFlag
import com.yandex.div.core.experiments.Experiment.ACCESSIBILITY_ENABLED
import com.yandex.div.core.util.AccessibilityStateProvider
import com.yandex.div.core.util.expressionSubscriber
import com.yandex.div.core.view2.backbutton.BackHandlingRecyclerView
import com.yandex.div.core.view2.divs.widgets.DivInputView
import com.yandex.div.core.view2.divs.widgets.DivSliderView
import com.yandex.div.core.view2.divs.widgets.DivCollectionHolder
import com.yandex.div.internal.core.ExpressionSubscriber
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.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
import com.yandex.div2.DivSeparator
import com.yandex.div2.DivSlider
import com.yandex.div2.DivTabs
import com.yandex.div2.DivText
@@ -35,173 +44,78 @@ internal class DivAccessibilityBinder @Inject constructor(
@ExperimentFlag(ACCESSIBILITY_ENABLED) val enabled: Boolean,
private val accessibilityStateProvider: AccessibilityStateProvider,
) {
fun bindAccessibilityMode(
fun bind(
view: View,
divView: Div2View,
mode: DivAccessibility.Mode?,
divBase: DivBase,
newDiv: DivBase,
oldDiv: DivBase?,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber,
) {
if (!enabled) {
return
}
val parentMode = (view.parent as? View)?.let {
divView.getPropagatedAccessibilityMode(it)
}
if (parentMode != null) {
val propagatedMode = getPropagatedMode(parentMode, mode ?: divBase.getDefaultAccessibilityMode)
view.applyAccessibilityMode(
propagatedMode,
divView,
isDescendant = parentMode == propagatedMode
)
} else {
view.applyAccessibilityMode(mode ?: divBase.getDefaultAccessibilityMode, divView, isDescendant = false)
}
}
fun bindType(view: View, divBase: DivBase, type: DivAccessibility.Type, resolver: ExpressionResolver) {
if (!accessibilityStateProvider.isAccessibilityEnabled(view.context)) {
if (newDiv.accessibility == null && oldDiv?.accessibility == null) {
// Shortcut for empty accessibility binding
if (enabled) {
view.applyMode()
}
return
}
val originalDelegate = ViewCompat.getAccessibilityDelegate(view)
val accessibilityType = type.toAccessibilityType(divBase, resolver)
view.bindType(newDiv, oldDiv)
view.bindDescriptionAndHint(newDiv, oldDiv, resolver, subscriber)
view.bindMode(newDiv, oldDiv, resolver, subscriber)
view.bindStateDescription(newDiv, oldDiv, resolver, subscriber)
//TODO: bind 'muteAfterAction' property
}
val accessibilityDelegate =
if (accessibilityType == AccessibilityType.LIST && view is BackHandlingRecyclerView) {
AccessibilityListDelegate(view)
} else if (originalDelegate is AccessibilityDelegateWrapper) {
originalDelegate.apply {
initializeAccessibilityNodeInfo = { _, info ->
info?.bindType(accessibilityType)
}
// region Type
private fun View.bindType(newDiv: DivBase, oldDiv: DivBase?) {
if (!accessibilityStateProvider.isAccessibilityEnabled(context)) return
if (oldDiv != null && newDiv.accessibility?.type == oldDiv.accessibility?.type) return
applyType(newDiv, newDiv.accessibility?.type)
}
private fun View.applyType(divBase: DivBase, accessibilityType: DivAccessibility.Type? = null) {
val type = accessibilityType ?: DivAccessibility.Type.AUTO
getAccessibilityDelegate(this, type.toAccessibilityType(divBase))?.let {
ViewCompat.setAccessibilityDelegate(this, it)
}
}
private fun getAccessibilityDelegate(view: View, type: AccessibilityType): AccessibilityDelegateCompat? {
if (type == AccessibilityType.LIST && view is BackHandlingRecyclerView) {
return AccessibilityListDelegate(view)
}
val className = type.toClassName
val heading = type == AccessibilityType.HEADER
val autoClassName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) view.accessibilityClassName else null
if ((className.isEmpty() || className == autoClassName) && !heading) return null
return object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
if (className.isNotEmpty()) {
info.className = className
}
} else {
AccessibilityDelegateWrapper(
originalDelegate,
initializeAccessibilityNodeInfo = { _, info ->
info?.bindType(accessibilityType)
})
info.isHeading = heading
}
ViewCompat.setAccessibilityDelegate(view, accessibilityDelegate)
}
private val DivBase.getDefaultAccessibilityMode: DivAccessibility.Mode
get() = when (this) {
is DivImage -> if (accessibility == null && doubletapActions.isNullOrEmpty() &&
actions.isNullOrEmpty() && longtapActions.isNullOrEmpty()) {
DivAccessibility.Mode.EXCLUDE
} else {
DivAccessibility.Mode.DEFAULT
}
is DivSeparator -> if (accessibility == null && doubletapActions.isNullOrEmpty() &&
actions.isNullOrEmpty() && longtapActions.isNullOrEmpty()) {
DivAccessibility.Mode.EXCLUDE
} else {
DivAccessibility.Mode.DEFAULT
}
else -> DivAccessibility.Mode.DEFAULT
}
/**
* Sets [AccessibilityNodeInfoCompat]'s className so that TalkBack could
* properly recognize role of View provided by [DivAccessibility.Type].
* For example, if [type] is [DivAccessibility.Type.BUTTON], TalkBack announces View as "Button".
*/
private fun AccessibilityNodeInfoCompat.bindType(type: AccessibilityType) {
this.className = when (type) {
AccessibilityType.NONE -> ""
AccessibilityType.BUTTON -> "android.widget.Button"
AccessibilityType.EDIT_TEXT -> "android.widget.EditText"
AccessibilityType.HEADER -> "android.widget.TextView"
AccessibilityType.IMAGE -> "android.widget.ImageView"
AccessibilityType.LIST -> ""
AccessibilityType.PAGER -> "androidx.viewpager.widget.ViewPager"
AccessibilityType.SLIDER -> "android.widget.SeekBar"
AccessibilityType.SELECT -> "android.widget.Spinner"
AccessibilityType.TAB_WIDGET -> "android.widget.TabWidget"
AccessibilityType.TEXT -> "android.widget.TextView"
AccessibilityType.CHECK_BOX -> "android.widget.CheckBox"
AccessibilityType.RADIO_BUTTON -> "android.widget.RadioButton"
}
if (AccessibilityType.HEADER == type) {
this.isHeading = true
}
}
private val DivAccessibility.Mode.priority
get() = when (this) {
DivAccessibility.Mode.EXCLUDE -> 0
DivAccessibility.Mode.MERGE -> 1
DivAccessibility.Mode.DEFAULT -> 2
}
private fun getPropagatedMode(
parentMode: DivAccessibility.Mode,
mode: DivAccessibility.Mode
): DivAccessibility.Mode {
return if (parentMode.priority < mode.priority) parentMode else mode
}
private fun View.applyAccessibilityMode(
mode: DivAccessibility.Mode,
divView: Div2View,
isDescendant: Boolean
) {
when (mode) {
DivAccessibility.Mode.MERGE -> {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
if (!isDescendant) {
isFocusable = this !is DivSliderView
} else {
setActionable(false)
}
}
DivAccessibility.Mode.EXCLUDE -> {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
isFocusable = false
if (this is DivInputView) isFocusableInTouchMode = true
}
DivAccessibility.Mode.DEFAULT -> {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
isFocusable = this !is DivSliderView
}
}
divView.setPropagatedAccessibilityMode(this, mode)
}
private fun View.setActionable(actionable: Boolean) {
isClickable = actionable
isLongClickable = actionable
isFocusable = actionable
}
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) {
private fun DivAccessibility.Type.toAccessibilityType(div: DivBase): AccessibilityType {
return when (this) {
DivAccessibility.Type.AUTO -> when {
div.accessibility?.mode?.evaluate(resolver) == DivAccessibility.Mode.EXCLUDE -> AccessibilityType.NONE
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 || div.isClickable(resolver))-> AccessibilityType.IMAGE
div is DivImage -> AccessibilityType.IMAGE
div is DivGifImage -> AccessibilityType.IMAGE
div is DivGallery && div.accessibility?.description != null -> AccessibilityType.PAGER
div is RadioButton -> AccessibilityType.RADIO_BUTTON
div is CheckBox -> AccessibilityType.CHECK_BOX
div is DivContainer -> AccessibilityType.CONTAINER
else -> AccessibilityType.NONE
}
DivAccessibility.Type.NONE -> AccessibilityType.NONE
@@ -216,6 +130,161 @@ internal class DivAccessibilityBinder @Inject constructor(
DivAccessibility.Type.RADIO -> AccessibilityType.RADIO_BUTTON
DivAccessibility.Type.CHECKBOX -> AccessibilityType.CHECK_BOX
}
}
private val AccessibilityType.toClassName: String get() {
return when (this) {
AccessibilityType.NONE -> ""
AccessibilityType.BUTTON -> "android.widget.Button"
AccessibilityType.EDIT_TEXT -> "android.widget.EditText"
AccessibilityType.HEADER -> ""
AccessibilityType.IMAGE -> "android.widget.ImageView"
AccessibilityType.LIST -> ""
AccessibilityType.PAGER -> "androidx.viewpager.widget.ViewPager"
AccessibilityType.SLIDER -> ""
AccessibilityType.SELECT -> "android.widget.Spinner"
AccessibilityType.TAB_WIDGET -> "android.widget.TabWidget"
AccessibilityType.TEXT -> "android.widget.TextView"
AccessibilityType.CHECK_BOX -> "android.widget.CheckBox"
AccessibilityType.RADIO_BUTTON -> "android.widget.RadioButton"
AccessibilityType.CONTAINER -> "android.view.ViewGroup"
}
}
// endregion
// region Description and Hint
private fun View.bindDescriptionAndHint(
newDiv: DivBase,
oldDiv: DivBase?,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber
) {
val newDescription = newDiv.accessibility?.description
val newHint = newDiv.accessibility?.hint
if (newDescription.equalsToConstant(oldDiv?.accessibility?.description) &&
newHint.equalsToConstant(oldDiv?.accessibility?.hint)) {
return
}
applyDescriptionAndHint(newDescription?.evaluate(resolver), newHint?.evaluate(resolver))
if (newDescription.isConstantOrNull() && newHint.isConstantOrNull()) return
val callback = { _: Any ->
applyDescriptionAndHint(newDescription?.evaluate(resolver), newHint?.evaluate(resolver))
}
subscriber.addSubscription(newDescription?.observe(resolver, callback))
subscriber.addSubscription(newHint?.observe(resolver, callback))
}
private fun View.applyDescriptionAndHint(description: String?, hint: String?) {
contentDescription = when {
description == null -> hint
hint == null -> description
else -> "$description\n$hint"
}
}
// endregion
// region Mode
private fun View.bindMode(
newDiv: DivBase,
oldDiv: DivBase?,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber
) {
if (!enabled) return
val newMode = newDiv.accessibility?.mode
if (newMode.equalsToConstant(oldDiv?.accessibility?.mode)) return
applyMode(newMode?.evaluate(resolver))
if (newMode.isConstantOrNull()) return
subscriber.addSubscription(newMode?.observe(resolver) { applyMode(it) })
}
private fun View.applyMode(mode: DivAccessibility.Mode? = null) {
if (this !is ViewGroup) {
importantForAccessibility = when {
mode == DivAccessibility.Mode.EXCLUDE -> View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
!contentDescription.isNullOrBlank() -> View.IMPORTANT_FOR_ACCESSIBILITY_YES
else -> View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
return
}
if (this !is DivCollectionHolder) return
if (mode == DivAccessibility.Mode.MERGE) {
updateContainerMode()
if (accessibilityObserver != null) return
val observer = createContentObserver()
expressionSubscriber.addSubscription(observer)
accessibilityObserver = observer
return
}
accessibilityObserver?.close()
accessibilityObserver = null
ViewCompat.setScreenReaderFocusable(this, false)
importantForAccessibility = if (mode == DivAccessibility.Mode.EXCLUDE) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
}
private fun ViewGroup.createContentObserver() = object : OnLayoutChangeListener, Disposable {
init {
addOnLayoutChangeListener(this)
}
override fun onLayoutChange(
v: View?, left: Int, top: Int, right: Int, bottom: Int,
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
) = updateContainerMode()
override fun close() = removeOnLayoutChangeListener(this)
}
private fun ViewGroup.updateContainerMode() {
val hasContent = children.any { it.isVisible }
ViewCompat.setScreenReaderFocusable(this, hasContent)
importantForAccessibility =
if (hasContent) View.IMPORTANT_FOR_ACCESSIBILITY_YES else View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
// endregion
// region State description
private fun View.bindStateDescription(
newDiv: DivBase,
oldDiv: DivBase?,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber
) {
val newStateDescription = newDiv.accessibility?.stateDescription
if (newStateDescription.equalsToConstant(oldDiv?.accessibility?.stateDescription)) return
applyStateDescription(newStateDescription?.evaluate(resolver))
if (newStateDescription.isConstantOrNull()) return
subscriber.addSubscription(newStateDescription?.observe(resolver) { applyStateDescription(it) })
}
private fun View.applyStateDescription(stateDescription: String?) =
ViewCompat.setStateDescription(this, stateDescription)
private enum class AccessibilityType {
NONE,
@@ -231,5 +300,6 @@ internal class DivAccessibilityBinder @Inject constructor(
TEXT,
RADIO_BUTTON,
CHECK_BOX,
CONTAINER,
}
}
@@ -3,6 +3,7 @@ package com.yandex.div.core.view2.backbutton
import android.view.KeyEvent
import android.view.View
import com.yandex.div.core.view2.backbutton.BackKeyPressedHelper.OnBackClickListener
import com.yandex.div.core.view2.divs.gainAccessibilityFocus
/**
* This class helps to handle BACK key inside some View.
@@ -30,7 +31,7 @@ internal class BackKeyPressedHelper(private val mOwnerView: View) {
*/
fun setOnBackClickListener(onBackClickListener: OnBackClickListener?) {
mOnBackClickListener = onBackClickListener
setupFocus()
setupAccessibilityFocus()
}
/**
@@ -61,7 +62,7 @@ internal class BackKeyPressedHelper(private val mOwnerView: View) {
*/
fun onWindowFocusChanged(hasWindowFocus: Boolean) {
if (hasWindowFocus) {
setupFocus()
setupAccessibilityFocus()
}
}
@@ -69,18 +70,16 @@ internal class BackKeyPressedHelper(private val mOwnerView: View) {
* Call this from [View.onVisibilityChanged].
*/
fun onVisibilityChanged() {
setupFocus()
setupAccessibilityFocus()
}
private fun setupFocus() {
private fun setupAccessibilityFocus() {
if (mOnBackClickListener == null || !mOwnerView.hasWindowFocus()) return
mOwnerView.apply {
isFocusable = true
isFocusableInTouchMode = true
when {
isShown -> requestFocus()
hasFocus() -> rootView?.requestFocus(View.FOCUS_UP)
isShown -> gainAccessibilityFocus()
isAccessibilityFocused -> rootView?.gainAccessibilityFocus()
}
}
}
@@ -24,7 +24,6 @@ import androidx.core.view.children
import androidx.core.view.doOnNextLayout
import androidx.core.view.doOnPreDraw
import com.yandex.div.core.expression.suppressExpressionErrors
import com.yandex.div.core.util.AccessibilityStateProvider
import com.yandex.div.core.util.doOnActualLayout
import com.yandex.div.core.util.isLayoutRtl
import com.yandex.div.core.util.toIntSafely
@@ -56,7 +55,6 @@ import com.yandex.div.json.expressions.equalsToConstant
import com.yandex.div.json.expressions.isConstant
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
@@ -536,7 +534,6 @@ internal fun View.applyDivActions(
pressStartActions: List<DivAction>?,
pressEndActions: List<DivAction>?,
actionAnimation: DivAnimation,
accessibility: DivAccessibility?,
captureFocusOnAction: Expression<Boolean>,
) {
val actionBinder = context.divView.div2Component.actionBinder
@@ -556,7 +553,6 @@ internal fun View.applyDivActions(
pressStartActions,
pressEndActions,
actionAnimation,
accessibility,
captureFocusOnAction,
)
}
@@ -908,24 +904,6 @@ internal fun View.gainAccessibilityFocus() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
}
internal fun sendAccessibilityEventUnchecked(
event: Int,
view: View?,
accessibilityStateProvider: AccessibilityStateProvider
) {
view ?: return
if (accessibilityStateProvider.isAccessibilityEnabled(view.context)) {
view.sendAccessibilityEventUnchecked(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AccessibilityEvent(event)
} else {
@Suppress("DEPRECATION")
AccessibilityEvent.obtain(event)
}
)
}
}
internal fun ViewGroup.bindClipChildren(
newClipToBounds: Expression<Boolean>,
oldClipToBounds: Expression<Boolean>?,
@@ -5,12 +5,10 @@ import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.StringDef
import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.size
import com.yandex.div.R
import com.yandex.div.core.Div2Logger
import com.yandex.div.core.DivActionHandler
@@ -19,10 +17,8 @@ import com.yandex.div.core.DivViewFacade
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.dagger.DivScope
import com.yandex.div.core.dagger.ExperimentFlag
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.BindingContext
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.DivGestureListener
@@ -44,7 +40,6 @@ import com.yandex.div.internal.util.allIsNullOrEmpty
import com.yandex.div.internal.widget.menu.OverflowMenuWrapper
import com.yandex.div.json.expressions.Expression
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivAccessibility
import com.yandex.div2.DivAction
import com.yandex.div2.DivAnimation
import java.util.UUID
@@ -58,7 +53,6 @@ internal class DivActionBinder @Inject constructor(
private val divActionBeaconSender: DivActionBeaconSender,
@ExperimentFlag(LONGTAP_ACTIONS_PASS_TO_CHILD_ENABLED) private val longtapActionsPassToChild: Boolean,
@ExperimentFlag(IGNORE_ACTION_MENU_ITEMS_ENABLED) private val shouldIgnoreActionMenuItems: Boolean,
@ExperimentFlag(ACCESSIBILITY_ENABLED) private val accessibilityEnabled: Boolean
) {
private val passToParentLongClickListener: (View) -> Boolean = { view ->
var isLongClickHandled = false
@@ -83,7 +77,6 @@ internal class DivActionBinder @Inject constructor(
pressStartActions: List<DivAction>?,
pressEndActions: List<DivAction>?,
actionAnimation: DivAnimation,
accessibility: DivAccessibility?,
captureFocusOnAction: Expression<Boolean>,
) {
val resolver = context.expressionResolver
@@ -99,7 +92,6 @@ internal class DivActionBinder @Inject constructor(
pressStartActions = pressStartActions.onlyEnabled(resolver),
pressEndActions = pressEndActions.onlyEnabled(resolver),
actionAnimation = actionAnimation,
accessibility = accessibility,
captureFocusOnAction = captureFocusOnAction,
)
}
@@ -123,12 +115,8 @@ internal class DivActionBinder @Inject constructor(
pressStartActions: List<DivAction>,
pressEndActions: List<DivAction>,
actionAnimation: DivAnimation,
accessibility: DivAccessibility?,
captureFocusOnAction: Expression<Boolean>,
) {
val clickableState = target.isClickable
val longClickableState = target.isLongClickable
val divGestureListener = DivGestureListener(
awaitLongClick = longTapActions.isNotEmpty() || target.parentIsLongClickable()
)
@@ -155,54 +143,6 @@ internal class DivActionBinder @Inject constructor(
bindHoverActions(context, target, hoverStartActions, hoverEndActions)
target.attachTouchListeners(animatedTouchListener, pressTouchListener)
if (accessibilityEnabled) {
if (DivAccessibility.Mode.MERGE == context.divView.getPropagatedAccessibilityMode(target) &&
context.divView.isDescendantAccessibilityMode(target)) {
target.isClickable = clickableState
target.isLongClickable = longClickableState
}
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(
@@ -602,7 +542,7 @@ internal class DivActionBinder @Inject constructor(
val expressionResolver = context.expressionResolver
val menu = popupMenu.menu
for (itemData in items) {
val itemPosition = menu.size()
val itemPosition = menu.size
val menuItem = menu.add(itemData.text.evaluate(expressionResolver))
menuItem.setOnMenuItemClickListener {
var actionsHandled = false
@@ -7,7 +7,6 @@ import android.view.View
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.MarginLayoutParams
import android.view.ViewTreeObserver
import androidx.core.view.ViewCompat
import androidx.transition.Transition
import androidx.transition.TransitionManager
import androidx.transition.Visibility
@@ -37,10 +36,11 @@ import com.yandex.div.json.expressions.equalsToConstant
import com.yandex.div.json.expressions.isConstant
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.DivBase
import com.yandex.div2.DivInput
import com.yandex.div2.DivSize
import com.yandex.div2.DivSwitch
import com.yandex.div2.DivVisibility
import javax.inject.Inject
@@ -73,7 +73,7 @@ internal class DivBaseBinder @Inject constructor(
bindId(divView, div, oldDiv)
bindLayoutParams(div, oldDiv, resolver, subscriber)
bindLayoutProvider(bindingContext, div, oldDiv)
bindAccessibility(divView, div, oldDiv, resolver, subscriber)
bindAccessibility(div, oldDiv, resolver, subscriber)
bindAlpha(div, oldDiv, resolver, subscriber)
bindBackground(bindingContext, div, oldDiv, subscriber)
@@ -87,10 +87,7 @@ internal class DivBaseBinder @Inject constructor(
div.tooltips?.let { tooltipController.mapTooltip(this, it) }
// DivAccessibilityBinder is responsible for focus setup, so changing isFocusable only if binder is disabled
if (!divAccessibilityBinder.enabled) {
applyFocusableState(div)
}
applyFocusableState(div)
}
//region Id
@@ -379,135 +376,11 @@ internal class DivBaseBinder @Inject constructor(
//region Accessibility
private fun View.bindAccessibility(
divView: Div2View,
newDiv: DivBase,
oldDiv: DivBase?,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber
) {
if (newDiv.accessibility == null && oldDiv?.accessibility == null) {
// Shortcut for empty accessibility binding
applyAccessibilityMode(divView, newDiv, mode = null)
divAccessibilityBinder.bindType(this, newDiv, DivAccessibility.Type.AUTO, resolver)
return
}
bindAccessibilityType(newDiv, oldDiv, resolver)
bindAccessibilityDescriptionAndHint(newDiv, oldDiv, resolver, subscriber)
bindAccessibilityMode(divView, newDiv, resolver, subscriber)
bindAccessibilityStateDescription(newDiv, oldDiv, resolver, subscriber)
//TODO: bind 'muteAfterAction' property
}
private fun View.bindAccessibilityType(
newDiv: 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, resolver)
}
private fun View.bindAccessibilityDescriptionAndHint(
newDiv: DivBase,
oldDiv: DivBase?,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber
) {
if (newDiv.accessibility?.description.equalsToConstant(oldDiv?.accessibility?.description)
&& newDiv.accessibility?.hint.equalsToConstant(oldDiv?.accessibility?.hint)) {
return
}
applyAccessibilityDescriptionAndHint(
newDiv.accessibility?.description?.evaluate(resolver),
newDiv.accessibility?.hint?.evaluate(resolver)
)
if (newDiv.accessibility?.description.isConstantOrNull()
&& newDiv.accessibility?.hint.isConstantOrNull()) {
return
}
val callback = { _: Any ->
applyAccessibilityDescriptionAndHint(
newDiv.accessibility?.description?.evaluate(resolver),
newDiv.accessibility?.hint?.evaluate(resolver)
)
}
subscriber.addSubscription(newDiv.accessibility?.description?.observe(resolver, callback))
subscriber.addSubscription(newDiv.accessibility?.hint?.observe(resolver, callback))
}
private fun View.applyAccessibilityDescriptionAndHint(contentDescription: String?, hint: String?) {
this.contentDescription = when {
contentDescription == null -> hint
hint == null -> contentDescription
else -> "$contentDescription\n$hint"
}
}
private fun View.bindAccessibilityMode(
divView: Div2View,
newDiv: DivBase,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber
) {
// We can't compare accessibility mode with previous one due to actual value depends on parent mode
// and should be recalculated.
applyAccessibilityMode(divView, newDiv, newDiv.accessibility?.mode?.evaluate(resolver))
if (newDiv.accessibility?.mode.isConstantOrNull()) {
return
}
subscriber.addSubscription(
newDiv.accessibility?.mode?.observe(resolver) { mode ->
applyAccessibilityMode(divView, newDiv, mode)
val type = newDiv.accessibility?.type ?: DivAccessibility.Type.AUTO
if (type == DivAccessibility.Type.AUTO) {
divAccessibilityBinder.bindType(this, newDiv, type, resolver)
}
}
)
}
private fun View.applyAccessibilityMode(divView: Div2View, base: DivBase, mode: DivAccessibility.Mode?) {
divAccessibilityBinder.bindAccessibilityMode(this, divView, mode, base)
}
private fun View.bindAccessibilityStateDescription(
newDiv: DivBase,
oldDiv: DivBase?,
resolver: ExpressionResolver,
subscriber: ExpressionSubscriber
) {
if (newDiv.accessibility?.stateDescription.equalsToConstant(oldDiv?.accessibility?.stateDescription)) {
return
}
applyAccessibilityStateDescription(newDiv.accessibility?.stateDescription?.evaluate(resolver))
if (newDiv.accessibility?.stateDescription.isConstantOrNull()) {
return
}
subscriber.addSubscription(
newDiv.accessibility?.stateDescription?.observe(resolver) { stateDescription ->
applyAccessibilityStateDescription(stateDescription)
}
)
}
private fun View.applyAccessibilityStateDescription(stateDescription: String?) {
ViewCompat.setStateDescription(this, stateDescription)
}
) = divAccessibilityBinder.bind(this, newDiv, oldDiv, resolver, subscriber)
//endregion
@@ -765,6 +638,7 @@ internal class DivBaseBinder @Inject constructor(
//endregion
private fun View.applyFocusableState(div: DivBase) {
if (div is DivInput || div is DivSwitch) return
isFocusable = div.focus != null
}
@@ -110,7 +110,6 @@ internal class DivContainerBinder @Inject constructor(
div.pressStartActions,
div.pressEndActions,
div.actionAnimation,
div.accessibility,
div.captureFocusOnAction,
)
@@ -59,7 +59,6 @@ internal class DivGifImageBinder @Inject constructor(
div.pressStartActions,
div.pressEndActions,
div.actionAnimation,
div.accessibility,
div.captureFocusOnAction,
)
@@ -56,7 +56,6 @@ internal class DivGridBinder @Inject constructor(
div.pressStartActions,
div.pressEndActions,
div.actionAnimation,
div.accessibility,
div.captureFocusOnAction,
)
@@ -53,7 +53,6 @@ internal class DivImageBinder @Inject constructor(
div.pressStartActions,
div.pressEndActions,
div.actionAnimation,
div.accessibility,
div.captureFocusOnAction,
)
@@ -74,8 +74,6 @@ internal class DivInputBinder @Inject constructor(
path: DivStatePath
) {
val expressionResolver = bindingContext.expressionResolver
isFocusable = true
isFocusableInTouchMode = true
textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
accessibilityEnabled = accessibilityStateProvider.isAccessibilityEnabled(context)
@@ -30,7 +30,6 @@ internal class DivSeparatorBinder @Inject constructor(
div.pressStartActions,
div.pressEndActions,
div.actionAnimation,
div.accessibility,
div.captureFocusOnAction,
)
@@ -80,7 +80,6 @@ internal class DivTextBinder @Inject constructor(
div.pressStartActions,
div.pressEndActions,
div.actionAnimation,
div.accessibility,
div.captureFocusOnAction,
)
@@ -20,7 +20,6 @@ internal class DivPagerAdapter(
private val pageTranslations: SparseArray<Float>,
private val viewCreator: DivViewCreator,
path: DivStatePath,
private val accessibilityEnabled: Boolean,
private val pagerView: DivPagerView,
) : DivCollectionAdapter<DivPagerViewHolder>(bindingContext, path, items) {
@@ -65,7 +64,6 @@ internal class DivPagerAdapter(
view,
divBinder,
viewCreator,
accessibilityEnabled,
{ isHorizontal },
{ crossAxisAlignment },
)
@@ -91,7 +91,6 @@ internal class DivPagerBinder @Inject constructor(
pageTranslations,
viewCreator,
path,
a11yEnabled,
this
)
viewPager.adapter = adapter
@@ -1,7 +1,6 @@
package com.yandex.div.core.view2.divs.pager
import android.view.Gravity
import com.yandex.div.R
import com.yandex.div.core.state.DivStatePath
import com.yandex.div.core.util.doOnEveryDetach
import com.yandex.div.core.view2.BindingContext
@@ -22,7 +21,6 @@ internal class DivPagerViewHolder(
private val pageLayout: DivPagerPageLayout,
divBinder: DivBinder,
viewCreator: DivViewCreator,
private val accessibilityEnabled: Boolean,
private val isHorizontal: () -> Boolean,
private val crossAxisAlignment: () -> ItemAlignment,
) : DivCollectionViewHolder(pageLayout, parentContext, divBinder, viewCreator) {
@@ -40,10 +38,6 @@ internal class DivPagerViewHolder(
(pageLayout.child?.layoutParams as? DivLayoutParams)
?.setCrossAxisAlignment(div.value(), bindingContext.expressionResolver)
if (accessibilityEnabled) {
pageLayout.setTag(R.id.div_pager_item_clip_id, position)
}
}
private fun DivLayoutParams.setCrossAxisAlignment(div: DivBase, resolver: ExpressionResolver) {
@@ -1,11 +1,14 @@
package com.yandex.div.core.view2.divs.widgets
import com.yandex.div.core.Disposable
import com.yandex.div.internal.core.DivItemBuilderResult
internal interface DivCollectionHolder {
var items: List<DivItemBuilderResult>?
var accessibilityObserver: Disposable?
}
internal class DivCollectionHolderMixin : DivCollectionHolder {
override var items: List<DivItemBuilderResult>? = null
override var accessibilityObserver: Disposable? = null
}
@@ -50,14 +50,13 @@ internal class DivInputView @JvmOverloads constructor(
private var _hint: String? = null
private var _isFocusable = true
private var editorActionListener: OnEditorActionListener? = null
var enabled = true
internal set(value) {
field = value
isFocusable = _isFocusable
isFocusable = value
isFocusableInTouchMode = value
}
internal var accessibilityEnabled: Boolean = false
@@ -96,13 +95,6 @@ internal class DivInputView @JvmOverloads constructor(
drawBorderClipped(canvas, scrollX, scrollY) { super.draw(it) }
}
override fun setFocusable(focusable: Boolean) {
_isFocusable = focusable
val isFocusable = _isFocusable && enabled
super.setFocusable(isFocusable)
isFocusableInTouchMode = isFocusable
}
override fun setOnEditorActionListener(l: OnEditorActionListener?) {
super.setOnEditorActionListener(l)
editorActionListener = l
@@ -9,12 +9,13 @@ import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate
import androidx.viewpager2.widget.ViewPager2
import com.yandex.div.R
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.view2.divs.drawShadow
import com.yandex.div.core.view2.divs.pager.PagerSelectedActionsDispatcher
import com.yandex.div.core.widget.DivViewWrapper
import com.yandex.div.core.widget.ViewPager2Wrapper
import com.yandex.div.internal.widget.OnInterceptTouchEventListener
import com.yandex.div.internal.widget.OnInterceptTouchEventListenerHost
@@ -82,25 +83,29 @@ internal class DivPagerView @JvmOverloads constructor(
private val accessibilityDelegate by lazy(LazyThreadSafetyMode.NONE) {
val recycler = getRecyclerView() ?: return@lazy null
recycler.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS
object : RecyclerViewAccessibilityDelegate(recycler) {
override fun onRequestSendAccessibilityEvent(
host: ViewGroup, child: View, event: AccessibilityEvent
host: ViewGroup,
child: View,
event: AccessibilityEvent
): Boolean {
if (event.eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
getFocusedChildPos(child)?.let { pos ->
if (currentItem != pos) {
recycler.performAccessibilityAction(
if (pos > currentItem) AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
else AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD,
null
)
}
}
}
performActionIfNeeded(child, event)
return super.onRequestSendAccessibilityEvent(host, child, event)
}
private fun performActionIfNeeded(child: View, event: AccessibilityEvent) {
if (event.eventType != AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) return
val pos = getWrapperFor(child)?.let { recycler.getChildAdapterPosition(it) } ?: return
if (currentItem == pos || pos == RecyclerView.NO_POSITION) return
val action = if (pos > currentItem) {
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
} else {
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
}
recycler.performAccessibilityAction(action, null)
}
}
}
@@ -156,11 +161,11 @@ internal class DivPagerView @JvmOverloads constructor(
return wrappedChild.getChildAt(0)
}
private fun getFocusedChildPos(child: View): Int? {
var child = child
while (child != this) {
(child.getTag(R.id.div_pager_item_clip_id) as? Int)?.let { return it }
child = child.parent as? View ?: return null
private fun getWrapperFor(child: View): View? {
var parent = child
while (parent != getRecyclerView()) {
if (parent is DivViewWrapper) return parent
parent = parent.parent as? View ?: return null
}
return null
}
@@ -1,10 +1,11 @@
package com.yandex.div.core.widget
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup.LayoutParams
import com.yandex.div.core.util.makeFocusable
import androidx.core.view.isEmpty
import com.yandex.div.core.view2.BindingContext
import com.yandex.div.core.view2.divs.widgets.DivBorderDrawer
import com.yandex.div.core.view2.divs.widgets.DivBorderSupports
@@ -26,15 +27,17 @@ internal open class DivViewWrapper @JvmOverloads constructor(
): FrameContainerLayout(context, attrs, defStyleAttr), DivBorderSupports, TransientView by TransientViewMixin() {
val child: View?
get() = if (childCount == 0) null else getChildAt(0)
get() = if (isEmpty()) null else getChildAt(0)
init {
makeFocusable()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
defaultFocusHighlightEnabled = false
}
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
override fun addView(child: View?, index: Int, params: LayoutParams?) {
require(childCount == 0) { "ViewWrapper can host only one child view" }
require(isEmpty()) { "ViewWrapper can host only one child view" }
super.addView(child, 0, params)
}
@@ -111,4 +111,6 @@ internal open class ViewPager2Wrapper @JvmOverloads constructor(
}
return maxValue
}
override fun getAccessibilityClassName() = "androidx.viewpager.widget.ViewPager"
}
@@ -3,7 +3,6 @@ package com.yandex.div.internal.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
@@ -14,14 +13,15 @@ import android.widget.AbsListView.CHOICE_MODE_SINGLE
import android.widget.BaseAdapter
import android.widget.TextView
import androidx.appcompat.widget.ListPopupWindow
import androidx.core.graphics.drawable.toDrawable
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.view2.divs.dpToPx
import com.yandex.div.core.view2.divs.clearFocusOnClick
import com.yandex.div.core.view2.divs.dpToPx
import com.yandex.div.core.view2.reuse.InputFocusTracker
private const val POPUP_ITEM_HEIGHT = 48
internal open class SelectView constructor(context: Context) : EllipsizedTextView(context) {
internal open class SelectView(context: Context) : EllipsizedTextView(context) {
init {
this.setOnClickListener {
focusTracker?.let { tracker -> clearFocusOnClick(tracker) }
@@ -45,7 +45,7 @@ internal open class SelectView constructor(context: Context) : EllipsizedTextVie
}
setOverlapAnchor(true)
setBackgroundDrawable(ColorDrawable(Color.WHITE))
setBackgroundDrawable(Color.WHITE.toDrawable())
setAdapter(adapter)
}
@@ -87,6 +87,8 @@ internal open class SelectView constructor(context: Context) : EllipsizedTextVie
info.text = text
}
override fun getAccessibilityClassName() = "android.widget.Spinner"
@Mockable
private class PopupWindow @JvmOverloads constructor(
private val context: Context,
@@ -28,7 +28,7 @@ internal open class TextViewWithAccessibleSpans(
init {
AccessibilityStateProvider.evaluateTouchModeEnabled(context)
if (AccessibilityStateProvider.touchModeEnabled == true) {
if (AccessibilityStateProvider.touchExplorationEnabled == true) {
spanHelper = SpanHelper()
ViewCompat.setAccessibilityDelegate(this, spanHelper)
accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_POLITE
@@ -38,7 +38,7 @@ internal open class TextViewWithAccessibleSpans(
}
internal fun addImageSpan(span: ImageSpan) {
if (AccessibilityStateProvider.touchModeEnabled == true) {
if (AccessibilityStateProvider.touchExplorationEnabled == true) {
imageSpans.add(span)
if (span.accessibility?.contentDescription != null || span.accessibility?.onClickAction != null) {
accessibleImageSpans.add(span)
@@ -56,7 +56,7 @@ internal open class TextViewWithAccessibleSpans(
}
private fun evaluateAndSetContentDescription() {
if (AccessibilityStateProvider.touchModeEnabled != true) {
if (AccessibilityStateProvider.touchExplorationEnabled != true) {
super.setContentDescription(_contentDescription)
return
}
@@ -0,0 +1,150 @@
package com.yandex.div.internal.widget.slider
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT
import android.widget.SeekBar
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.customview.widget.ExploreByTouchHelper
import com.yandex.div.R
import com.yandex.div.internal.widget.slider.SliderView.Companion.boundsHeight
import com.yandex.div.internal.widget.slider.SliderView.Companion.boundsWidth
import com.yandex.div.internal.widget.slider.SliderView.Thumb
import kotlin.math.max
import kotlin.math.roundToInt
private const val THUMB_VIRTUAL_VIEW_ID = 0
private const val SECONDARY_THUMB_VIRTUAL_VIEW_ID = 1
/**
* Provides info about virtual view hierarchy for accessibility services.
*/
internal class SliderAccessibilityHelper(private val slider: SliderView) : ExploreByTouchHelper(slider) {
private val bounds = Rect()
private val step get() = max(((slider.maxValue - slider.minValue) * 0.05).roundToInt(), 1)
init {
ViewCompat.setAccessibilityDelegate(slider, this)
slider.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_POLITE
}
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) = Unit
override fun getVirtualViewAt(x: Float, y: Float): Int {
if (x < slider.paddingLeft) return THUMB_VIRTUAL_VIEW_ID
return when (slider.getClosestThumb(x.toInt())) {
Thumb.THUMB -> THUMB_VIRTUAL_VIEW_ID
Thumb.THUMB_SECONDARY -> SECONDARY_THUMB_VIRTUAL_VIEW_ID
}
}
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
virtualViewIds.add(THUMB_VIRTUAL_VIEW_ID)
slider.thumbSecondaryValue?.let {
virtualViewIds.add(SECONDARY_THUMB_VIRTUAL_VIEW_ID)
}
}
override fun onPopulateNodeForVirtualView(
virtualViewId: Int,
node: AccessibilityNodeInfoCompat
) {
node.apply {
className = SeekBar::class.java.name
rangeInfo = AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(
RANGE_TYPE_INT,
slider.minValue,
slider.maxValue,
virtualViewId.toThumbValue()
)
val description = StringBuilder()
slider.contentDescription?.let { description.append(it).append(",") }
description.append(startOrEndDescription(virtualViewId))
contentDescription = description.toString()
addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_FORWARD)
addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_BACKWARD)
updateBounds(virtualViewId)
setBoundsInParent(bounds)
}
}
override fun onPerformActionForVirtualView(virtualViewId: Int, action: Int, arguments: Bundle?): Boolean {
val value = when (action) {
android.R.id.accessibilityActionSetProgress -> {
if (arguments?.containsKey(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE) != true) {
return false
}
arguments.getFloat(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE)
}
AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD -> virtualViewId.toThumbValue() + step
AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD -> virtualViewId.toThumbValue() - step
else -> return false
}
setThumbValue(virtualViewId, value)
return true
}
private fun startOrEndDescription(virtualViewId: Int): String {
return when {
slider.thumbSecondaryValue == null -> ""
virtualViewId == THUMB_VIRTUAL_VIEW_ID -> slider.context.getString(R.string.div_slider_range_start)
virtualViewId == SECONDARY_THUMB_VIRTUAL_VIEW_ID -> slider.context.getString(R.string.div_slider_range_end)
else -> ""
}
}
private fun updateBounds(index: Int) {
val width: Int
val height: Int
when (index) {
SECONDARY_THUMB_VIRTUAL_VIEW_ID -> {
width = slider.thumbSecondaryDrawable.boundsWidth
height = slider.thumbSecondaryDrawable.boundsHeight
}
else -> {
width = slider.thumbDrawable.boundsWidth
height = slider.thumbDrawable.boundsHeight
}
}
val position = slider.getPositionInView(index.toThumbValue())
bounds.apply {
left = position
right = position + width
top = (slider.height + slider.paddingTop - slider.paddingBottom - height) / 2
bottom = (slider.height + slider.paddingTop - slider.paddingBottom + height) / 2
}
}
private fun setThumbValue(virtualViewId: Int, value: Float) {
slider.setValueToAccessibilityThumb(virtualViewId.toThumb(), value)
sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED)
invalidateVirtualView(virtualViewId)
}
private fun Int.toThumb(): Thumb {
return when {
this == THUMB_VIRTUAL_VIEW_ID -> Thumb.THUMB
slider.thumbSecondaryValue != null -> Thumb.THUMB_SECONDARY
else -> Thumb.THUMB
}
}
private fun Int.toThumbValue(): Float {
return if (this == THUMB_VIRTUAL_VIEW_ID) {
slider.thumbValue
} else {
slider.thumbSecondaryValue ?: slider.thumbValue
}
}
}
@@ -0,0 +1,22 @@
package com.yandex.div.internal.widget.slider
import android.animation.Animator
internal class SliderThumbAnimatorListener(
private val onAnimationEnd: (Boolean) -> Unit
) : Animator.AnimatorListener {
private var hasCanceled = false
override fun onAnimationEnd(animation: Animator) = onAnimationEnd(hasCanceled)
override fun onAnimationCancel(animation: Animator) {
hasCanceled = true
}
override fun onAnimationStart(animation: Animator) {
hasCanceled = false
}
override fun onAnimationRepeat(animation: Animator) = Unit
}
@@ -1,6 +1,5 @@
package com.yandex.div.internal.widget.slider
import android.animation.Animator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
@@ -8,21 +7,13 @@ import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.SeekBar
import androidx.annotation.Px
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.customview.widget.ExploreByTouchHelper
import com.yandex.div.R
import com.yandex.div.core.ObserverList
import com.yandex.div.core.util.isLayoutRtl
import com.yandex.div.internal.widget.slider.shapes.TextDrawable
@@ -40,8 +31,6 @@ private const val UNSET_VALUE = -1
private const val DEFAULT_ANIMATION_DURATION = 300L
private const val DEFAULT_ANIMATION_ENABLED = true
private const val DEFAULT_INTERCEPTION_ANGLE = 45f
private const val THUMB_VIRTUAL_VIEW_ID = 0
private const val SECONDARY_THUMB_VIRTUAL_VIEW_ID = 1
open class SliderView @JvmOverloads constructor(
context: Context,
@@ -70,56 +59,25 @@ open class SliderView @JvmOverloads constructor(
}
private var sliderAnimator: ValueAnimator? = null
private var prevThumbValue: Float = DEFAULT_MIN_VALUE
private var sliderSecondaryAnimator: ValueAnimator? = null
private var prevThumbSecondaryValue: Float? = null
private val animatorListener = object : Animator.AnimatorListener {
var prevThumbValue: Float = DEFAULT_MIN_VALUE
private var hasCanceled = false
override fun onAnimationEnd(animation: Animator) {
sliderAnimator = null
if (!hasCanceled) {
notifyThumbChangedListeners(prevThumbValue, thumbValue)
}
private val animatorListener = SliderThumbAnimatorListener { hasCanceled ->
sliderAnimator = null
if (!hasCanceled) {
notifyThumbChangedListeners(prevThumbValue, thumbValue)
}
override fun onAnimationCancel(animation: Animator) {
hasCanceled = true
}
override fun onAnimationStart(animation: Animator) {
hasCanceled = false
}
override fun onAnimationRepeat(animation: Animator) = Unit
}
private val animatorSecondaryListener = object : Animator.AnimatorListener {
var prevThumbSecondaryValue: Float? = null
private var hasCanceled = false
override fun onAnimationEnd(animation: Animator) {
sliderSecondaryAnimator = null
if (!hasCanceled) {
notifyThumbSecondaryChangedListeners(
prevThumbSecondaryValue,
thumbSecondaryValue
)
}
private val animatorSecondaryListener = SliderThumbAnimatorListener { hasCanceled ->
sliderSecondaryAnimator = null
if (!hasCanceled) {
notifyThumbSecondaryChangedListeners(
prevThumbSecondaryValue,
thumbSecondaryValue
)
}
override fun onAnimationCancel(animation: Animator) {
hasCanceled = true
}
override fun onAnimationStart(animation: Animator) {
hasCanceled = false
}
override fun onAnimationRepeat(animation: Animator) = Unit
}
val ranges = mutableListOf<Range>()
@@ -138,7 +96,7 @@ open class SliderView @JvmOverloads constructor(
* This allows the basic animation effects (alpha, scale, translate, rotate) to be accelerated,
* decelerated, repeated, etc.
*/
var animationInterpolator = AccelerateDecelerateInterpolator()
private var animationInterpolator = AccelerateDecelerateInterpolator()
/**
* Animation enabled flag.
@@ -240,7 +198,7 @@ open class SliderView @JvmOverloads constructor(
if (thumbValue == newValue) return
if (animated && animationEnabled) {
if (sliderAnimator == null) {
animatorListener.prevThumbValue = thumbValue
prevThumbValue = thumbValue
}
sliderAnimator?.cancel()
sliderAnimator = ValueAnimator.ofFloat(thumbValue, newValue).apply {
@@ -257,9 +215,9 @@ open class SliderView @JvmOverloads constructor(
sliderAnimator?.cancel()
}
if (forced || sliderAnimator == null) {
animatorListener.prevThumbValue = thumbValue
prevThumbValue = thumbValue
thumbValue = newValue
notifyThumbChangedListeners(animatorListener.prevThumbValue, thumbValue)
notifyThumbChangedListeners(prevThumbValue, thumbValue)
}
}
invalidate()
@@ -277,7 +235,7 @@ open class SliderView @JvmOverloads constructor(
/**
* The appearance of thumb text.
* Returns [null] if doesn't exist.
* Returns null if doesn't exist.
*/
var thumbTextDrawable: TextDrawable? = null
set(drawable) {
@@ -287,21 +245,16 @@ open class SliderView @JvmOverloads constructor(
/**
* The value of thumb secondary. Should be in range from [minValue] to [maxValue].
* Returns [null] if doesn't exist.
* Returns null if doesn't exist.
*/
var thumbSecondaryValue: Float? = null
private set
private val a11yHelper: A11yHelper = A11yHelper(this)
init {
ViewCompat.setAccessibilityDelegate(this, a11yHelper)
accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_POLITE
}
private val a11yHelper = SliderAccessibilityHelper(this)
/**
* Set the value of thumb secondary.
* @param value should be in range from [minValue] to [maxValue] or be [null]
* @param value should be in range from [minValue] to [maxValue] or be null
* @param animated change value with animation
*/
fun setThumbSecondaryValue(value: Float?, animated: Boolean = animationEnabled) {
@@ -310,7 +263,7 @@ open class SliderView @JvmOverloads constructor(
/**
* Tries to set the value of thumb secondary.
* @param value should be in range from [minValue] to [maxValue] or be [null]
* @param value should be in range from [minValue] to [maxValue] or be null
* @param animated change value with animation
* @param forced if [animated] is false and [forced] is true,
* then value will be set regardless of the running animation
@@ -324,7 +277,7 @@ open class SliderView @JvmOverloads constructor(
if (thumbSecondaryValue == newValue) return
if (animated && animationEnabled && thumbSecondaryValue != null && newValue != null) {
if (sliderSecondaryAnimator == null) {
animatorSecondaryListener.prevThumbSecondaryValue = thumbSecondaryValue
prevThumbSecondaryValue = thumbSecondaryValue
}
sliderSecondaryAnimator?.cancel()
sliderSecondaryAnimator = ValueAnimator.ofFloat(thumbSecondaryValue!!, newValue).apply {
@@ -341,10 +294,10 @@ open class SliderView @JvmOverloads constructor(
sliderSecondaryAnimator?.cancel()
}
if (forced || sliderSecondaryAnimator == null) {
animatorSecondaryListener.prevThumbSecondaryValue = thumbSecondaryValue
prevThumbSecondaryValue = thumbSecondaryValue
thumbSecondaryValue = newValue
notifyThumbSecondaryChangedListeners(
animatorSecondaryListener.prevThumbSecondaryValue,
prevThumbSecondaryValue,
thumbSecondaryValue
)
}
@@ -354,7 +307,7 @@ open class SliderView @JvmOverloads constructor(
/**
* The appearance of thumb secondary.
* Returns [null] if doesn't exist.
* Returns null if doesn't exist.
*/
var thumbSecondaryDrawable: Drawable? = null
set(value) {
@@ -365,7 +318,7 @@ open class SliderView @JvmOverloads constructor(
/**
* The appearance of thumb secondary text.
* Returns [null] if doesn't exist.
* Returns null if doesn't exist.
*/
var thumbSecondTextDrawable: TextDrawable? = null
set(drawable) {
@@ -393,10 +346,6 @@ open class SliderView @JvmOverloads constructor(
listeners.addObserver(listener)
}
fun removeOnChangedListener(listener: ChangedListener) {
listeners.removeObserver(listener)
}
fun clearOnThumbChangedListener() = listeners.clear()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -441,10 +390,6 @@ open class SliderView @JvmOverloads constructor(
return maxOf(thumbHeight, trackHeight, rangesHeight)
}
private val Drawable?.boundsWidth get() = this?.bounds?.width() ?: 0
private val Drawable?.boundsHeight get() = this?.bounds?.height() ?: 0
override fun getSuggestedMinimumWidth(): Int {
val tickCount = (maxValue - minValue + 1).toInt()
@@ -592,7 +537,7 @@ open class SliderView @JvmOverloads constructor(
return false
}
private fun getClosestThumb(position: Int): Thumb {
internal fun getClosestThumb(position: Int): Thumb {
if (!isThumbSecondaryEnabled()) {
return Thumb.THUMB
}
@@ -618,6 +563,9 @@ open class SliderView @JvmOverloads constructor(
return a11yHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
}
internal fun setValueToAccessibilityThumb(thumb: Thumb, value: Float) =
setValueToThumb(thumb, value.inBoarders(), animated = false, forced = true)
private fun setValueToThumb(thumb: Thumb, value: Float, animated: Boolean, forced: Boolean = false) =
when (thumb) {
Thumb.THUMB -> trySetThumbValue(value, animated, forced)
@@ -683,6 +631,8 @@ open class SliderView @JvmOverloads constructor(
interpolator = animationInterpolator
}
internal fun getPositionInView(thumbValue: Float) = thumbValue.toPosition() + paddingLeft
/**
* Calculates slider active range presented by two values, where start <= end.
* If there is only one thumb then its value will be considered as the end of the range.
@@ -714,7 +664,7 @@ open class SliderView @JvmOverloads constructor(
}
}
private enum class Thumb {
internal enum class Thumb {
THUMB, THUMB_SECONDARY
}
@@ -732,114 +682,9 @@ open class SliderView @JvmOverloads constructor(
@Px var endPosition = 0
}
/**
* Provides info about virtual view hierarchy for accessibility services.
*/
private inner class A11yHelper(private val slider: SliderView) : ExploreByTouchHelper(slider) {
private val bounds = Rect()
private val step get() = max(((maxValue - minValue) * 0.05).roundToInt(), 1)
internal companion object {
val Drawable?.boundsWidth get() = this?.bounds?.width() ?: 0
override fun getVirtualViewAt(x: Float, y: Float): Int {
if (x < leftPaddingOffset) {
return THUMB_VIRTUAL_VIEW_ID
}
val position = when (getClosestThumb(x.toInt())) {
Thumb.THUMB -> THUMB_VIRTUAL_VIEW_ID
Thumb.THUMB_SECONDARY -> SECONDARY_THUMB_VIRTUAL_VIEW_ID
}
return position
}
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
virtualViewIds.add(THUMB_VIRTUAL_VIEW_ID)
if (thumbSecondaryValue != null) virtualViewIds.add(SECONDARY_THUMB_VIRTUAL_VIEW_ID)
}
override fun onPopulateNodeForVirtualView(
virtualViewId: Int,
node: AccessibilityNodeInfoCompat
) {
node.className = SeekBar::class.java.name
node.rangeInfo = AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(
RANGE_TYPE_INT, minValue, maxValue, virtualViewId.toThumbValue()
)
val contentDescription = StringBuilder()
slider.contentDescription?.let {
contentDescription.append(it).append(",")
}
contentDescription.append(startOrEndDescription(virtualViewId))
node.contentDescription = contentDescription.toString()
node.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_FORWARD)
node.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_BACKWARD)
updateBounds(virtualViewId)
node.setBoundsInParent(bounds)
}
override fun onPerformActionForVirtualView(virtualViewId: Int, action: Int, arguments: Bundle?): Boolean {
when (action) {
android.R.id.accessibilityActionSetProgress -> {
if (arguments == null || !arguments.containsKey(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE)) {
return false
}
val value = arguments.getFloat(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE)
setThumbValue(virtualViewId, value)
}
AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD ->
setThumbValue(virtualViewId, virtualViewId.toThumbValue() + step)
AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD ->
setThumbValue(virtualViewId, virtualViewId.toThumbValue() - step)
else -> return false
}
return true
}
private fun startOrEndDescription(virtualViewId: Int): String {
return when {
thumbSecondaryValue == null -> ""
virtualViewId == THUMB_VIRTUAL_VIEW_ID -> context.getString(R.string.div_slider_range_start)
virtualViewId == SECONDARY_THUMB_VIRTUAL_VIEW_ID -> context.getString(R.string.div_slider_range_end)
else -> ""
}
}
private fun updateBounds(index: Int) {
val width: Int
val height: Int
when (index) {
SECONDARY_THUMB_VIRTUAL_VIEW_ID -> {
width = thumbSecondaryDrawable.boundsWidth
height = thumbSecondaryDrawable.boundsHeight
}
else -> {
width = thumbDrawable.boundsWidth
height = thumbDrawable.boundsHeight
}
}
val position = index.toThumbValue().toPosition() + slider.paddingLeft
bounds.left = position
bounds.right = position + width
bounds.top = slider.height / 2 - height / 2
bounds.bottom = slider.height / 2 + height / 2
}
private fun setThumbValue(virtualViewId: Int, value: Float) {
setValueToThumb(virtualViewId.toThumb(), value.inBoarders(), animated = false, forced = true)
sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED)
invalidateVirtualView(virtualViewId)
}
private fun Int.toThumb(): Thumb = when {
this == THUMB_VIRTUAL_VIEW_ID -> Thumb.THUMB
thumbSecondaryValue != null -> Thumb.THUMB_SECONDARY
else -> Thumb.THUMB
}
private fun Int.toThumbValue(): Float = when {
this == THUMB_VIRTUAL_VIEW_ID -> thumbValue
else -> thumbSecondaryValue ?: thumbValue
}
val Drawable?.boundsHeight get() = this?.bounds?.height() ?: 0
}
}
@@ -12,9 +12,6 @@ import android.text.TextUtils;
import android.text.method.TransformationMethod;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -146,10 +143,6 @@ public final class TabView extends SuperLineHeightTextView {
if (mBoldTextOnSelection && changed) {
setupTypeface();
}
if (changed && selected) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
}
private void setupTypeface() {
@@ -178,19 +171,9 @@ public final class TabView extends SuperLineHeightTextView {
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
// This view masquerades as an action bar tab.
public CharSequence getAccessibilityClassName() {
//noinspection deprecation
event.setClassName(ActionBar.Tab.class.getName());
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
// This view masquerades as an action bar tab.
//noinspection deprecation
info.setClassName(ActionBar.Tab.class.getName());
return ActionBar.Tab.class.getName();
}
/**
@@ -84,4 +84,6 @@ internal open class TabsLayout @JvmOverloads constructor(
bottomMargin = resources.getDimensionPixelSize(R.dimen.title_tab_title_margin_vertical)
}
}
override fun getAccessibilityClassName() = "android.widget.TabWidget"
}
@@ -30,7 +30,6 @@
<item name="div_transition_position" type="id"/>
<item name="div_releasable_list" type="id"/>
<item name="div_pager_item_clip_id" type="id"/>
<item name="div_layout_provider_listener_id" type="id"/>
</resources>
@@ -0,0 +1,77 @@
package com.yandex.div.core.view2
import android.app.Activity
import android.view.View
import com.yandex.div.DivDataTag
import com.yandex.div.core.Div2Context
import com.yandex.div.core.DivConfiguration
import com.yandex.div.core.images.DivImageDownloadCallback
import com.yandex.div.core.images.DivImageLoader
import com.yandex.div.core.images.LoadReference
import com.yandex.div.core.util.AccessibilityStateProvider
import com.yandex.div.core.view2.divs.UnitTestData
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class TypeAutoAccessibilityTest {
private val activity = Robolectric.buildActivity(Activity::class.java).get()
private val imageLoader = mock<DivImageLoader> {
on { loadImage(any(), any<DivImageDownloadCallback>()) } doReturn LoadReference { }
}
private val divContext = Div2Context(activity, DivConfiguration.Builder(imageLoader).build())
private val data = UnitTestData("accessibility", "auto_types.json", "regression_test_data").dataWithTemplates
private val divView: Div2View
init {
AccessibilityStateProvider.touchExplorationEnabled = true
divView = Div2View(divContext)
divView.setData(data, DivDataTag("tag"))
}
@Test
fun `text is important for accessibility`() {
checkViewIsImportantForAccessibility("text")
}
@Test
fun `input is important for accessibility`() {
checkViewIsImportantForAccessibility("input")
}
@Test
fun `select is important for accessibility`() {
checkViewIsImportantForAccessibility("select")
}
@Test
fun `image with description is important for accessibility`() {
checkViewIsImportantForAccessibility("image with description")
}
@Test
fun `image without action or description is not important for accessibility`() {
checkViewIsImportantForAccessibility("image", isImportant = false)
}
@Test
fun `slider is important for accessibility`() {
checkViewIsImportantForAccessibility("slider")
}
@Test
fun `separator is not important for accessibility`() {
checkViewIsImportantForAccessibility("separator", isImportant = false)
}
private fun checkViewIsImportantForAccessibility(tag: String, isImportant: Boolean = true) {
Assert.assertEquals(isImportant, divView.findViewWithTag<View>(tag)?.isImportantForAccessibility)
}
}
@@ -25,7 +25,6 @@ internal fun assertActionApplied(context: BindingContext, target: View, actionUr
pressStartActions = anyOrNull(),
pressEndActions = anyOrNull(),
actionAnimation = any(),
accessibility = anyOrNull(),
captureFocusOnAction = any(),
)
@@ -33,7 +33,8 @@ import org.robolectric.RuntimeEnvironment
open class DivBinderTest {
internal val actionBinder = mock<DivActionBinder> {
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), any(), anyOrNull(), any()) }.thenCallRealMethod()
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(),
anyOrNull(), any(), any()) }.thenCallRealMethod()
}
private val mockComponent = mock<Div2Component>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {
on { actionBinder } doReturn actionBinder
@@ -83,6 +84,7 @@ open class DivBinderTest {
private val viewIdProvider = DivViewIdProvider()
@Suppress("unused")
private val viewComponent = mock<Div2ViewComponent>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {
on { viewIdProvider } doReturn viewIdProvider
on { releaseViewVisitor } doReturn visitor
@@ -38,7 +38,8 @@ internal fun divView(
)
val actionBinder = mock<DivActionBinder> {
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), any(), anyOrNull(), any()) }.thenCallRealMethod()
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(),
anyOrNull(), any(), any()) }.thenCallRealMethod()
}
val component = mock<Div2Component>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {
@@ -12,6 +12,7 @@ import java.io.File
class UnitTestData(
private val dir: String,
private val fileName: String,
private val testDataDir: String = "unit_test_data"
) {
val data: DivData
@@ -21,7 +22,7 @@ class UnitTestData(
val div: Div
get() {
if (_div == null) {
val path = "${BuildConfig.DIV2_JSON_PATH}/unit_test_data/$dir/$fileName"
val path = "${BuildConfig.DIV2_JSON_PATH}/$testDataDir/$dir/$fileName"
val jsonString = File(path).readText(Charsets.UTF_8)
val json = JSONObject(jsonString)
val environment = DivParsingEnvironment(ParsingErrorLogger.LOG)
@@ -32,7 +33,7 @@ class UnitTestData(
val dataWithTemplates: DivData
get() {
val path = "${BuildConfig.DIV2_JSON_PATH}/unit_test_data/$dir/$fileName"
val path = "${BuildConfig.DIV2_JSON_PATH}/$testDataDir/$dir/$fileName"
val jsonString = File(path).readText(Charsets.UTF_8)
val json = JSONObject(jsonString)
val templates = json.getJSONObject("templates")
@@ -46,7 +47,7 @@ class UnitTestData(
val patchWithTemplates: DivPatch
get() {
val path = "${BuildConfig.DIV2_JSON_PATH}/unit_test_data/$dir/$fileName"
val path = "${BuildConfig.DIV2_JSON_PATH}/$testDataDir/$dir/$fileName"
val jsonString = File(path).readText(Charsets.UTF_8)
val json = JSONObject(jsonString)
val templates = json.getJSONObject("templates")
@@ -10,7 +10,7 @@ import org.junit.Test
class DivFocusableInputTest {
private val activityTestRule = ActivityTestRule(DummyActivity::class.java)
private val activityTestRule = ActivityTestRule(DummyActivity::class.java, true)
@get:Rule
val rule = uiTestRule { activityTestRule }
@@ -7,13 +7,15 @@ import com.yandex.divkit.demo.DummyActivity
import org.junit.Rule
import org.junit.Test
private const val TEXT_BEFORE_BREAK = "https://Text_with different+symbols(123)@site.ru"
internal const val TEXT_WITH_DIFFERENT_SYMBOLS = "$TEXT_BEFORE_BREAK\nsecond_line"
internal const val SEARCH_KEY_TAPPED = "Search key tapped!"
internal const val TEXT_WITH_DIFFERENT_SYMBOLS =
"https://Text_with different\nsymbols+(123)@site.ru"
private const val TEXT_WITHOUT_BREAK = "https://Text_with differentsymbols+(123)@site.ru"
private const val SEARCH_KEY_TAPPED = "Search key tapped!"
class DivInputKeyboardTypeTest {
private val activityRule = ActivityTestRule(DummyActivity::class.java)
private val activityRule = ActivityTestRule(DummyActivity::class.java, true)
@get:Rule
val rule = uiTestRule { activityRule }
@@ -30,7 +32,7 @@ class DivInputKeyboardTypeTest {
fun checkSingleLineText() {
checkType(
type = "single_line_text",
expectedText = TEXT_BEFORE_BREAK
expectedText = TEXT_WITHOUT_BREAK
)
}
@@ -72,7 +74,7 @@ class DivInputKeyboardTypeTest {
fun checkEmail() {
checkType(
type = "email",
expectedText = TEXT_BEFORE_BREAK
expectedText = TEXT_WITHOUT_BREAK
)
}
@@ -80,7 +82,7 @@ class DivInputKeyboardTypeTest {
fun checkUri() {
checkType(
type = "uri",
expectedText = TEXT_BEFORE_BREAK
expectedText = TEXT_WITHOUT_BREAK
)
}
@@ -1,88 +0,0 @@
package com.yandex.div
import androidx.test.rule.ActivityTestRule
import com.yandex.div.rule.uiTestRule
import com.yandex.div.steps.accessibility
import com.yandex.divkit.demo.DummyActivity
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class TypeAutoAccessibilityTest {
private val activityTestRule = ActivityTestRule(DummyActivity::class.java)
@get:Rule
val rule = uiTestRule { activityTestRule }
@Before
fun buildContainer() {
accessibility {
testAsset = "regression_test_data/accessibility/auto_types.json"
activityTestRule.buildContainer()
}
}
@Test
fun checkTextHasCorrectClassnameAndFocusable() {
accessibility {
assert {
checkTextIsFocusable()
}
}
}
@Test
fun checkInputHasCorrectClassnameAndFocusable() {
accessibility {
assert {
checkInputIsFocusable()
}
}
}
@Test
fun checkSelectHasCorrectClassnameAndFocusable() {
accessibility {
assert {
checkSelectIsFocusable()
}
}
}
@Test
fun checkImageWithDescriptionHasCorrectClassnameAndFocusable() {
accessibility {
assert {
checkImageWithDescriptionIsFocusable()
}
}
}
@Test
fun checkImageWithoutActionOrDescriptionHasNoClassnameAndNotFocusable() {
accessibility {
assert {
checkImageWithoutDescriptionIsNotFocusable()
}
}
}
@Test
fun checkSliderHasCorrectClassnameAndNotFocusable() {
accessibility {
assert {
// slider itself should be to focusable, only it's thumbs are focusable
checkSliderIsNotFocusable()
}
}
}
@Test
fun checkSeparatorHasNoClassnameAndNotFocusable() {
accessibility {
assert {
checkSeparatorIsNotFocusable()
}
}
}
}
@@ -11,7 +11,7 @@ open class ActivityParamsTestRule<A : Activity>(
launchActivity: Boolean = true,
private val action: String? = null,
private val params: Bundle? = null
) : IntentsTestRule<A>(activityClass, false, launchActivity) {
) : IntentsTestRule<A>(activityClass, true, launchActivity) {
constructor(activityClass: Class<A>, vararg params: Pair<String, Any?>) : this(
activityClass = activityClass,
@@ -1,26 +1,39 @@
package com.yandex.div.steps
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import com.yandex.div.internal.widget.slider.SliderView
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.equalTo
object SliderViews {
val commonSlider get() = onView(isAssignableFrom(SliderView::class.java))
val defaultSlider get() = onView(allOf(withContentDescription("Default slider"),
isAssignableFrom(SliderView::class.java)))
val commonSlider: ViewInteraction get() = onView(isAssignableFrom(SliderView::class.java))
val max10Slider get() = onView(allOf(withContentDescription("Min - 0, max - 10"),
isAssignableFrom(SliderView::class.java)))
val defaultSlider: ViewInteraction get() = onView(allOf(
withTagValue(equalTo("default_slider")),
isAssignableFrom(SliderView::class.java))
)
val max3Slider get() = onView(allOf(withContentDescription("Min - 0, max - 3"),
isAssignableFrom(SliderView::class.java)))
val max10Slider: ViewInteraction get() = onView(allOf(
withTagValue(equalTo("min_0_max_10")),
isAssignableFrom(SliderView::class.java))
)
val doubleDefaultSlider get() = onView(allOf(withContentDescription("Double without ticks"),
isAssignableFrom(SliderView::class.java)))
val max3Slider: ViewInteraction get() = onView(allOf(
withTagValue(equalTo("min_0_max_3")),
isAssignableFrom(SliderView::class.java))
)
val doubleWithDivisionsSlider get() = onView(allOf(withContentDescription("Double with ticks"),
isAssignableFrom(SliderView::class.java)))
val doubleDefaultSlider: ViewInteraction get() = onView(allOf(
withTagValue(equalTo("double_without_ticks")),
isAssignableFrom(SliderView::class.java))
)
val doubleWithDivisionsSlider: ViewInteraction get() = onView(allOf(
withTagValue(equalTo("double_with_ticks")),
isAssignableFrom(SliderView::class.java))
)
}
@@ -1,79 +0,0 @@
package com.yandex.div.steps
import android.view.ViewGroup
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isFocusable
import androidx.test.espresso.matcher.ViewMatchers.isNotFocusable
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import androidx.test.rule.ActivityTestRule
import com.yandex.test.util.Report.step
import com.yandex.test.util.StepsDsl
import org.hamcrest.Matchers.`is`
private const val TEXT_ID = "text"
private const val INPUT_ID = "input"
private const val SELECT_ID = "select"
private const val IMAGE_ID = "image"
private const val IMAGE_WITH_DESCRIPTION_ID = "image with description"
private const val SLIDER_ID = "slider"
private const val SEPARATOR_ID = "separator"
private val textView get() = getViewWithId(TEXT_ID)
private val inputView get() = getViewWithId(INPUT_ID)
private val selectView get() = getViewWithId(SELECT_ID)
private val imageView get() = getViewWithId(IMAGE_ID)
private val imageWithDescriptionView get() = getViewWithId(IMAGE_WITH_DESCRIPTION_ID)
private val sliderView get() = getViewWithId(SLIDER_ID)
private val separatorView get() = getViewWithId(SEPARATOR_ID)
internal fun accessibility(f: TypeAutoAccessibilitySteps.() -> Unit) = f(TypeAutoAccessibilitySteps())
@StepsDsl
internal class TypeAutoAccessibilitySteps: DivTestAssetSteps() {
fun ActivityTestRule<*>.buildContainer(): Unit = step("Build container") {
buildContainer(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
internal fun assert(f: TypeAutoAccessibilityAssertions.() -> Unit) = f(TypeAutoAccessibilityAssertions())
}
@StepsDsl
internal class TypeAutoAccessibilityAssertions {
fun checkTextIsFocusable(): Unit = step("Check text view is focusable ") {
textView.checkFocusable()
}
fun checkInputIsFocusable(): Unit = step("Check input view is focusable ") {
inputView.checkFocusable()
}
fun checkSelectIsFocusable(): Unit = step("Check select view is focusable ") {
selectView.checkFocusable()
}
fun checkImageWithDescriptionIsFocusable(): Unit =
step("Check image with description is focusable ") {
imageWithDescriptionView.checkFocusable()
}
fun checkSeparatorIsNotFocusable(): Unit = step("Check separator is not focusable") {
separatorView.checkNotFocusable()
}
fun checkImageWithoutDescriptionIsNotFocusable(): Unit =
step("Check image without description is not focusable") {
imageView.checkNotFocusable()
}
fun checkSliderIsNotFocusable(): Unit = step("Check slider is not focusable") {
sliderView.checkNotFocusable() // only it's virtual view should be focusable
}
private fun ViewInteraction.checkFocusable(): Unit {
check(matches(isFocusable()))
}
private fun ViewInteraction.checkNotFocusable(): Unit {
check(matches(isNotFocusable()))
}
}
private fun getViewWithId(id: String) = onView(withTagValue(`is`(id)))
@@ -4,17 +4,15 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.FOCUS_BEFORE_DESCENDANTS
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.descendants
import androidx.recyclerview.widget.RecyclerView
import com.yandex.div.core.Div2Context
import com.yandex.div.core.view2.Div2View
import com.yandex.divkit.demo.R
@@ -76,8 +74,8 @@ class DivScreenshotActivity : AppCompatActivity() {
ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, divViewHeight)
id = R.id.morda_screenshot_div
removeAutofocusForOldApis()
hideCursor()
focusRecyclersBeforeDescendants()
}
applyConfiguration(divView)
setContentView(divView)
@@ -85,6 +83,12 @@ class DivScreenshotActivity : AppCompatActivity() {
fun getTestCaseJson() = assetReader.read(cardAssetName)
private fun View.removeAutofocusForOldApis() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
isFocusableInTouchMode = true
}
}
private fun ViewGroup.hideCursor() {
for (child in children) {
if (child is EditText) {
@@ -96,14 +100,6 @@ class DivScreenshotActivity : AppCompatActivity() {
}
}
private fun ViewGroup.focusRecyclersBeforeDescendants() {
descendants.forEach {
if (it is RecyclerView) {
it.descendantFocusability = FOCUS_BEFORE_DESCENDANTS
}
}
}
private fun applyConfiguration(view: View) {
val divJson = getTestCaseJson()
val configuration = if (divJson.has("configuration")) {
@@ -31,11 +31,11 @@ class Div2FocusScreenshotTest(case: String, escapedCase: String) {
@Test
@Screenshot(viewId = R.id.morda_screenshot_div, relativePath = "not_focused")
fun divScreenshotTopFocused() = Unit
fun divScreenshotNotFocused() = Unit
@Test
@Screenshot(viewId = R.id.morda_screenshot_div, relativePath = "focused")
fun divScreenshotBottomFocused() {
fun divScreenshotFocused() {
divFocus { clickOnTopInput() }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

@@ -11,7 +11,7 @@ open class ActivityParamsTestRule<A : Activity>(
launchActivity: Boolean = true,
private val action: String? = null,
private val params: Bundle? = null
) : IntentsTestRule<A>(activityClass, false, launchActivity) {
) : IntentsTestRule<A>(activityClass, true, launchActivity) {
constructor(activityClass: Class<A>, vararg params: Pair<String, Any?>) : this(
activityClass = activityClass,
@@ -1,7 +1,6 @@
{
"description": "Text with focused text color and focus background",
"platforms": [
"android",
"ios"
],
"div_data": {
@@ -160,6 +160,7 @@
},
{
"type": "slider",
"id": "double_with_ticks",
"width": {
"type": "match_parent"
},
@@ -158,6 +158,7 @@
"width": {
"type": "match_parent"
},
"id": "min_0_max_10",
"accessibility": {
"description": "Min - 0, max - 10"
},
@@ -453,7 +453,7 @@
},
{
"type": "slider_preset",
"id": "slider_default",
"id": "default_slider",
"thumb_value_variable": "first_thumb_value",
"accessibility": {
"description": "Default slider"
@@ -465,7 +465,7 @@
},
{
"type": "slider_preset",
"id": "slider_default",
"id": "min_0_max_10",
"min_value": 0,
"max_value": 10,
"thumb_value_variable": "second_thumb_value",
@@ -525,7 +525,7 @@
},
{
"type": "slider_preset",
"id": "slider_default",
"id": "min_0_max_3",
"min_value": 0,
"max_value": 3,
"thumb_value_variable": "third_thumb_value",
@@ -585,7 +585,7 @@
},
{
"type": "slider_preset",
"id": "slider_default",
"id": "double_with_ticks",
"min_value": 0,
"max_value": 10,
"thumb_value_variable": "fourth_thumb_value",
@@ -665,7 +665,7 @@
},
{
"type": "slider_preset",
"id": "slider_default",
"id": "double_without_ticks",
"thumb_value_variable": "fifth_thumb_value",
"thumb_secondary_value_variable": "fifth_thumb_secondary_value",
"accessibility": {
@@ -61,6 +61,7 @@
"items": [
{
"type": "slider",
"id": "min_0_max_10",
"accessibility": {
"description": "Min - 0, max - 10"
},

Some files were not shown because too many files have changed in this diff Show More