|
|
@ -6,19 +6,19 @@ package org.mozilla.fenix.browser
|
|
|
|
|
|
|
|
|
|
|
|
import android.animation.Animator
|
|
|
|
import android.animation.Animator
|
|
|
|
import android.animation.AnimatorListenerAdapter
|
|
|
|
import android.animation.AnimatorListenerAdapter
|
|
|
|
|
|
|
|
import android.animation.ValueAnimator
|
|
|
|
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.util.TypedValue
|
|
|
|
|
|
|
|
import android.view.View
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewConfiguration
|
|
|
|
import android.view.ViewConfiguration
|
|
|
|
import androidx.annotation.Dimension
|
|
|
|
import androidx.annotation.Dimension
|
|
|
|
import androidx.annotation.Dimension.DP
|
|
|
|
import androidx.annotation.Dimension.DP
|
|
|
|
|
|
|
|
import androidx.core.animation.doOnEnd
|
|
|
|
import androidx.core.graphics.contains
|
|
|
|
import androidx.core.graphics.contains
|
|
|
|
import androidx.core.graphics.toPoint
|
|
|
|
import androidx.core.graphics.toPoint
|
|
|
|
import androidx.core.view.isVisible
|
|
|
|
import androidx.core.view.isVisible
|
|
|
|
import androidx.dynamicanimation.animation.DynamicAnimation
|
|
|
|
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
|
|
|
import androidx.dynamicanimation.animation.FlingAnimation
|
|
|
|
|
|
|
|
import mozilla.components.browser.session.Session
|
|
|
|
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
|
|
|
@ -61,11 +61,6 @@ class ToolbarGestureHandler(
|
|
|
|
|
|
|
|
|
|
|
|
private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
|
|
|
|
private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
|
|
|
|
private val minimumFlingVelocity = ViewConfiguration.get(activity).scaledMinimumFlingVelocity
|
|
|
|
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
|
|
|
|
private var gestureDirection = GestureDirection.LEFT_TO_RIGHT
|
|
|
|
|
|
|
|
|
|
|
@ -143,25 +138,12 @@ class ToolbarGestureHandler(
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
val destination = getDestination()
|
|
|
|
val destination = getDestination()
|
|
|
|
if (destination is Destination.Tab && isGestureComplete(velocityX)) {
|
|
|
|
if (destination is Destination.Tab && isGestureComplete(velocityX)) {
|
|
|
|
animateToNextTab(velocityX, destination.session)
|
|
|
|
animateToNextTab(destination.session)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
animateCanceledGesture(velocityX)
|
|
|
|
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 {
|
|
|
|
private fun getDestination(): Destination {
|
|
|
|
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
|
|
|
|
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
|
|
|
|
val currentSession = sessionManager.selectedSession ?: return Destination.None
|
|
|
|
val currentSession = sessionManager.selectedSession ?: return Destination.None
|
|
|
@ -234,73 +216,59 @@ class ToolbarGestureHandler(
|
|
|
|
abs(velocityX) >= minimumFlingVelocity)
|
|
|
|
abs(velocityX) >= minimumFlingVelocity)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun getVelocityFromFling(velocityX: Float): Float {
|
|
|
|
private fun getAnimator(finalContextX: Float, duration: Long): ValueAnimator {
|
|
|
|
return max(abs(velocityX), defaultVelocity)
|
|
|
|
return ValueAnimator.ofFloat(contentLayout.translationX, finalContextX).apply {
|
|
|
|
|
|
|
|
this.duration = duration
|
|
|
|
|
|
|
|
this.interpolator = LinearOutSlowInInterpolator()
|
|
|
|
|
|
|
|
addUpdateListener { animator ->
|
|
|
|
|
|
|
|
val value = animator.animatedValue as Float
|
|
|
|
|
|
|
|
contentLayout.translationX = value
|
|
|
|
|
|
|
|
tabPreview.translationX = when (gestureDirection) {
|
|
|
|
|
|
|
|
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
|
|
|
|
|
|
|
|
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun animateToNextTab(velocityX: Float, session: Session) {
|
|
|
|
private fun animateToNextTab(session: Session) {
|
|
|
|
val browserFinalXCoordinate: Float = when (gestureDirection) {
|
|
|
|
val browserFinalXCoordinate: Float = when (gestureDirection) {
|
|
|
|
GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset
|
|
|
|
GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset
|
|
|
|
GestureDirection.LEFT_TO_RIGHT -> 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
|
|
|
|
// Finish animating the contentLayout off screen and tabPreview on screen
|
|
|
|
createFlingAnimation(
|
|
|
|
getAnimator(browserFinalXCoordinate, FINISHED_GESTURE_ANIMATION_DURATION).apply {
|
|
|
|
view = contentLayout,
|
|
|
|
doOnEnd {
|
|
|
|
minValue = min(0f, browserFinalXCoordinate),
|
|
|
|
contentLayout.translationX = 0f
|
|
|
|
maxValue = max(0f, browserFinalXCoordinate),
|
|
|
|
sessionManager.select(session)
|
|
|
|
startVelocity = animationVelocity
|
|
|
|
|
|
|
|
).addUpdateListener { _, value, _ ->
|
|
|
|
// Fade out the tab preview to prevent flickering
|
|
|
|
tabPreview.translationX = when (gestureDirection) {
|
|
|
|
val shortAnimationDuration =
|
|
|
|
GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
|
|
|
|
activity.resources.getInteger(android.R.integer.config_shortAnimTime)
|
|
|
|
GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
|
|
|
|
tabPreview.animate()
|
|
|
|
|
|
|
|
.alpha(0f)
|
|
|
|
|
|
|
|
.setDuration(shortAnimationDuration.toLong())
|
|
|
|
|
|
|
|
.setListener(object : AnimatorListenerAdapter() {
|
|
|
|
|
|
|
|
override fun onAnimationEnd(animation: Animator?) {
|
|
|
|
|
|
|
|
tabPreview.isVisible = false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}.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()
|
|
|
|
}.start()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun animateCanceledGesture(gestureVelocity: Float) {
|
|
|
|
private fun animateCanceledGesture(velocityX: Float) {
|
|
|
|
val velocity = if (getDestination() is Destination.None) {
|
|
|
|
val duration = if (abs(velocityX) >= minimumFlingVelocity) {
|
|
|
|
defaultVelocity
|
|
|
|
CANCELED_FLING_ANIMATION_DURATION
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
getVelocityFromFling(gestureVelocity)
|
|
|
|
CANCELED_GESTURE_ANIMATION_DURATION
|
|
|
|
}.let { v ->
|
|
|
|
|
|
|
|
when (gestureDirection) {
|
|
|
|
|
|
|
|
GestureDirection.RIGHT_TO_LEFT -> v
|
|
|
|
|
|
|
|
GestureDirection.LEFT_TO_RIGHT -> -v
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
createFlingAnimation(
|
|
|
|
getAnimator(0f, duration).apply {
|
|
|
|
view = contentLayout,
|
|
|
|
doOnEnd {
|
|
|
|
minValue = min(0f, contentLayout.translationX),
|
|
|
|
tabPreview.isVisible = false
|
|
|
|
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()
|
|
|
|
}.start()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -337,15 +305,24 @@ class ToolbarGestureHandler(
|
|
|
|
private const val OVERSCROLL_HIDE_PERCENT = 0.20
|
|
|
|
private const val OVERSCROLL_HIDE_PERCENT = 0.20
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* The speed of the fling animation (in dp per second).
|
|
|
|
* The size of the gap between the tab preview and content layout.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@Dimension(unit = DP)
|
|
|
|
@Dimension(unit = DP)
|
|
|
|
private const val MINIMUM_ANIMATION_VELOCITY = 1500f
|
|
|
|
private const val PREVIEW_OFFSET = 48
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* The size of the gap between the tab preview and content layout.
|
|
|
|
* Animation duration when switching to another tab
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@Dimension(unit = DP)
|
|
|
|
private const val FINISHED_GESTURE_ANIMATION_DURATION = 250L
|
|
|
|
private const val PREVIEW_OFFSET = 48
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Animation duration gesture is canceled due to the swipe not being far enough
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private const val CANCELED_GESTURE_ANIMATION_DURATION = 200L
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Animation duration gesture is canceled due to a swipe in the opposite direction
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private const val CANCELED_FLING_ANIMATION_DURATION = 150L
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|