1
0
Fork 0

For #12865, #12990 - Disable swipe to switch tabs gesture when the keyboard is visible.

master
Kainalu Hagiwara 2020-07-28 11:25:17 -07:00
parent a8fd37740d
commit 57c7955637
3 changed files with 153 additions and 17 deletions

View File

@ -9,7 +9,6 @@ import android.animation.AnimatorListenerAdapter
import android.app.Activity import android.app.Activity
import android.graphics.PointF import android.graphics.PointF
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
@ -17,7 +16,6 @@ import androidx.annotation.Dimension
import androidx.annotation.Dimension.DP import androidx.annotation.Dimension.DP
import androidx.core.graphics.contains import androidx.core.graphics.contains
import androidx.core.graphics.toPoint import androidx.core.graphics.toPoint
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FlingAnimation import androidx.dynamicanimation.animation.FlingAnimation
@ -25,6 +23,8 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
import mozilla.components.support.ktx.android.view.getRectWithViewLocation import mozilla.components.support.ktx.android.view.getRectWithViewLocation
import org.mozilla.fenix.ext.getWindowInsets
import org.mozilla.fenix.ext.isKeyboardVisible
import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import kotlin.math.abs import kotlin.math.abs
@ -35,7 +35,6 @@ import kotlin.math.min
* Handles intercepting touch events on the toolbar for swipe gestures and executes the * Handles intercepting touch events on the toolbar for swipe gestures and executes the
* necessary animations. * necessary animations.
*/ */
@Suppress("LargeClass", "TooManyFunctions")
class ToolbarGestureHandler( class ToolbarGestureHandler(
private val activity: Activity, private val activity: Activity,
private val contentLayout: View, private val contentLayout: View,
@ -56,18 +55,6 @@ class ToolbarGestureHandler(
private val windowWidth: Int private val windowWidth: Int
get() = activity.resources.displayMetrics.widthPixels get() = activity.resources.displayMetrics.widthPixels
private val windowInsets: WindowInsetsCompat?
get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// In theory, the rootWindowInsets should exist at this point but if the decorView is
// not attached for some reason we'll get a NullPointerException without the check.
activity.window.decorView.rootWindowInsets?.let {
WindowInsetsCompat.toWindowInsetsCompat(it)
}
} else {
null
}
private val previewOffset = PREVIEW_OFFSET.dpToPx(activity.resources.displayMetrics) private val previewOffset = PREVIEW_OFFSET.dpToPx(activity.resources.displayMetrics)
private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
@ -89,7 +76,12 @@ class ToolbarGestureHandler(
GestureDirection.LEFT_TO_RIGHT GestureDirection.LEFT_TO_RIGHT
} }
return if (start.isInToolbar() && abs(dx) > touchSlop && abs(dy) < abs(dx)) { return if (
!activity.window.decorView.isKeyboardVisible() &&
start.isInToolbar() &&
abs(dx) > touchSlop &&
abs(dy) < abs(dx)
) {
preparePreview(getDestination()) preparePreview(getDestination())
true true
} else { } else {
@ -313,7 +305,7 @@ class ToolbarGestureHandler(
val toolbarLocation = toolbarLayout.getRectWithViewLocation() val toolbarLocation = toolbarLayout.getRectWithViewLocation()
// In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so // In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so
// lets make our swipe area taller by that amount // lets make our swipe area taller by that amount
windowInsets?.let { insets -> activity.window.decorView.getWindowInsets()?.let { insets ->
if (activity.settings().shouldUseBottomToolbar) { if (activity.settings().shouldUseBottomToolbar) {
toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom) toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom)
} }

View File

@ -5,8 +5,12 @@
package org.mozilla.fenix.ext package org.mozilla.fenix.ext
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.view.TouchDelegate import android.view.TouchDelegate
import android.view.View import android.view.View
import androidx.annotation.Dimension
import androidx.annotation.VisibleForTesting
import androidx.core.view.WindowInsetsCompat
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
fun View.increaseTapArea(extraDps: Int) { fun View.increaseTapArea(extraDps: Int) {
@ -26,3 +30,61 @@ fun View.removeTouchDelegate() {
parent.touchDelegate = null parent.touchDelegate = null
} }
} }
/**
* A safer version of [ViewCompat.getRootWindowInsets] that does not throw a NullPointerException
* if the view is not attached.
*/
fun View.getWindowInsets(): WindowInsetsCompat? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rootWindowInsets?.let {
WindowInsetsCompat.toWindowInsetsCompat(it)
}
} else {
null
}
}
/**
* Checks if the keyboard is visible
*
* Inspired by https://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
* API 30 adds a native method for this. We should use it (and a compat method if one
* is added) when it becomes available
*/
fun View.isKeyboardVisible(): Boolean {
// Since we have insets in M and above, we don't need to guess what the keyboard height is.
// Otherwise, we make a guess at the minimum height of the keyboard to account for the
// navigation bar.
val minimumKeyboardHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
0
} else {
MINIMUM_KEYBOARD_HEIGHT.dpToPx(resources.displayMetrics)
}
return getKeyboardHeight() > minimumKeyboardHeight
}
@VisibleForTesting
internal fun View.getWindowVisibleDisplayFrame(): Rect = with(Rect()) {
getWindowVisibleDisplayFrame(this)
this
}
@VisibleForTesting
internal fun View.getKeyboardHeight(): Int {
val windowRect = getWindowVisibleDisplayFrame()
val statusBarHeight = windowRect.top
var keyboardHeight = rootView.height - (windowRect.height() + statusBarHeight)
getWindowInsets()?.let {
keyboardHeight -= it.stableInsetBottom
}
return keyboardHeight
}
/**
* The assumed minimum height of the keyboard.
*/
@VisibleForTesting
@Dimension(unit = Dimension.DP)
internal const val MINIMUM_KEYBOARD_HEIGHT = 100

View File

@ -5,14 +5,18 @@
package org.mozilla.fenix.ext package org.mozilla.fenix.ext
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.WindowInsets
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.WindowInsetsCompat
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.Runs import io.mockk.Runs
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.just import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.slot import io.mockk.slot
import io.mockk.verify import io.mockk.verify
@ -20,7 +24,11 @@ import mozilla.components.support.ktx.android.util.dpToPx
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(FenixRobolectricTestRunner::class)
class ViewTest { class ViewTest {
@MockK private lateinit var view: View @MockK private lateinit var view: View
@ -31,6 +39,7 @@ class ViewTest {
fun setup() { fun setup() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt") mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
mockkStatic("org.mozilla.fenix.ext.ViewKt")
every { view.resources.displayMetrics } returns displayMetrics every { view.resources.displayMetrics } returns displayMetrics
every { view.parent } returns parent every { view.parent } returns parent
@ -66,4 +75,77 @@ class ViewTest {
view.removeTouchDelegate() view.removeTouchDelegate()
verify { parent.touchDelegate = null } verify { parent.touchDelegate = null }
} }
@Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1])
@Test
fun `getWindowInsets returns null below API 23`() {
assertEquals(null, view.getWindowInsets())
}
@Test
fun `getWindowInsets returns null when the system insets don't exist`() {
every { view.rootWindowInsets } returns null
assertEquals(null, view.getWindowInsets())
}
@Test
fun `getWindowInsets returns the compat insets when the system insets exist`() {
val rootInsets: WindowInsets = mockk(relaxed = true)
every { view.rootWindowInsets } returns rootInsets
assertEquals(WindowInsetsCompat.toWindowInsetsCompat(rootInsets), view.getWindowInsets())
}
@Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1])
@Test
fun `getKeyboardHeight accounts for status bar below API 23`() {
every { view.getWindowVisibleDisplayFrame() } returns Rect(0, 50, 1000, 500)
every { view.rootView.height } returns 1000
assertEquals(500, view.getKeyboardHeight())
}
@Test
fun `getKeyboardHeight accounts for status bar and navigation bar`() {
every { view.getWindowVisibleDisplayFrame() } returns Rect(0, 50, 1000, 500)
every { view.rootView.height } returns 1000
every { view.getWindowInsets() } returns mockk(relaxed = true) {
every { stableInsetBottom } returns 50
}
assertEquals(450, view.getKeyboardHeight())
}
@Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1])
@Test
fun `isKeyboardVisible returns false when the keyboard height is less than or equal to the minimum threshold`() {
val threshold = MINIMUM_KEYBOARD_HEIGHT.dpToPx(displayMetrics)
every { view.getKeyboardHeight() } returns threshold - 1
assertEquals(false, view.isKeyboardVisible())
every { view.getKeyboardHeight() } returns threshold
assertEquals(false, view.isKeyboardVisible())
}
@Config(sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1])
@Test
fun `isKeyboardVisible returns true when the keyboard height is greater than the minimum threshold`() {
val threshold = MINIMUM_KEYBOARD_HEIGHT.dpToPx(displayMetrics)
every { view.getKeyboardHeight() } returns threshold + 1
assertEquals(true, view.isKeyboardVisible())
}
@Test
fun `isKeyboardVisible returns false when the keyboard height is 0`() {
every { view.getKeyboardHeight() } returns 0
assertEquals(false, view.isKeyboardVisible())
}
@Test
fun `isKeyboardVisible returns true when the keyboard height is greater than 0`() {
every { view.getKeyboardHeight() } returns 100
assertEquals(true, view.isKeyboardVisible())
}
} }