mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-17 15:26:23 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/3481 - Implement swipe on toolbar to switch tabs.
This commit is contained in:
parent
d85e95f6b8
commit
a6a8c4dc08
@ -503,6 +503,7 @@ dependencies {
|
||||
implementation Deps.androidx_lifecycle_viewmodel
|
||||
implementation Deps.androidx_core
|
||||
implementation Deps.androidx_core_ktx
|
||||
implementation Deps.androidx_dynamic_animation
|
||||
implementation Deps.androidx_transition
|
||||
implementation Deps.androidx_work_ktx
|
||||
implementation Deps.google_material
|
||||
|
@ -24,4 +24,9 @@ object FeatureFlags {
|
||||
* Enables new tab tray pref
|
||||
*/
|
||||
val tabTray = Config.channel.isNightlyOrDebug
|
||||
|
||||
/**
|
||||
* Enables swipe on toolbar to switch tabs
|
||||
*/
|
||||
val swipeToSwitchTabs = Config.channel.isNightlyOrDebug
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.fragment_browser.*
|
||||
import kotlinx.android.synthetic.main.fragment_browser.view.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.session.Session
|
||||
@ -29,6 +30,7 @@ import mozilla.components.feature.tab.collections.TabCollection
|
||||
import mozilla.components.feature.tabs.WindowFeature
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import org.mozilla.fenix.FeatureFlags
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.addons.runIfFragmentIsAttached
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
@ -66,11 +68,24 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||
return view
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun initializeUI(view: View): Session? {
|
||||
val context = requireContext()
|
||||
val components = context.components
|
||||
|
||||
return super.initializeUI(view)?.also {
|
||||
if (FeatureFlags.swipeToSwitchTabs) {
|
||||
gestureLayout.addGestureListener(
|
||||
ToolbarGestureHandler(
|
||||
activity = requireActivity(),
|
||||
contentLayout = browserLayout,
|
||||
tabPreview = tabPreview,
|
||||
toolbarLayout = browserToolbarView.view,
|
||||
sessionManager = components.core.sessionManager
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val readerModeAction =
|
||||
BrowserToolbar.ToggleButton(
|
||||
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
|
||||
|
@ -0,0 +1,136 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.browser
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
|
||||
/**
|
||||
* Interface that allows intercepting and handling swipe gestures received in a [SwipeGestureLayout].
|
||||
*/
|
||||
interface SwipeGestureListener {
|
||||
|
||||
/**
|
||||
* Called when the [SwipeGestureLayout] detects the start of a swipe gesture. The listener
|
||||
* should return true if it wants to handle the swipe gesture. If the listener returns false
|
||||
* it will not receive any callbacks for future events that the swipe produces.
|
||||
*
|
||||
* @param start the initial point where the gesture started
|
||||
* @param next the next point in the gesture
|
||||
*/
|
||||
fun onSwipeStarted(start: PointF, next: PointF): Boolean
|
||||
|
||||
/**
|
||||
* Called when the swipe gesture receives a new event.
|
||||
*
|
||||
* @param distanceX the change along the x-axis since the last swipe update
|
||||
* @param distanceY the change along the y-axis since the last swipe update
|
||||
*/
|
||||
fun onSwipeUpdate(distanceX: Float, distanceY: Float)
|
||||
|
||||
/**
|
||||
* Called when the user finishes the swipe gesture (ie lifts their finger off the screen)
|
||||
*
|
||||
* @param velocityX the velocity of the swipe along the x-axis
|
||||
* @param velocityY the velocity of the swipe along the y-axis
|
||||
*/
|
||||
fun onSwipeFinished(velocityX: Float, velocityY: Float)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [FrameLayout] that allows listeners to intercept and handle swipe events.
|
||||
*
|
||||
* Listeners are called in the order they are added and the first listener to intercept a swipe event
|
||||
* is the only listener that will receive events for the duration of that swipe.
|
||||
*/
|
||||
class SwipeGestureLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDown(e: MotionEvent?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
val start = e1?.let { event -> PointF(event.rawX, event.rawY) } ?: return false
|
||||
val next = e2?.let { event -> PointF(event.rawX, event.rawY) } ?: return false
|
||||
|
||||
if (activeListener == null && !handledInitialScroll) {
|
||||
activeListener = listeners.firstOrNull { listener ->
|
||||
listener.onSwipeStarted(start, next)
|
||||
}
|
||||
handledInitialScroll = true
|
||||
}
|
||||
activeListener?.onSwipeUpdate(distanceX, distanceY)
|
||||
return activeListener != null
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
activeListener?.onSwipeFinished(velocityX, velocityY)
|
||||
return if (activeListener != null) {
|
||||
activeListener = null
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val gestureDetector = GestureDetectorCompat(context, gestureListener)
|
||||
|
||||
private val listeners = mutableListOf<SwipeGestureListener>()
|
||||
private var activeListener: SwipeGestureListener? = null
|
||||
private var handledInitialScroll = false
|
||||
|
||||
fun addGestureListener(listener: SwipeGestureListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
|
||||
return when (event?.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
handledInitialScroll = false
|
||||
gestureDetector.onTouchEvent(event)
|
||||
false
|
||||
}
|
||||
else -> gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
return when (event?.actionMasked) {
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
||||
gestureDetector.onTouchEvent(event)
|
||||
// If the active listener is not null here, then we haven't detected a fling
|
||||
// so notify the listener that the swipe was finished with 0 velocity
|
||||
activeListener?.onSwipeFinished(
|
||||
velocityX = 0f,
|
||||
velocityY = 0f
|
||||
)
|
||||
activeListener = null
|
||||
false
|
||||
}
|
||||
else -> gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
67
app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt
Normal file
67
app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt
Normal file
@ -0,0 +1,67 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.browser
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import kotlinx.android.synthetic.main.tab_preview.view.*
|
||||
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
||||
import mozilla.components.support.images.ext.loadIntoView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
|
||||
class TabPreview @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyle) {
|
||||
|
||||
private val thumbnailLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
|
||||
|
||||
init {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
inflater.inflate(R.layout.tab_preview, this, true)
|
||||
|
||||
if (!context.settings().shouldUseBottomToolbar) {
|
||||
fakeToolbar.updateLayoutParams<LayoutParams> {
|
||||
gravity = Gravity.TOP
|
||||
}
|
||||
|
||||
fakeToolbar.background = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, context),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
menuButton.setColorFilter(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
ThemeManager.resolveAttribute(R.attr.primaryText, context)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
previewThumbnail.translationY = if (!context.settings().shouldUseBottomToolbar) {
|
||||
fakeToolbar.height.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPreviewThumbnail(thumbnailId: String) {
|
||||
thumbnailLoader.loadIntoView(previewThumbnail, thumbnailId)
|
||||
}
|
||||
}
|
@ -0,0 +1,356 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.browser
|
||||
|
||||
import android.animation.Animator
|
||||
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
|
||||
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
|
||||
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.sessionsOfType
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
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,
|
||||
private val tabPreview: TabPreview,
|
||||
private val toolbarLayout: View,
|
||||
private val sessionManager: SessionManager
|
||||
) : SwipeGestureListener {
|
||||
|
||||
private enum class GestureDirection {
|
||||
LEFT_TO_RIGHT, RIGHT_TO_LEFT
|
||||
}
|
||||
|
||||
private sealed class Destination {
|
||||
data class Tab(val session: Session) : Destination()
|
||||
object None : Destination()
|
||||
}
|
||||
|
||||
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
|
||||
private val minimumFlingVelocity = ViewConfiguration.get(activity).scaledMinimumFlingVelocity
|
||||
private val defaultVelocity = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
MINIMUM_ANIMATION_VELOCITY,
|
||||
activity.resources.displayMetrics
|
||||
)
|
||||
|
||||
private var gestureDirection = GestureDirection.LEFT_TO_RIGHT
|
||||
|
||||
override fun onSwipeStarted(start: PointF, next: PointF): Boolean {
|
||||
val dx = next.x - start.x
|
||||
val dy = next.y - start.y
|
||||
gestureDirection = if (dx < 0) {
|
||||
GestureDirection.RIGHT_TO_LEFT
|
||||
} else {
|
||||
GestureDirection.LEFT_TO_RIGHT
|
||||
}
|
||||
|
||||
return if (start.isInToolbar() && abs(dx) > touchSlop && abs(dy) < abs(dx)) {
|
||||
preparePreview(getDestination())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwipeUpdate(distanceX: Float, distanceY: Float) {
|
||||
when (getDestination()) {
|
||||
is Destination.Tab -> {
|
||||
// Restrict the range of motion for the views so you can't start a swipe in one direction
|
||||
// then move your finger far enough in the other direction and make the content visually
|
||||
// start sliding off screen the other way.
|
||||
tabPreview.translationX = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> min(
|
||||
windowWidth.toFloat() + previewOffset,
|
||||
tabPreview.translationX - distanceX
|
||||
)
|
||||
GestureDirection.LEFT_TO_RIGHT -> max(
|
||||
-windowWidth.toFloat() - previewOffset,
|
||||
tabPreview.translationX - distanceX
|
||||
)
|
||||
}
|
||||
contentLayout.translationX = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> min(
|
||||
0f,
|
||||
contentLayout.translationX - distanceX
|
||||
)
|
||||
GestureDirection.LEFT_TO_RIGHT -> max(
|
||||
0f,
|
||||
contentLayout.translationX - distanceX
|
||||
)
|
||||
}
|
||||
}
|
||||
is Destination.None -> {
|
||||
// If there is no "next" tab to swipe to in the gesture direction, only do a
|
||||
// partial animation to show that we are at the end of the tab list
|
||||
val maxContentHidden = contentLayout.width * OVERSCROLL_HIDE_PERCENT
|
||||
contentLayout.translationX = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> max(
|
||||
-maxContentHidden.toFloat(),
|
||||
contentLayout.translationX - distanceX
|
||||
).coerceAtMost(0f)
|
||||
GestureDirection.LEFT_TO_RIGHT -> min(
|
||||
maxContentHidden.toFloat(),
|
||||
contentLayout.translationX - distanceX
|
||||
).coerceAtLeast(0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwipeFinished(
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
) {
|
||||
val destination = getDestination()
|
||||
if (destination is Destination.Tab && isGestureComplete(velocityX)) {
|
||||
animateToNextTab(velocityX, destination.session)
|
||||
} else {
|
||||
animateCanceledGesture(velocityX)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFlingAnimation(
|
||||
view: View,
|
||||
minValue: Float,
|
||||
maxValue: Float,
|
||||
startVelocity: Float
|
||||
): FlingAnimation =
|
||||
FlingAnimation(view, DynamicAnimation.TRANSLATION_X).apply {
|
||||
setMinValue(minValue)
|
||||
setMaxValue(maxValue)
|
||||
setStartVelocity(startVelocity)
|
||||
friction = ViewConfiguration.getScrollFriction()
|
||||
}
|
||||
|
||||
private fun getDestination(): Destination {
|
||||
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
|
||||
val currentSession = sessionManager.selectedSession ?: return Destination.None
|
||||
val currentIndex = sessionManager.sessionsOfType(currentSession.private).indexOfFirst {
|
||||
it.id == currentSession.id
|
||||
}
|
||||
|
||||
return if (currentIndex == -1) {
|
||||
Destination.None
|
||||
} else {
|
||||
val sessions = sessionManager.sessionsOfType(currentSession.private)
|
||||
val index = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> if (isLtr) {
|
||||
currentIndex + 1
|
||||
} else {
|
||||
currentIndex - 1
|
||||
}
|
||||
GestureDirection.LEFT_TO_RIGHT -> if (isLtr) {
|
||||
currentIndex - 1
|
||||
} else {
|
||||
currentIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (index < sessions.count() && index >= 0) {
|
||||
Destination.Tab(sessions.elementAt(index))
|
||||
} else {
|
||||
Destination.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun preparePreview(destination: Destination) {
|
||||
val thumbnailId = when (destination) {
|
||||
is Destination.Tab -> destination.session.id
|
||||
is Destination.None -> return
|
||||
}
|
||||
|
||||
tabPreview.loadPreviewThumbnail(thumbnailId)
|
||||
tabPreview.alpha = 1f
|
||||
tabPreview.translationX = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> windowWidth.toFloat() + previewOffset
|
||||
GestureDirection.LEFT_TO_RIGHT -> -windowWidth.toFloat() - previewOffset
|
||||
}
|
||||
tabPreview.isVisible = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the gesture is complete based on the position of tab preview and the velocity of
|
||||
* the gesture. A completed gesture means the user has indicated they want to swipe to the next
|
||||
* tab. The gesture is considered complete if one of the following is true:
|
||||
*
|
||||
* 1. The user initiated a fling in the same direction as the initial movement
|
||||
* 2. There is no fling initiated, but the percentage of the tab preview shown is at least
|
||||
* [GESTURE_FINISH_PERCENT]
|
||||
*
|
||||
* If the user initiated a fling in the opposite direction of the initial movement, the
|
||||
* gesture is always considered incomplete.
|
||||
*/
|
||||
private fun isGestureComplete(velocityX: Float): Boolean {
|
||||
val previewWidth = tabPreview.getRectWithViewLocation().visibleWidth.toDouble()
|
||||
val velocityMatchesDirection = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> velocityX <= 0
|
||||
GestureDirection.LEFT_TO_RIGHT -> velocityX >= 0
|
||||
}
|
||||
val reverseFling =
|
||||
abs(velocityX) >= minimumFlingVelocity && !velocityMatchesDirection
|
||||
|
||||
return !reverseFling && (previewWidth / windowWidth >= GESTURE_FINISH_PERCENT ||
|
||||
abs(velocityX) >= minimumFlingVelocity)
|
||||
}
|
||||
|
||||
private fun getVelocityFromFling(velocityX: Float): Float {
|
||||
return max(abs(velocityX), defaultVelocity)
|
||||
}
|
||||
|
||||
private fun animateToNextTab(velocityX: Float, session: Session) {
|
||||
val browserFinalXCoordinate: Float = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset
|
||||
GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset
|
||||
}
|
||||
val animationVelocity = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> -getVelocityFromFling(velocityX)
|
||||
GestureDirection.LEFT_TO_RIGHT -> getVelocityFromFling(velocityX)
|
||||
}
|
||||
|
||||
// Finish animating the contentLayout off screen and tabPreview on screen
|
||||
createFlingAnimation(
|
||||
view = contentLayout,
|
||||
minValue = min(0f, browserFinalXCoordinate),
|
||||
maxValue = max(0f, browserFinalXCoordinate),
|
||||
startVelocity = animationVelocity
|
||||
).addUpdateListener { _, value, _ ->
|
||||
tabPreview.translationX = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
|
||||
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
|
||||
}
|
||||
}.addEndListener { _, _, _, _ ->
|
||||
contentLayout.translationX = 0f
|
||||
sessionManager.select(session)
|
||||
|
||||
// Fade out the tab preview to prevent flickering
|
||||
val shortAnimationDuration =
|
||||
activity.resources.getInteger(android.R.integer.config_shortAnimTime)
|
||||
tabPreview.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(shortAnimationDuration.toLong())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
tabPreview.isVisible = false
|
||||
}
|
||||
})
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun animateCanceledGesture(gestureVelocity: Float) {
|
||||
val velocity = if (getDestination() is Destination.None) {
|
||||
defaultVelocity
|
||||
} else {
|
||||
getVelocityFromFling(gestureVelocity)
|
||||
}.let { v ->
|
||||
when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> v
|
||||
GestureDirection.LEFT_TO_RIGHT -> -v
|
||||
}
|
||||
}
|
||||
|
||||
createFlingAnimation(
|
||||
view = contentLayout,
|
||||
minValue = min(0f, contentLayout.translationX),
|
||||
maxValue = max(0f, contentLayout.translationX),
|
||||
startVelocity = velocity
|
||||
).addUpdateListener { _, value, _ ->
|
||||
tabPreview.translationX = when (gestureDirection) {
|
||||
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
|
||||
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
|
||||
}
|
||||
}.addEndListener { _, _, _, _ ->
|
||||
tabPreview.isVisible = false
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun PointF.isInToolbar(): Boolean {
|
||||
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 ->
|
||||
if (activity.settings().shouldUseBottomToolbar) {
|
||||
toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom)
|
||||
}
|
||||
}
|
||||
return toolbarLocation.contains(toPoint())
|
||||
}
|
||||
|
||||
private val Rect.visibleWidth: Int
|
||||
get() = if (left < 0) {
|
||||
right
|
||||
} else {
|
||||
windowWidth - left
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The percentage of the tab preview that needs to be visible to consider the
|
||||
* tab switching gesture complete.
|
||||
*/
|
||||
private const val GESTURE_FINISH_PERCENT = 0.25
|
||||
|
||||
/**
|
||||
* The percentage of the content view that can be hidden by the tab switching gesture if
|
||||
* there is not tab available to switch to
|
||||
*/
|
||||
private const val OVERSCROLL_HIDE_PERCENT = 0.20
|
||||
|
||||
/**
|
||||
* The speed of the fling animation (in dp per second).
|
||||
*/
|
||||
@Dimension(unit = DP)
|
||||
private const val MINIMUM_ANIMATION_VELOCITY = 1500f
|
||||
|
||||
/**
|
||||
* The size of the gap between the tab preview and content layout.
|
||||
*/
|
||||
@Dimension(unit = DP)
|
||||
private const val PREVIEW_OFFSET = 48
|
||||
}
|
||||
}
|
@ -2,51 +2,66 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.mozilla.fenix.browser.SwipeGestureLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/browserLayout"
|
||||
android:id="@+id/gestureLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="browser.BrowserFragment">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/browserLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<mozilla.components.concept.engine.EngineView
|
||||
android:id="@+id/engineView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/stubFindInPage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:inflatedId="@+id/findInPageView"
|
||||
android:layout="@layout/stub_find_in_page" />
|
||||
|
||||
<include
|
||||
android:id="@+id/viewDynamicDownloadDialog"
|
||||
layout="@layout/download_dialog_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:visibility="gone" />
|
||||
|
||||
<mozilla.components.feature.readerview.view.ReaderViewControlsBar
|
||||
android:id="@+id/readerViewControlsBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?foundation"
|
||||
android:elevation="24dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<org.mozilla.fenix.browser.TabPreview
|
||||
android:id="@+id/tabPreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
<mozilla.components.concept.engine.EngineView
|
||||
android:id="@+id/engineView"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/stubFindInPage"
|
||||
android:inflatedId="@+id/findInPageView"
|
||||
android:layout="@layout/stub_find_in_page"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp" />
|
||||
|
||||
<include
|
||||
android:id="@+id/viewDynamicDownloadDialog"
|
||||
layout="@layout/download_dialog_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<mozilla.components.feature.readerview.view.ReaderViewControlsBar
|
||||
android:id="@+id/readerViewControlsBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?foundation"
|
||||
android:elevation="24dp"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</org.mozilla.fenix.browser.SwipeGestureLayout>
|
||||
|
44
app/src/main/res/layout/tab_preview.xml
Normal file
44
app/src/main/res/layout/tab_preview.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
|
||||
android:id="@+id/previewThumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?tabTrayThumbnailItemBackground" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/fakeToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/browser_toolbar_height"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?bottomBarBackground"
|
||||
android:elevation="5dp"
|
||||
android:foregroundGravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:id="@+id/toolbar_wrapper"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/home_search_background" />
|
||||
|
||||
<org.mozilla.fenix.components.toolbar.TabCounter
|
||||
android:id="@+id/tab_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<mozilla.components.browser.menu.view.MenuButton
|
||||
android:id="@+id/menuButton"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center" />
|
||||
</LinearLayout>
|
||||
</merge>
|
@ -28,6 +28,7 @@ object Versions {
|
||||
const val androidx_paging = "2.1.0"
|
||||
const val androidx_transition = "1.3.0"
|
||||
const val androidx_work = "2.2.0"
|
||||
const val androidx_dynamic_animation = "1.0.0"
|
||||
const val google_material = "1.1.0"
|
||||
const val google_flexbox = "2.0.1"
|
||||
|
||||
@ -170,6 +171,7 @@ object Deps {
|
||||
const val androidx_recyclerview = "androidx.recyclerview:recyclerview:${Versions.androidx_recyclerview}"
|
||||
const val androidx_core = "androidx.core:core:${Versions.androidx_core}"
|
||||
const val androidx_core_ktx = "androidx.core:core-ktx:${Versions.androidx_core}"
|
||||
const val androidx_dynamic_animation = "androidx.dynamicanimation:dynamicanimation:${Versions.androidx_dynamic_animation}"
|
||||
const val androidx_transition = "androidx.transition:transition:${Versions.androidx_transition}"
|
||||
const val androidx_work_ktx = "androidx.work:work-runtime-ktx:${Versions.androidx_work}"
|
||||
const val androidx_work_testing = "androidx.work:work-testing:${Versions.androidx_work}"
|
||||
|
Loading…
Reference in New Issue
Block a user