add support of non-modal tooltip mode and 'close_by_tap_outside' property

commit_hash:cffc66e9420e0c2eb81b8d4317433379c0104a7d
This commit is contained in:
gulevsky
2025-02-20 16:33:32 +03:00
parent 15a98c3f01
commit 2d8d69db31
10 changed files with 240 additions and 157 deletions
+1
View File
@@ -1643,6 +1643,7 @@
"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",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/UiTestDivActionHandler.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/UiTestDivActionHandler.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/Utils.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/Utils.kt",
"client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/ViewInteractionExt.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/ViewInteractionExt.kt",
+1
View File
@@ -4,6 +4,7 @@
* Fixed items creation by `item_builder` in `container` with local variables.
* Removed redundant check for API 26.
* Throw errors when switching to ambiguous state.
* Supported non-modal tooltip mode and `close_by_tap_outside` flag.
# iOS Client:
* Add flag `defaultTextAutoEllipsize`. It allows to change default value of `auto_ellipsize` property to `false`.
@@ -11,16 +11,16 @@ import android.transition.TransitionValues
import android.transition.Visibility
import android.view.View
import android.view.ViewGroup
import android.widget.PopupWindow
import androidx.annotation.RequiresApi
import com.yandex.div.core.animation.SpringInterpolator
import com.yandex.div.core.util.SafePopupWindow
import com.yandex.div.core.util.androidInterpolator
import com.yandex.div.core.view2.divs.dpToPxF
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivAnimation
import com.yandex.div2.DivTooltip
internal fun SafePopupWindow.setupAnimation(divTooltip: DivTooltip, resolver: ExpressionResolver) {
internal fun PopupWindow.setupAnimation(divTooltip: DivTooltip, resolver: ExpressionResolver) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val animationIn = divTooltip.animationIn
enterTransition = if (animationIn != null) {
@@ -40,7 +40,7 @@ internal fun SafePopupWindow.setupAnimation(divTooltip: DivTooltip, resolver: Ex
}
}
internal fun SafePopupWindow.clearAnimation() {
internal fun PopupWindow.clearAnimation() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
removeTransition()
} else {
@@ -49,7 +49,7 @@ internal fun SafePopupWindow.clearAnimation() {
}
@RequiresApi(Build.VERSION_CODES.M)
private fun SafePopupWindow.removeTransition() {
private fun PopupWindow.removeTransition() {
enterTransition = null
exitTransition = null
}
@@ -2,30 +2,31 @@ package com.yandex.div.core.tooltip
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.view.MotionEvent
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.view2.divs.drawChildrenShadows
import com.yandex.div.core.widget.DivViewWrapper
import com.yandex.div.internal.Assert
@Mockable
internal class DivTooltipContainer(
internal class DivTooltipContainer @JvmOverloads constructor(
context: Context,
val tooltipView: View,
): RelativeLayout(context) {
private var onWrapperTouchListener: () -> Unit = { }
private var touchDownOnBackground = false
private var isInitialized = false
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
): DivViewWrapper(context, attrs, defStyleAttr) {
init {
addView(tooltipView)
isInitialized = true
clipChildren = false
clipToPadding = false
}
val tooltipView: View?
get() = if (childCount == 0) null else getChildAt(0)
fun updateLocation(x: Int, y: Int, width: Int, height: Int) {
val layoutParams = tooltipView.layoutParams as? LayoutParams ?: return
val tooltipView = this.tooltipView ?: return
val layoutParams = tooltipView.layoutParams as? MarginLayoutParams ?: return
layoutParams.leftMargin = x
layoutParams.topMargin = y
@@ -35,64 +36,16 @@ internal class DivTooltipContainer(
tooltipView.requestLayout()
}
fun setPopupDismissCallback(callback: () -> Unit) {
onWrapperTouchListener = callback
}
override fun dispatchDraw(canvas: Canvas) {
drawChildrenShadows(canvas)
super.dispatchDraw(canvas)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
touchDownOnBackground = isTouchOutsideTooltipView(event)
return super.onTouchEvent(event)
}
MotionEvent.ACTION_UP -> {
if (touchDownOnBackground && isTouchOutsideTooltipView(event)) {
touchDownOnBackground = false
onWrapperTouchListener.invoke()
super.onTouchEvent(event)
return true
} else {
touchDownOnBackground = false
return super.onTouchEvent(event)
}
}
override fun addView(child: View?, index: Int, params: LayoutParams?) {
if (childCount == 1) {
Assert.fail("Adding more than one child view to DivTooltipContainer is not allowed.")
return
}
return super.onTouchEvent(event)
super.addView(child, index, params)
}
private fun isTouchOutsideTooltipView(event: MotionEvent?): Boolean {
event ?: return false
val child = tooltipView
val rect = Rect()
child.getHitRect(rect)
return !rect.contains(event.x.toInt(), event.y.toInt())
}
private fun assertAddingView() =
Assert.fail("Adding children to DivTooltipContainer is not allowed.")
private fun assertRemovingView() =
Assert.fail("Removing children of DivTooltipContainer is not allowed.")
final override fun addView(child: View?) =
if (isInitialized) assertAddingView() else super.addView(child)
final override fun addView(child: View?, index: Int) =
if (isInitialized) assertAddingView() else super.addView(child, index)
final override fun addView(child: View?, index: Int, height: Int) =
if (isInitialized) assertAddingView() else super.addView(child, index, height)
override fun removeView(view: View?) =
if (isInitialized) assertRemovingView() else super.removeView(view)
override fun removeViewAt(index: Int) =
if (isInitialized) assertRemovingView() else super.removeViewAt(index)
}
@@ -1,16 +1,16 @@
package com.yandex.div.core.tooltip
import android.annotation.SuppressLint
import android.graphics.Point
import android.graphics.Rect
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.Window
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
import android.widget.PopupWindow
import androidx.activity.OnBackPressedCallback
import androidx.activity.findViewTreeOnBackPressedDispatcherOwner
import androidx.annotation.VisibleForTesting
@@ -30,12 +30,14 @@ 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
import com.yandex.div.internal.Assert
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.Div
import com.yandex.div2.DivTooltip
import com.yandex.div2.DivTooltipMode
import javax.inject.Inject
@@ -159,18 +161,36 @@ internal class DivTooltipController @VisibleForTesting constructor(
}
val resolver = context.expressionResolver
val div = divTooltip.div
val tooltipContainer = divTooltipViewBuilder.buildTooltipView(div, div2View, context, resolver) ?: return
val displayMetrics = anchor.resources.displayMetrics
val width = divTooltip.div.value().width.toLayoutParamsSize(displayMetrics, resolver)
val height = divTooltip.div.value().height.toLayoutParamsSize(displayMetrics, resolver)
val tooltipContainer = divTooltipViewBuilder.buildTooltipView(context, div, width, height)
val tooltipView = tooltipContainer.tooltipView ?: return
val popup = createPopup(
tooltipContainer,
MATCH_PARENT,
MATCH_PARENT,
width,
height,
).apply {
setDismissOnTouchOutside(tooltipContainer)
setupAnimation(divTooltip, resolver)
isFocusable = true
isTouchable = true
isOutsideTouchable = divTooltip.shouldDismissByOutsideTouch(resolver)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
isFocusable = true
isTouchModal = divTooltip.isModal()
} else {
isFocusable = divTooltip.isModal()
}
setTouchInterceptor(
PopupWindowTouchListener(
this,
tooltipView,
divTooltip.isModal(),
divTooltip.shouldDismissByOutsideTouch(resolver)
)
)
setupAnimation(divTooltip, resolver)
}
val onBackPressedCallback = createOnBackPressCallback(divTooltip, div2View)
val tooltipData = TooltipData(popup, div, null, onBackPressedCallback)
@@ -182,28 +202,29 @@ internal class DivTooltipController @VisibleForTesting constructor(
divVisibilityActionTracker.trackDetachedView(context, tooltipContainer, it)
}
tooltipRestrictor.tooltipShownCallback?.onDivTooltipDismissed(div2View, anchor, divTooltip)
popup.removeBackPressedCallback(tooltipData)
popup.removeBackPressedCallback(tooltipData, accessibilityStateProvider)
}
tooltips[divTooltip.id] = tooltipData
val ticket = divPreloader.preload(div, resolver) { hasFailures ->
if (!hasFailures && !tooltipData.dismissed && anchor.isViewAttachedToWindow()
if (!hasFailures && !tooltipData.dismissed && anchor.isAttachedToWindow
&& tooltipRestrictor.canShowTooltip(div2View, anchor, divTooltip, multiple)) {
tooltipContainer.doOnActualLayout {
val windowFrame = div2View.getWindowFrame()
val location = calcPopupLocation(tooltipView, anchor, divTooltip, windowFrame, resolver)
val finalTooltipWidth = minOf(tooltipView.width, windowFrame.right)
val finalTooltipHeight = minOf(tooltipView.height, windowFrame.bottom)
val location = calcPopupLocation(tooltipView, anchor, divTooltip, resolver)
val tooltipWidth = minOf(tooltipView.width, windowFrame.width())
val tooltipHeight = minOf(tooltipView.height, windowFrame.height())
if (finalTooltipWidth < tooltipView.width) {
if (tooltipWidth < tooltipView.width) {
errorCollectors.getOrCreate(div2View.dataTag, div2View.divData)
.logWarning(Throwable("Tooltip width > screen size, width was changed"))
}
if (finalTooltipHeight < tooltipView.height) {
if (tooltipHeight < tooltipView.height) {
errorCollectors.getOrCreate(div2View.dataTag, div2View.divData)
.logWarning(Throwable("Tooltip height > screen size, height was changed"))
}
tooltipContainer.updateLocation(location.x, location.y, finalTooltipWidth, finalTooltipHeight)
popup.update(location.x, location.y, tooltipWidth, tooltipHeight)
startVisibilityTracking(context, div, tooltipContainer)
tooltipRestrictor.tooltipShownCallback?.onDivTooltipShown(div2View, anchor, divTooltip)
}
@@ -220,11 +241,6 @@ internal class DivTooltipController @VisibleForTesting constructor(
tooltips[divTooltip.id]?.ticket = ticket
}
private fun SafePopupWindow.removeBackPressedCallback(data: TooltipData) {
sendAccessibilityEventUnchecked(TYPE_WINDOW_STATE_CHANGED, contentView, accessibilityStateProvider)
data.onBackPressedCallback?.isEnabled = false
}
private fun createOnBackPressCallback(divTooltip: DivTooltip, divView: Div2View) =
if (accessibilityStateProvider.isAccessibilityEnabled(divView.getContext())) {
object : OnBackPressedCallback(true) {
@@ -266,6 +282,28 @@ private class TooltipData(
var dismissed: Boolean = false,
)
private class PopupWindowTouchListener(
private val popupWindow: PopupWindow,
private val tooltipView: View,
private val isModal: Boolean,
private val shouldDismissByOutsideTouch: Boolean
) : View.OnTouchListener {
private val hitRect = Rect()
override fun onTouch(view: View, event: MotionEvent): Boolean {
tooltipView.getHitRect(hitRect)
return when {
hitRect.contains(event.x.toInt(), event.y.toInt()) -> false
else -> {
if (shouldDismissByOutsideTouch) popupWindow.dismiss()
isModal
}
}
}
}
private fun findChildWithTooltip(tooltipId: String, view: View): Pair<DivTooltip, View>? {
@Suppress("UNCHECKED_CAST")
(view.getTag(R.id.div_tooltips_tag) as? List<DivTooltip>)?.let { tooltips ->
@@ -285,42 +323,28 @@ private fun findChildWithTooltip(tooltipId: String, view: View): Pair<DivTooltip
return null
}
@SuppressLint("ClickableViewAccessibility")
private fun SafePopupWindow.setDismissOnTouchOutside(tooltipContainer: DivTooltipContainer) {
tooltipContainer.setPopupDismissCallback {
dismiss()
}
}
private fun View.isViewAttachedToWindow(): Boolean {
return this.isAttachedToWindow
}
@VisibleForTesting
internal fun calcPopupLocation(popupView: View, anchor: View, divTooltip: DivTooltip,
windowFrame: Rect,resolver: ExpressionResolver): Point {
internal fun calcPopupLocation(
popupView: View,
anchor: View,
divTooltip: DivTooltip,
resolver: ExpressionResolver
): Point {
val locationArray = IntArray(2)
anchor.getLocationInWindow(locationArray)
locationArray[1] -= windowFrame.top
val location = Point(locationArray[0], locationArray[1])
val position = divTooltip.position.evaluate(resolver)
location.x += when (position) {
DivTooltip.Position.LEFT, DivTooltip.Position.TOP_LEFT, DivTooltip.Position.BOTTOM_LEFT ->
-popupView.width
DivTooltip.Position.TOP_RIGHT, DivTooltip.Position.RIGHT, DivTooltip.Position.BOTTOM_RIGHT ->
anchor.width
DivTooltip.Position.TOP, DivTooltip.Position.BOTTOM, DivTooltip.Position.CENTER ->
anchor.width / 2 - popupView.width / 2
DivTooltip.Position.LEFT, DivTooltip.Position.TOP_LEFT, DivTooltip.Position.BOTTOM_LEFT -> -popupView.width
DivTooltip.Position.TOP_RIGHT, DivTooltip.Position.RIGHT, DivTooltip.Position.BOTTOM_RIGHT -> anchor.width
DivTooltip.Position.TOP, DivTooltip.Position.BOTTOM, DivTooltip.Position.CENTER -> (anchor.width - popupView.width) / 2
}
location.y += when (position) {
DivTooltip.Position.TOP_LEFT, DivTooltip.Position.TOP, DivTooltip.Position.TOP_RIGHT ->
-popupView.height
DivTooltip.Position.BOTTOM_LEFT, DivTooltip.Position.BOTTOM, DivTooltip.Position.BOTTOM_RIGHT ->
anchor.height
DivTooltip.Position.LEFT, DivTooltip.Position.RIGHT, DivTooltip.Position.CENTER ->
anchor.height / 2 - popupView.height / 2
DivTooltip.Position.TOP_LEFT, DivTooltip.Position.TOP, DivTooltip.Position.TOP_RIGHT -> -popupView.height
DivTooltip.Position.BOTTOM_LEFT, DivTooltip.Position.BOTTOM, DivTooltip.Position.BOTTOM_RIGHT -> anchor.height
DivTooltip.Position.LEFT, DivTooltip.Position.RIGHT, DivTooltip.Position.CENTER -> (anchor.height - popupView.height) / 2
}
val displayMetrics = anchor.resources.displayMetrics
@@ -331,6 +355,22 @@ internal fun calcPopupLocation(popupView: View, anchor: View, divTooltip: DivToo
return location
}
private fun PopupWindow.removeBackPressedCallback(
data: TooltipData,
accessibilityStateProvider: AccessibilityStateProvider
) {
sendAccessibilityEventUnchecked(TYPE_WINDOW_STATE_CHANGED, contentView, accessibilityStateProvider)
data.onBackPressedCallback?.isEnabled = false
}
private fun DivTooltip.isModal(): Boolean {
return mode is DivTooltipMode.Modal
}
private fun DivTooltip.shouldDismissByOutsideTouch(resolver: ExpressionResolver): Boolean {
return closeByTapOutside.evaluate(resolver)
}
private fun Div2View.getWindowFrame(): Rect {
val windowFrame = Rect()
getWindowVisibleDisplayFrame(windowFrame)
@@ -2,20 +2,14 @@ package com.yandex.div.core.tooltip
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.RelativeLayout.ALIGN_PARENT_LEFT
import android.widget.RelativeLayout.ALIGN_PARENT_TOP
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.RelativeLayout.LayoutParams
import com.yandex.div.core.actions.logError
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.dagger.DivScope
import com.yandex.div.core.state.DivStatePath
import com.yandex.div.core.view2.BindingContext
import com.yandex.div.core.view2.Div2Builder
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.divs.toLayoutParamsSize
import com.yandex.div.internal.Assert
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.Div
import javax.inject.Inject
import javax.inject.Provider
@@ -27,42 +21,30 @@ internal class DivTooltipViewBuilder @Inject constructor(
) {
fun buildTooltipView(
div: Div,
div2View: Div2View,
context: BindingContext,
resolver: ExpressionResolver
): DivTooltipContainer? {
val tooltipView = prepareTooltipView(div, div2View, context, resolver) ?: return null
return DivTooltipContainer(context.divView.getContext(), tooltipView).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
isClickable = true
isFocusable = true
div: Div,
width: Int = WRAP_CONTENT,
height: Int = WRAP_CONTENT,
): DivTooltipContainer {
val tooltipView = prepareTooltipView(context, div)
return DivTooltipContainer(context.divView.getContext()).apply {
addView(tooltipView)
layoutParams = ViewGroup.LayoutParams(width, height)
}
}
private fun prepareTooltipView(
div: Div,
div2View: Div2View,
context: BindingContext,
resolver: ExpressionResolver
): View? {
val tooltipView = div2Builder.get().buildView(div, context, DivStatePath.fromState(0))
if (tooltipView == null) {
Assert.fail("Broken div in popup")
div2View.logError(AssertionError("Broken div in popup!"))
return null
}
div: Div,
): View {
val divBase = div.value()
val tooltipView = div2Builder.get().buildView(div, context, DivStatePath.fromState(0))
val resolver = context.expressionResolver
val displayMetrics = tooltipView.context.resources.displayMetrics
tooltipView.layoutParams = LayoutParams(
divBase.width.toLayoutParamsSize(displayMetrics, resolver),
divBase.height.toLayoutParamsSize(displayMetrics, resolver),
).apply {
addRule(ALIGN_PARENT_LEFT)
addRule(ALIGN_PARENT_TOP)
}
)
tooltipView.isFocusable = true
return tooltipView
}
@@ -28,6 +28,7 @@ import com.yandex.div2.DivTooltip
import com.yandex.div2.DivVisibilityAction
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
@@ -109,7 +110,7 @@ class DivTooltipControllerTest {
}
private val divTooltipViewBuilder = mock<DivTooltipViewBuilder> {
on { buildTooltipView(any(), any(), any(), any()) } doReturn tooltipWrapper
on { buildTooltipView(any(), any(), anyInt(), anyInt()) } doReturn tooltipWrapper
}
private val tooltipShownCallback = mock<DivTooltipRestrictor.DivTooltipShownCallback>()
@@ -270,7 +271,6 @@ class DivTooltipControllerTest {
offset = null,
position = position.asExpression()
),
Rect(),
ExpressionResolver.EMPTY,
)
@@ -2,12 +2,12 @@ package com.yandex.div.steps
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.yandex.div.utils.clickOutside
import com.yandex.div.utils.swipeLeftOutside
import com.yandex.div2.DivTooltip
import com.yandex.test.util.Report.step
import com.yandex.test.util.StepsDsl
@@ -22,11 +22,11 @@ internal class DivTooltipSteps {
}
fun clickOnTooltipWrapper(): Unit = step("Click on tooltip background") {
tooltipBackground().perform(click())
tooltip().perform(clickOutside())
}
fun swipeOnTooltipWrapper(): Unit = step("Swipe on tooltip background") {
tooltipBackground().perform(swipeDown())
tooltip().perform(swipeLeftOutside())
}
fun clickTooltip(): Unit = step("click on tooltip") {
@@ -56,4 +56,3 @@ internal class DivTooltipAssertions {
}
private fun tooltip() = onView(withText("tooltip_text")).inRoot(isPlatformPopup())
private fun tooltipBackground() = onView(withChild(withText("tooltip_text")))
@@ -0,0 +1,104 @@
package com.yandex.div.utils
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.FloatRange
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.Press
import androidx.test.espresso.action.Swipe
import androidx.test.espresso.action.Tap
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import org.hamcrest.Matcher
fun clickOutside(@FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f): ViewAction {
return object : GeneralOutsideAction(bias) {
private val precision = Press.FINGER.describePrecision()
override fun getConstraints(): Matcher<View> = isDisplayed()
override fun getDescription(): String = "click outside of view"
override fun performOutside(uiController: UiController, view: View, location: IntArray) {
val coordinates = floatArrayOf(location[X].toFloat(), location[Y].toFloat())
Tap.SINGLE.sendTap(uiController, coordinates, precision, 0, 0)
}
}
}
fun swipeLeftOutside(
distance: Int = 64,
@FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
): ViewAction {
return object : GeneralOutsideAction(bias) {
private val precision = Press.FINGER.describePrecision()
override fun getConstraints(): Matcher<View> = isDisplayed()
override fun getDescription(): String = "swipe left outside of view"
override fun performOutside(uiController: UiController, view: View, location: IntArray) {
val startCoordinates = floatArrayOf(location[X].toFloat() - distance / 2, location[Y].toFloat())
val endCoordinates = floatArrayOf(location[X].toFloat() + distance / 2, location[Y].toFloat())
Swipe.FAST.sendSwipe(uiController, startCoordinates, endCoordinates, precision)
}
}
}
private const val X = 0
private const val Y = 1
private abstract class GeneralOutsideAction(
@FloatRange(from = 0.0, to = 1.0) private val bias: Float
) : ViewAction {
override fun perform(uiController: UiController, view: View) {
val viewLocation = intArrayOf(0, 0)
view.getLocationInWindow(viewLocation)
view.offsetLocationByWindowLayoutParams(viewLocation)
val viewBounds = Rect(
/* left = */ viewLocation[X],
/* top = */ viewLocation[Y],
/* right = */ viewLocation[X] + view.width,
/* bottom = */ viewLocation[Y] + view.height
)
val windowBounds = Rect()
view.getWindowVisibleDisplayFrame(windowBounds)
val leftSpace = viewBounds.left - windowBounds.left
val topSpace = viewBounds.top - windowBounds.top
val rightSpace = windowBounds.right - viewBounds.right
val bottomSpace = windowBounds.bottom - viewBounds.bottom
val actionLocation = when (intArrayOf(leftSpace, topSpace, rightSpace, bottomSpace).maxOrNull()) {
leftSpace -> intArrayOf(viewBounds.left - (leftSpace * bias).toInt(), viewBounds.centerY())
topSpace -> intArrayOf(viewBounds.centerX(), viewBounds.top - (topSpace * bias).toInt())
rightSpace -> intArrayOf(viewBounds.right + (rightSpace * bias).toInt(), viewBounds.centerY())
bottomSpace -> intArrayOf(viewBounds.centerX(), viewBounds.bottom + (bottomSpace * bias).toInt())
else -> return
}
performOutside(uiController, view, actionLocation)
}
abstract fun performOutside(uiController: UiController, view: View, location: IntArray)
}
private fun View.offsetLocationByWindowLayoutParams(location: IntArray) {
val parentView = parent as? ViewGroup ?: return
val layoutParams = parentView.layoutParams
if (layoutParams is WindowManager.LayoutParams) {
location[X] = location[X] + layoutParams.x
location[Y] = location[Y] + layoutParams.y
return
} else {
parentView.offsetLocationByWindowLayoutParams(location)
}
}
+4 -1
View File
@@ -59,7 +59,9 @@
"$ref": "div-tooltip-mode.json",
"default_value": "{ \"type\": \"modal\" }",
"$description": "translations.json#/div_tooltip_mode",
"platforms": []
"platforms": [
"android"
]
},
"tap_outside_actions": {
"type": "array",
@@ -76,6 +78,7 @@
"$description": "translations.json#/div_tooltip_close_by_tap_outside",
"default_value": "true",
"platforms": [
"android",
"ios"
]
},