diff --git a/.mapping.json b/.mapping.json index d57419fa6..7478cf014 100644 --- a/.mapping.json +++ b/.mapping.json @@ -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", diff --git a/CHANGELOG.md b/CHANGELOG.md index e60c04d74..8f206253a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipAnimation.kt b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipAnimation.kt index 0a393f8a1..27648f0f6 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipAnimation.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipAnimation.kt @@ -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 } diff --git a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipContainer.kt b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipContainer.kt index fedda9546..15d3df4f9 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipContainer.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipContainer.kt @@ -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) } diff --git a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipController.kt b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipController.kt index cc7242d6b..c3fa940fb 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipController.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipController.kt @@ -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? { @Suppress("UNCHECKED_CAST") (view.getTag(R.id.div_tooltips_tag) as? List)?.let { tooltips -> @@ -285,42 +323,28 @@ private fun findChildWithTooltip(tooltipId: String, view: View): Pair - -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) diff --git a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipViewBuilder.kt b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipViewBuilder.kt index 15d33b405..59da4294d 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipViewBuilder.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/tooltip/DivTooltipViewBuilder.kt @@ -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 } diff --git a/client/android/div/src/test/java/com/yandex/div/core/tooltip/DivTooltipControllerTest.kt b/client/android/div/src/test/java/com/yandex/div/core/tooltip/DivTooltipControllerTest.kt index 037c49ba1..fc417edfc 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/tooltip/DivTooltipControllerTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/tooltip/DivTooltipControllerTest.kt @@ -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 { - on { buildTooltipView(any(), any(), any(), any()) } doReturn tooltipWrapper + on { buildTooltipView(any(), any(), anyInt(), anyInt()) } doReturn tooltipWrapper } private val tooltipShownCallback = mock() @@ -270,7 +271,6 @@ class DivTooltipControllerTest { offset = null, position = position.asExpression() ), - Rect(), ExpressionResolver.EMPTY, ) diff --git a/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivTooltipSteps.kt b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivTooltipSteps.kt index d4b9149a7..f147d667a 100644 --- a/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivTooltipSteps.kt +++ b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivTooltipSteps.kt @@ -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"))) diff --git a/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/OutsideActions.kt b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/OutsideActions.kt new file mode 100644 index 000000000..e0c9b3c33 --- /dev/null +++ b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/utils/OutsideActions.kt @@ -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 = 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 = 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) + } +} diff --git a/schema/div-tooltip.json b/schema/div-tooltip.json index 99ce81ad4..6ec151df9 100644 --- a/schema/div-tooltip.json +++ b/schema/div-tooltip.json @@ -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" ] },