parent
a8fd37740d
commit
57c7955637
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue