Coverage Summary for Class: InAppInsets (cloud.mindbox.mobile_sdk.inapp.presentation.view)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| InAppInsets |
100%
(1/1)
|
|
100%
(4/4)
|
100%
(28/28)
|
| InAppInsets$Companion |
|
| Total |
100%
(1/1)
|
|
100%
(4/4)
|
100%
(28/28)
|
package cloud.mindbox.mobile_sdk.inapp.presentation.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.*
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.*
import cloud.mindbox.mobile_sdk.SnackbarPosition
import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType
import cloud.mindbox.mobile_sdk.isTop
import cloud.mindbox.mobile_sdk.logger.mindboxLogI
import cloud.mindbox.mobile_sdk.px
import kotlin.math.abs
internal class InAppConstraintLayout : ConstraintLayout, BackButtonLayout {
fun setSwipeToDismissCallback(callback: () -> Unit) {
swipeToDismissCallback = callback
}
override fun setBackListener(listener: (() -> Unit)?) {
backButtonHandler = listener?.let { BackButtonHandler(it) }
}
private var swipeToDismissCallback: (() -> Unit)? = null
private var backButtonHandler: BackButtonHandler? = null
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean =
if (keyCode == KeyEvent.KEYCODE_BACK && backButtonHandler != null) {
true
} else {
super.onKeyDown(keyCode, event)
}
override fun dispatchKeyEvent(event: KeyEvent?): Boolean =
if (backButtonHandler?.dispatchKeyEvent(event) == true) {
true
} else {
super.dispatchKeyEvent(event)
}
internal var webViewInsets: InAppInsets = InAppInsets()
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
)
companion object {
private const val ANIM_DURATION = 500L
private const val ANIM_SWIPE_DURATION = 100L
private const val MODAL_WINDOW_MARGIN = 40
}
@SuppressLint("ClickableViewAccessibility")
private fun prepareLayoutForSnackbar(snackBarInAppType: InAppType.Snackbar) {
val statusBarHeight = getStatusBarHeight()
val navigationBarHeight = getNavigationBarHeight()
updateLayoutParams<FrameLayout.LayoutParams> {
when (snackBarInAppType.position.margin.kind) {
InAppType.Snackbar.Position.Margin.MarginKind.DP -> {
when (snackBarInAppType.position.gravity.vertical) {
SnackbarPosition.TOP -> {
gravity = Gravity.TOP
setMargins(
snackBarInAppType.position.margin.left.px,
snackBarInAppType.position.margin.top.px + statusBarHeight,
snackBarInAppType.position.margin.right.px,
0
)
}
SnackbarPosition.BOTTOM -> {
gravity = Gravity.BOTTOM
setMargins(
snackBarInAppType.position.margin.left.px,
0,
snackBarInAppType.position.margin.right.px,
snackBarInAppType.position.margin.bottom.px + navigationBarHeight
)
}
}
}
}
}
var startingY = 0f
doOnLayout { startingY = this.y }
val self: View = this
val gestureDetector =
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
var rightDY = 0f
override fun onDown(e: MotionEvent): Boolean {
rightDY = self.y - e.rawY
self.parent?.requestDisallowInterceptTouchEvent(true)
return true
}
override fun onSingleTapUp(e: MotionEvent): Boolean {
val tapTimeout: Int = ViewConfiguration.getLongPressTimeout()
val clickTime = e.eventTime - e.downTime
if (clickTime <= tapTimeout) {
this@InAppConstraintLayout.mindboxLogI("Click performed with duration = ${clickTime}ms.")
self.performClick()
return true
} else {
this@InAppConstraintLayout.mindboxLogI("Ignore long click with duration = ${clickTime}ms. Timeout = ${tapTimeout}ms.")
return false
}
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
val displacement: Float = if (snackBarInAppType.isTop()) {
minOf(e2.rawY + rightDY, startingY)
} else {
maxOf(e2.rawY + rightDY, startingY)
}
self.y = displacement
return true
}
}).apply {
@Suppress("UsePropertyAccessSyntax")
setIsLongpressEnabled(false)
}
setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(event)
if (event.actionMasked == MotionEvent.ACTION_UP) {
self.parent?.requestDisallowInterceptTouchEvent(false)
if (abs(self.translationY) > (height / 2)) {
swipeToDismissCallback?.invoke()
} else {
self.animate()
.y(startingY)
.setDuration(ANIM_SWIPE_DURATION)
.start()
}
}
true
}
}
@SuppressLint("ClickableViewAccessibility", "InternalInsetResource", "DiscouragedApi")
private fun getNavigationBarHeight(): Int {
var navigationBarHeight = 0
val navBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
if (navBarResourceId > 0) {
navigationBarHeight = resources.getDimensionPixelSize(navBarResourceId)
}
return navigationBarHeight
}
@SuppressLint("ClickableViewAccessibility", "InternalInsetResource", "DiscouragedApi")
private fun getStatusBarHeight(): Int {
var statusBarHeight = 0
val statusBarResourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
if (statusBarResourceId > 0) {
statusBarHeight = resources.getDimensionPixelSize(statusBarResourceId)
}
return statusBarHeight
}
fun slideUp(isReverse: Boolean = false, onAnimationEnd: Runnable = Runnable { }) {
val travelDistance = (height + marginBottom).toFloat()
if (isReverse) {
animate().translationY(travelDistance)
.setDuration(ANIM_DURATION)
.setInterpolator(AccelerateDecelerateInterpolator())
.withEndAction(onAnimationEnd)
.start()
} else {
translationY = travelDistance
isVisible = true
animate().translationY(0f)
.setDuration(ANIM_DURATION)
.setInterpolator(AccelerateDecelerateInterpolator())
.withEndAction(onAnimationEnd)
.start()
}
}
fun slideDown(isReverse: Boolean = false, onAnimationEnd: Runnable = Runnable { }) {
val travelDistance = -(height + marginTop).toFloat()
if (isReverse) {
animate().translationY(travelDistance)
.setDuration(ANIM_DURATION)
.setInterpolator(AccelerateDecelerateInterpolator())
.withEndAction(onAnimationEnd)
.start()
} else {
translationY = travelDistance
isVisible = true
animate().translationY(0f)
.setDuration(ANIM_DURATION)
.setInterpolator(AccelerateDecelerateInterpolator())
.withEndAction(onAnimationEnd)
.start()
}
}
private fun prepareLayoutForWebView() {
updateLayoutParams<MarginLayoutParams> {
setMargins(0, 0, 0, 0)
}
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = Gravity.CENTER
height = FrameLayout.LayoutParams.MATCH_PARENT
}
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInset ->
val inset = windowInset.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
or WindowInsetsCompat.Type.navigationBars()
)
webViewInsets = InAppInsets(
left = inset.left,
top = inset.top,
right = inset.right,
bottom = maxOf(inset.bottom, getNavigationBarHeight())
)
view.updatePadding(
bottom = windowInset.getInsets(
WindowInsetsCompat.Type.ime()
).bottom
)
mindboxLogI("Webview Insets: $inset")
windowInset
}
}
private fun prepareLayoutForModalWindow() {
updateLayoutParams<MarginLayoutParams> {
setMargins(
MODAL_WINDOW_MARGIN.px,
MODAL_WINDOW_MARGIN.px,
MODAL_WINDOW_MARGIN.px,
MODAL_WINDOW_MARGIN.px
)
}
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = Gravity.CENTER
}
}
fun prepareLayoutForInApp(inAppType: InAppType) {
when (inAppType) {
is InAppType.WebView -> prepareLayoutForWebView()
is InAppType.ModalWindow -> prepareLayoutForModalWindow()
is InAppType.Snackbar -> prepareLayoutForSnackbar(inAppType)
}
}
constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int,
defStyleRes: Int,
) : super(
context, attrs, defStyleAttr, defStyleRes
)
}
internal data class InAppInsets(
val left: Int = 0,
val top: Int = 0,
val right: Int = 0,
val bottom: Int = 0
) {
companion object {
const val LEFT = "left"
const val TOP = "top"
const val RIGHT = "right"
const val BOTTOM = "bottom"
}
}
internal fun interface BackButtonLayout {
fun setBackListener(listener: (() -> Unit)?)
}
/**
* Clones the current constraint state, applies [block] to it, then commits the result back.
* Removes the clone/applyTo boilerplate from call sites.
*/
internal fun InAppConstraintLayout.updateConstraints(block: ConstraintSet.() -> Unit) {
ConstraintSet().apply {
clone(this@updateConstraints)
block()
applyTo(this@updateConstraints)
}
}