parent
a8fd37740d
commit
57c7955637
|
@ -9,7 +9,6 @@ import android.animation.AnimatorListenerAdapter
|
|||
import android.app.Activity
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
|
@ -17,7 +16,6 @@ import androidx.annotation.Dimension
|
|||
import androidx.annotation.Dimension.DP
|
||||
import androidx.core.graphics.contains
|
||||
import androidx.core.graphics.toPoint
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||
import androidx.dynamicanimation.animation.FlingAnimation
|
||||
|
@ -25,6 +23,8 @@ import mozilla.components.browser.session.Session
|
|||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
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.settings
|
||||
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
|
||||
* necessary animations.
|
||||
*/
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
class ToolbarGestureHandler(
|
||||
private val activity: Activity,
|
||||
private val contentLayout: View,
|
||||
|
@ -56,18 +55,6 @@ class ToolbarGestureHandler(
|
|||
private val windowWidth: Int
|
||||
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 touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
|
||||
|
@ -89,7 +76,12 @@ class ToolbarGestureHandler(
|
|||
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())
|
||||
true
|
||||
} else {
|
||||
|
@ -313,7 +305,7 @@ class ToolbarGestureHandler(
|
|||
val toolbarLocation = toolbarLayout.getRectWithViewLocation()
|
||||
// In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so
|
||||
// lets make our swipe area taller by that amount
|
||||
windowInsets?.let { insets ->
|
||||
activity.window.decorView.getWindowInsets()?.let { insets ->
|
||||
if (activity.settings().shouldUseBottomToolbar) {
|
||||
toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,12 @@
|
|||
package org.mozilla.fenix.ext
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.view.TouchDelegate
|
||||
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
|
||||
|
||||
fun View.increaseTapArea(extraDps: Int) {
|
||||
|
@ -26,3 +30,61 @@ fun View.removeTouchDelegate() {
|
|||
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
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
package org.mozilla.fenix.ext
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
|
@ -20,7 +24,11 @@ import mozilla.components.support.ktx.android.util.dpToPx
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
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 {
|
||||
|
||||
@MockK private lateinit var view: View
|
||||
|
@ -31,6 +39,7 @@ class ViewTest {
|
|||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
|
||||
mockkStatic("org.mozilla.fenix.ext.ViewKt")
|
||||
|
||||
every { view.resources.displayMetrics } returns displayMetrics
|
||||
every { view.parent } returns parent
|
||||
|
@ -66,4 +75,77 @@ class ViewTest {
|
|||
view.removeTouchDelegate()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue