Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.graphics.Color
import android.os.Build
import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.Window
import android.view.WindowManager
import android.widget.ProgressBar
import android.widget.RelativeLayout
import androidx.activity.ComponentActivity
import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorInt
import androidx.annotation.RequiresApi
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.children
import com.shopify.checkoutkit.ShopifyCheckoutKit.log

Expand All @@ -32,6 +37,7 @@ internal class CheckoutDialog(
) : ComponentDialog(context) {

private var presentedCheckoutWebView: CheckoutWebView? = null
private lateinit var keyboardInsets: CheckoutDialogKeyboardInsets

private val backNavigationCallback = object : OnBackPressedCallback(enabled = true) {
override fun handleOnBackPressed() {
Expand All @@ -46,15 +52,9 @@ internal class CheckoutDialog(

fun start(context: ComponentActivity) {
log.d(LOG_TAG, "Dialog start called.")
window?.configureForCheckoutDialog()
setContentView(R.layout.dialog_checkout)
window?.setLayout(MATCH_PARENT, WRAP_CONTENT)
window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
// Although this flag is deprecated in newest targets, it's properly
// addressing the keyboard focus on the WebView within the dialog.
// The non-deprecated alternative (insets listener) does notify about
// keyboard insets when visible, but it is not adjusting the pan
// properly into the fields. To be investigated further.
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
keyboardInsets = CheckoutDialogKeyboardInsets(findViewById(R.id.checkoutKitRoot))

log.d(LOG_TAG, "Finding or creating WebView.")
val checkoutWebView = CheckoutWebView.checkoutViewFor(checkoutUrl, context)
Expand Down Expand Up @@ -100,6 +100,13 @@ internal class CheckoutDialog(

log.d(LOG_TAG, "Showing dialog.")
show()
// Dialog window size is only applied reliably after show().
window?.setLayout(MATCH_PARENT, MATCH_PARENT)
keyboardInsets.requestApplyInsets()
}

internal fun applyKeyboardInset(imeBottom: Int) {
keyboardInsets.applyKeyboardInset(imeBottom)
}

private fun MenuItem.setupCloseButton(colorScheme: ColorScheme) {
Expand Down Expand Up @@ -140,7 +147,7 @@ internal class CheckoutDialog(
findViewById<RelativeLayout>(R.id.checkoutKitContainer).apply {
log.d(LOG_TAG, "Found parent view, setting its colors and layout params.")
setBackgroundColor(colorScheme.webViewBackgroundColor())
val layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
val layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
layoutParams.addRule(RelativeLayout.BELOW, R.id.progressBar)
checkoutWebView.removeFromParent()
log.d(LOG_TAG, "Adding WebView to parent view.")
Expand Down Expand Up @@ -200,3 +207,80 @@ internal class CheckoutDialog(
private const val LOG_TAG = "CheckoutDialog"
}
}

internal fun Window.configureForCheckoutDialog() {
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
setLayout(MATCH_PARENT, MATCH_PARENT)
WindowCompat.setDecorFitsSystemWindows(this, false)
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
clearFitInsets()
}
}

@RequiresApi(Build.VERSION_CODES.R)
private fun Window.clearFitInsets() {
attributes = attributes.apply {
setFitInsetsTypes(0)
setFitInsetsSides(0)
}
}

internal class CheckoutDialogKeyboardInsets(
private val root: View,
) {
private val rootPaddingTop = root.paddingTop
private val rootPaddingBottom = root.paddingBottom

init {
ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val bottomInset = if (imeBottom > 0) imeBottom else systemBars.bottom
applyInsets(
top = unappliedTopInset(systemBars.top),
bottom = unappliedBottomInset(bottomInset),
)
insets
}
}

fun requestApplyInsets() {
ViewCompat.requestApplyInsets(root)
}

fun applyKeyboardInset(imeBottom: Int) {
applyInsets(top = 0, bottom = imeBottom.coerceAtLeast(0))
}

private fun applyInsets(top: Int, bottom: Int) {
root.setPadding(
root.paddingLeft,
rootPaddingTop + top.coerceAtLeast(0),
root.paddingRight,
rootPaddingBottom + bottom.coerceAtLeast(0),
)
}

private fun unappliedTopInset(inset: Int): Int {
if (inset <= 0 || root.height == 0) return inset
return if (root.locationOnScreenY() >= inset - INSET_APPLIED_TOLERANCE_PX) 0 else inset
}

private fun unappliedBottomInset(inset: Int): Int {
if (inset <= 0 || root.height == 0) return inset
val rootBottom = root.locationOnScreenY() + root.height
val insetTop = root.resources.displayMetrics.heightPixels - inset
return if (rootBottom <= insetTop + INSET_APPLIED_TOLERANCE_PX) 0 else inset
}

private fun View.locationOnScreenY(): Int {
val location = IntArray(2)
getLocationOnScreen(location)
return location[1]
}

private companion object {
private const val INSET_APPLIED_TOLERANCE_PX = 2
}
}
12 changes: 7 additions & 5 deletions platforms/android/lib/src/main/res/layout/dialog_checkout.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/checkoutKitRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
android:background="@android:color/transparent"
android:orientation="vertical">

<androidx.appcompat.widget.Toolbar
android:id="@+id/checkoutKitHeader"
Expand All @@ -18,9 +20,9 @@
<RelativeLayout
android:id="@+id/checkoutKitContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:layout_below="@id/checkoutKitHeader"
android:visibility="visible">

<ProgressBar
Expand All @@ -32,4 +34,4 @@
android:indeterminate="false" />

</RelativeLayout>
</RelativeLayout>
</LinearLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import android.app.Dialog
import android.graphics.drawable.ColorDrawable
import android.os.Looper
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.WindowManager
import android.webkit.WebView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.activity.ComponentActivity
import androidx.appcompat.widget.Toolbar
Expand Down Expand Up @@ -63,6 +66,24 @@ class CheckoutDialogTest {
assertThat(dialog.isShowing).isTrue
}

@Test
fun `dialog fills height and handles keyboard with insets`() {
ShopifyCheckoutKit.present("https://shopify.com", activity, processor)

val dialog = ShadowDialog.getLatestDialog()
val attributes = dialog.window?.attributes
val root = dialog.findViewById<View>(R.id.checkoutKitRoot)
val container = dialog.findViewById<RelativeLayout>(R.id.checkoutKitContainer)
val containerLayoutParams = container.layoutParams as LinearLayout.LayoutParams

assertThat(root).isInstanceOf(LinearLayout::class.java)
assertThat(attributes?.height).isEqualTo(MATCH_PARENT)
assertThat(attributes?.softInputMode?.and(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING))
.isEqualTo(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
assertThat(containerLayoutParams.height).isZero()
assertThat(containerLayoutParams.weight).isEqualTo(1f)
}

@Test
fun `checkoutView is added to the container when dialog is presented`() {
ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
Expand All @@ -83,8 +104,26 @@ class CheckoutDialogTest {
val webView: WebView = ShadowDialog.getLatestDialog()
.findViewById<RelativeLayout>(R.id.checkoutKitContainer)
.children.firstOrNull { it is WebView } as WebView? ?: fail("No WebVew found in dialog")
val layoutParams = webView.layoutParams as RelativeLayout.LayoutParams

assertThat(shadowOf(webView).wasOnResumeCalled()).isTrue()
assertThat(layoutParams.width).isEqualTo(MATCH_PARENT)
assertThat(layoutParams.height).isEqualTo(MATCH_PARENT)
}

@Test
fun `ime inset changes pad checkout content without resizing window`() {
ShopifyCheckoutKit.present("https://shopify.com", activity, processor)
val dialog = ShadowDialog.getLatestDialog() as CheckoutDialog
val root = dialog.findViewById<View>(R.id.checkoutKitRoot)
dialog.window?.setLayout(MATCH_PARENT, 400)

assertThat(root.paddingBottom).isZero()

dialog.applyKeyboardInset(100)

assertThat(dialog.window?.attributes?.height).isEqualTo(400)
assertThat(root.paddingBottom).isEqualTo(100)
}

@Test
Expand Down Expand Up @@ -238,7 +277,7 @@ class CheckoutDialogTest {
}

@Test
fun `sets WebView container background color based on current configuration`() {
fun `sets checkout content background color based on current configuration`() {
val customColors = customColors()
ShopifyCheckoutKit.configuration.colorScheme = ColorScheme.Web(customColors)

Expand Down
Loading
Loading