mirror of
https://github.com/divkit/divkit.git
synced 2026-06-06 20:07:59 +00:00
add support of non-modal tooltip mode and 'close_by_tap_outside' property
commit_hash:cffc66e9420e0c2eb81b8d4317433379c0104a7d
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+18
-65
@@ -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)
|
||||
}
|
||||
|
||||
+88
-48
@@ -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)
|
||||
|
||||
+14
-32
@@ -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
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
+4
-5
@@ -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")))
|
||||
|
||||
+104
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user