1
0
Fork 0

For #3481 - Implement swipe on toolbar to switch tabs.

master
Kainalu Hagiwara 2020-06-25 14:52:52 -07:00 committed by Jeff Boek
parent 9a2da5bb0f
commit e7dc5580b2
9 changed files with 680 additions and 39 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)!!,

View File

@ -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)
}
}
}

View 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)
}
}

View File

@ -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
}
}

View File

@ -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>

View 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>

View File

@ -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}"