Merge remote-tracking branch 'abhijitvalluri/tabstray_new_tab_button_bottom_layout' into fork

pull/58/head
Adam Novak 4 years ago
commit 5e4d669ab8

@ -0,0 +1,810 @@
package org.mozilla.fenix.components.topsheet
import android.animation.ValueAnimator
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.*
import androidx.annotation.IntDef
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.NestedScrollingChild
import androidx.core.view.ViewCompat
import androidx.customview.widget.ViewDragHelper
import com.google.android.material.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import java.lang.ref.WeakReference
import kotlin.math.abs
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ /**
* An interaction behavior plugin for a child view of [CoordinatorLayout] to make it work as
* a bottom sheet.
*/
class TopSheetBehavior<V : View?>
/**
* Default constructor for inflating TopSheetBehaviors from layout.
*
* @param context The [Context].
* @param attrs The [AttributeSet].
*/(context: Context, attrs: AttributeSet?) : CoordinatorLayout.Behavior<V>(context, attrs) {
/**
* Callback for monitoring events about top sheets.
*/
abstract class TopSheetCallback {
/**
* Called when the top sheet changes its state.
*
* @param topSheet The top sheet view.
* @param newState The new state. This will be one of [.STATE_DRAGGING],
* [.STATE_SETTLING], [.STATE_EXPANDED],
* [.STATE_COLLAPSED], or [.STATE_HIDDEN].
*/
abstract fun onStateChanged(
topSheet: View,
@State newState: Int
)
/**
* Called when the top sheet is being dragged.
*
* @param topSheet The top sheet view.
* @param slideOffset The new offset of this top sheet within its range, from 0 to 1
* when it is moving upward, and from 0 to -1 when it moving downward.
* @param isOpening detect showing
*/
abstract fun onSlide(
topSheet: View,
slideOffset: Float,
isOpening: Boolean?
)
}
/**
* @hide
*/
@IntDef(
STATE_EXPANDED,
STATE_COLLAPSED,
STATE_DRAGGING,
STATE_SETTLING,
STATE_HIDDEN
)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
annotation class State
private var mMaximumVelocity = 0f
private var mPeekHeight = 0
private var mMinOffset = 0
private var mMaxOffset = 0
private var skipCollapsed = false
/**
* Gets/Sets the height of the top sheet when it is collapsed.
*
* @var peekHeight The height of the collapsed top sheet in pixels.
* @attr ref com.google.android.material.R.styleable#TopSheetBehavior_Params_behavior_peekHeight
*/
private var peekHeight: Int
get() = mPeekHeight
set(peekHeight) {
mPeekHeight = 0.coerceAtLeast(peekHeight)
if (mViewRef != null && mViewRef!!.get() != null) {
mMinOffset =
(-mViewRef!!.get()!!.height).coerceAtLeast(
-(mViewRef!!.get()!!.height - mPeekHeight))
}
}
@State
private var mState = STATE_COLLAPSED
private var mViewDragHelper: ViewDragHelper? = null
private var mIgnoreEvents = false
private var mLastNestedScrollDy = 0
private var mNestedScrolled = false
private var mParentHeight = 0
private var mViewRef: WeakReference<V?>? = null
private var mNestedScrollingChildRef: WeakReference<View?>? = null
private var mCallback: TopSheetCallback? = null
private var mVelocityTracker: VelocityTracker? = null
private var mActivePointerId = 0
private var mInitialY = 0
private var mTouchingScrollingChild = false
/** True if Behavior has a non-null value for the @shapeAppearance attribute */
private var shapeThemingEnabled = false
/** Default Shape Appearance to be used in topsheet */
private var shapeAppearanceModelDefault: ShapeAppearanceModel? = null
private var materialShapeDrawable: MaterialShapeDrawable? = null
private var interpolatorAnimator: ValueAnimator? = null
private var isShapeExpanded = false
var elevation = -1f
var isHideable = false
init {
val a = context.obtainStyledAttributes(
attrs,
R.styleable.BottomSheetBehavior_Layout
)
shapeThemingEnabled =
a.hasValue(R.styleable.BottomSheetBehavior_Layout_shapeAppearance)
createMaterialShapeDrawable(context, attrs!!)
createShapeValueAnimator()
peekHeight = context.resources.displayMetrics.heightPixels * 3 / 4
isHideable = a.getBoolean(
R.styleable.BottomSheetBehavior_Layout_behavior_hideable,
false
)
skipCollapsed = a.getBoolean(
R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
false
)
a.recycle()
val configuration =
ViewConfiguration.get(context)
mMaximumVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
}
override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable? {
return SavedState(
super.onSaveInstanceState(
parent,
child
), mState
)
}
override fun onRestoreInstanceState(
parent: CoordinatorLayout,
child: V,
state: Parcelable
) {
val ss =
state as SavedState
super.onRestoreInstanceState(parent, child, ss.superState)
// Intermediate states are restored as collapsed state
mState = if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) {
STATE_COLLAPSED
} else {
ss.state
}
}
override fun onLayoutChild(
parent: CoordinatorLayout,
child: V,
layoutDirection: Int
): Boolean {
if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child as View)) {
child.fitsSystemWindows = true
}
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
// default to android:background declared in styles or layout.
if (shapeThemingEnabled && materialShapeDrawable != null) {
ViewCompat.setBackground(child as View, materialShapeDrawable)
}
// Set elevation on MaterialShapeDrawable
// Set elevation on MaterialShapeDrawable
if (materialShapeDrawable != null) {
// Use elevation attr if set on topsheet; otherwise, use elevation of child view.
materialShapeDrawable!!.elevation =
if (elevation == -1f) ViewCompat.getElevation(child as View) else elevation
// Update the material shape based on initial state.
isShapeExpanded = state == BottomSheetBehavior.STATE_EXPANDED
materialShapeDrawable!!.interpolation = if (isShapeExpanded) 0f else 1f
}
val savedTop = child!!.top
// First let the parent lay it out
parent.onLayoutChild(child, layoutDirection)
// Offset the top sheet
mParentHeight = parent.height
mMinOffset = (-child.height).coerceAtLeast(-(child.height - mPeekHeight))
mMaxOffset = 0
if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMaxOffset)
} else if (isHideable && mState == STATE_HIDDEN) {
ViewCompat.offsetTopAndBottom(child, mParentHeight)
} else if (mState == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset)
} else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
ViewCompat.offsetTopAndBottom(child, savedTop - child.top)
}
if (mViewDragHelper == null) {
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback)
}
mViewRef = WeakReference(child)
mNestedScrollingChildRef =
WeakReference(findScrollingChild(child))
return true
}
override fun onInterceptTouchEvent(
parent: CoordinatorLayout,
child: V,
event: MotionEvent
): Boolean {
if (!child!!.isShown) {
return false
}
val action = event.actionMasked
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset()
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain()
}
mVelocityTracker!!.addMovement(event)
when (action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
mTouchingScrollingChild = false
mActivePointerId = MotionEvent.INVALID_POINTER_ID
// Reset the ignore flag
if (mIgnoreEvents) {
mIgnoreEvents = false
return false
}
}
MotionEvent.ACTION_DOWN -> {
val initialX = event.x.toInt()
mInitialY = event.y.toInt()
val scroll = mNestedScrollingChildRef!!.get()
if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
mActivePointerId = event.getPointerId(event.actionIndex)
mTouchingScrollingChild = true
}
mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
!parent.isPointInChildBounds(child, initialX, mInitialY)
}
}
if (!mIgnoreEvents && mViewDragHelper!!.shouldInterceptTouchEvent(event)) {
return true
}
// We have to handle cases that the ViewDragHelper does not capture the top sheet because
// it is not the top most view of its parent. This is not necessary when the touch event is
// happening over the scrolling content as nested scrolling logic handles that case.
val scroll = mNestedScrollingChildRef!!.get()
return action == MotionEvent.ACTION_MOVE && scroll != null &&
!mIgnoreEvents && mState != STATE_DRAGGING &&
!parent.isPointInChildBounds(
scroll,
event.x.toInt(),
event.y.toInt()
) && abs(mInitialY - event.y) > mViewDragHelper!!.touchSlop
}
override fun onTouchEvent(
parent: CoordinatorLayout,
child: V,
event: MotionEvent
): Boolean {
if (!child!!.isShown) {
return false
}
val action = event.actionMasked
if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
return true
}
if (mViewDragHelper != null) {
//no crash
mViewDragHelper!!.processTouchEvent(event)
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset()
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain()
}
mVelocityTracker!!.addMovement(event)
// The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
// to capture the top sheet in case it is not captured and the touch slop is passed.
if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents) {
if (abs(mInitialY - event.y) > mViewDragHelper!!.touchSlop) {
mViewDragHelper!!.captureChildView(
child,
event.getPointerId(event.actionIndex)
)
}
}
}
return !mIgnoreEvents
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
nestedScrollAxes: Int
): Boolean {
mLastNestedScrollDy = 0
mNestedScrolled = false
return nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout, child: V, target: View, dx: Int,
dy: Int, consumed: IntArray
) {
val scrollingChild = mNestedScrollingChildRef!!.get()
if (target !== scrollingChild) {
return
}
val currentTop = child!!.top
val newTop = currentTop - dy
if (dy > 0) { // Upward
if (!target.canScrollVertically(1)) {
if (newTop >= mMinOffset || isHideable) {
consumed[1] = dy
ViewCompat.offsetTopAndBottom(child, -dy)
setStateInternal(STATE_DRAGGING)
} else {
consumed[1] = currentTop - mMinOffset
ViewCompat.offsetTopAndBottom(child, -consumed[1])
setStateInternal(STATE_COLLAPSED)
}
}
} else if (dy < 0) { // Downward
// Negative to check scrolling up, positive to check scrolling down
if (newTop < mMaxOffset) {
consumed[1] = dy
ViewCompat.offsetTopAndBottom(child, -dy)
setStateInternal(STATE_DRAGGING)
} else {
consumed[1] = currentTop - mMaxOffset
ViewCompat.offsetTopAndBottom(child, -consumed[1])
setStateInternal(STATE_EXPANDED)
}
}
dispatchOnSlide(child.top)
mLastNestedScrollDy = dy
mNestedScrolled = true
}
override fun onStopNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View
) {
if (child!!.top == mMaxOffset) {
setStateInternal(STATE_EXPANDED)
return
}
if (target !== mNestedScrollingChildRef!!.get() || !mNestedScrolled) {
return
}
val top: Int
val targetState: Int
if (mLastNestedScrollDy < 0) {
top = mMaxOffset
targetState = STATE_EXPANDED
} else if (isHideable && shouldHide(child, yVelocity)) {
top = -child.height
targetState = STATE_HIDDEN
} else if (mLastNestedScrollDy == 0) {
val currentTop = child.top
if (abs(currentTop - mMinOffset) > abs(currentTop - mMaxOffset)) {
top = mMaxOffset
targetState = STATE_EXPANDED
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
if (mViewDragHelper!!.smoothSlideViewTo(child, child.left, top)) {
setStateInternal(STATE_SETTLING)
ViewCompat.postOnAnimation(
child,
SettleRunnable(
child,
targetState
)
)
} else {
setStateInternal(targetState)
}
mNestedScrolled = false
}
override fun onNestedPreFling(
coordinatorLayout: CoordinatorLayout, child: V, target: View,
velocityX: Float, velocityY: Float
): Boolean {
return target === mNestedScrollingChildRef!!.get() &&
(mState != STATE_EXPANDED ||
super.onNestedPreFling(
coordinatorLayout, child, target,
velocityX, velocityY
))
}
/**
* Sets a callback to be notified of top sheet events.
*
* @param callback The callback to notify when top sheet events occur.
*/
fun setTopSheetCallback(callback: TopSheetCallback?) {
mCallback = callback
}
/**
* Gets/Sets the state of the top sheet. When set, the top sheet will transition to
* that state with animation.
*
* @var state One of [.STATE_EXPANDED], [.STATE_COLLAPSED], [.STATE_DRAGGING],
* and [.STATE_SETTLING].
*/
@get:State
var state: Int
get() = mState
set(state) {
if (state == mState) {
return
}
if (mViewRef == null) {
// The view is not laid out yet; modify mState and let onLayoutChild handle it later
if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
isHideable && state == STATE_HIDDEN
) {
mState = state
}
return
}
val child = mViewRef!!.get() ?: return
val top: Int
top = if (state == STATE_COLLAPSED) {
mMinOffset
} else if (state == STATE_EXPANDED) {
mMaxOffset
} else if (isHideable && state == STATE_HIDDEN) {
-child.height
} else {
throw IllegalArgumentException("Illegal state argument: $state")
}
setStateInternal(STATE_SETTLING)
if (mViewDragHelper!!.smoothSlideViewTo(child, child.left, top)) {
ViewCompat.postOnAnimation(
child,
SettleRunnable(child, state)
)
}
}
private var oldState = mState
private fun setStateInternal(@State state: Int) {
if (state == STATE_COLLAPSED || state == STATE_EXPANDED) {
oldState = state
}
if (mState == state) {
return
}
mState = state
updateDrawableForTargetState(state)
val topSheet: View? = mViewRef!!.get()
if (topSheet != null && mCallback != null) {
mCallback!!.onStateChanged(topSheet, state)
}
}
private fun updateDrawableForTargetState(@BottomSheetBehavior.State state: Int) {
if (state == BottomSheetBehavior.STATE_SETTLING) {
// Special case: we want to know which state we're settling to, so wait for another call.
return
}
val expand = state == BottomSheetBehavior.STATE_EXPANDED
if (isShapeExpanded != expand) {
isShapeExpanded = expand
if (materialShapeDrawable != null && interpolatorAnimator != null) {
if (interpolatorAnimator!!.isRunning) {
interpolatorAnimator!!.reverse()
} else {
val to = if (expand) 0f else 1f
val from = 1f - to
interpolatorAnimator!!.setFloatValues(from, to)
interpolatorAnimator!!.start()
}
}
}
}
private fun reset() {
mActivePointerId = ViewDragHelper.INVALID_POINTER
if (mVelocityTracker != null) {
mVelocityTracker!!.recycle()
mVelocityTracker = null
}
}
private fun shouldHide(child: View, yvel: Float): Boolean {
if (child.top > mMinOffset) {
// It should not hide, but collapse.
return false
}
val newTop = child.top + yvel * HIDE_FRICTION
return abs(newTop - mMinOffset) / mPeekHeight.toFloat() > HIDE_THRESHOLD
}
private fun findScrollingChild(view: View): View? {
if (view is NestedScrollingChild) {
return view
}
if (view is ViewGroup) {
var i = 0
val count = view.childCount
while (i < count) {
val scrollingChild = findScrollingChild(view.getChildAt(i))
if (scrollingChild != null) {
return scrollingChild
}
i++
}
}
return null
}
private val yVelocity: Float
get() {
mVelocityTracker!!.computeCurrentVelocity(1000, mMaximumVelocity)
return mVelocityTracker!!.getYVelocity(mActivePointerId)
}
private val mDragCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
override fun tryCaptureView(
child: View,
pointerId: Int
): Boolean {
if (mState == STATE_DRAGGING) {
return false
}
if (mTouchingScrollingChild) {
return false
}
if (mState == STATE_EXPANDED && mActivePointerId == pointerId) {
val scroll = mNestedScrollingChildRef!!.get()
if (scroll != null && scroll.canScrollVertically(-1)) {
// Let the content scroll up
return false
}
}
return mViewRef != null && mViewRef!!.get() === child
}
override fun onViewPositionChanged(
changedView: View,
left: Int,
top: Int,
dx: Int,
dy: Int
) {
dispatchOnSlide(top)
}
override fun onViewDragStateChanged(state: Int) {
if (state == ViewDragHelper.STATE_DRAGGING) {
setStateInternal(STATE_DRAGGING)
}
}
override fun onViewReleased(
releasedChild: View,
xvel: Float,
yvel: Float
) {
val top: Int
@State val targetState: Int
if (yvel > 0) { // Moving up
top = mMaxOffset
targetState = STATE_EXPANDED
} else if (isHideable && shouldHide(releasedChild, yvel)) {
top = -mViewRef!!.get()!!.height
targetState = STATE_HIDDEN
} else if (yvel == 0f) {
val currentTop = releasedChild.top
if (abs(currentTop - mMinOffset) > abs(currentTop - mMaxOffset)) {
top = mMaxOffset
targetState = STATE_EXPANDED
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
if (mViewDragHelper!!.settleCapturedViewAt(releasedChild.left, top)) {
setStateInternal(STATE_SETTLING)
ViewCompat.postOnAnimation(
releasedChild,
SettleRunnable(
releasedChild,
targetState
)
)
} else {
setStateInternal(targetState)
}
}
override fun clampViewPositionVertical(
child: View,
top: Int,
dy: Int
): Int {
return constrain(
top,
if (isHideable) -child.height else mMinOffset,
mMaxOffset
)
}
override fun clampViewPositionHorizontal(
child: View,
left: Int,
dx: Int
): Int {
return child.left
}
override fun getViewVerticalDragRange(child: View): Int {
return if (isHideable) {
child.height
} else {
mMaxOffset - mMinOffset
}
}
}
private fun createMaterialShapeDrawable(
context: Context,
attrs: AttributeSet
) {
if (shapeThemingEnabled) {
shapeAppearanceModelDefault = ShapeAppearanceModel.builder(
context,
attrs,
R.attr.bottomSheetStyle,
DEF_STYLE_RES
)
.build()
materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModelDefault!!)
materialShapeDrawable?.initializeElevationOverlay(context)
val defaultColor = TypedValue()
context.theme
.resolveAttribute(android.R.attr.colorBackground, defaultColor, true)
materialShapeDrawable?.setTint(defaultColor.data)
}
}
private fun createShapeValueAnimator() {
interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f)
interpolatorAnimator?.duration = CORNER_ANIMATION_DURATION.toLong()
interpolatorAnimator!!.addUpdateListener { animation ->
val value = animation.animatedValue as Float
if (materialShapeDrawable != null) {
materialShapeDrawable!!.interpolation = value
}
}
}
private fun dispatchOnSlide(top: Int) {
val topSheet: View? = mViewRef!!.get()
if (topSheet != null && mCallback != null) {
val isOpening = oldState == STATE_COLLAPSED
if (top < mMinOffset) {
mCallback!!.onSlide(
topSheet,
(top - mMinOffset).toFloat() / mPeekHeight,
isOpening
)
} else {
mCallback!!.onSlide(
topSheet,
(top - mMinOffset).toFloat() / (mMaxOffset - mMinOffset), isOpening
)
}
}
}
private inner class SettleRunnable internal constructor(
private val mView: View,
@field:State @param:State private val mTargetState: Int
) :
Runnable {
override fun run() {
if (mViewDragHelper != null && mViewDragHelper!!.continueSettling(true)) {
ViewCompat.postOnAnimation(mView, this)
} else {
setStateInternal(mTargetState)
}
}
}
private class SavedState(superState: Parcelable?, @State val state: Int) :
AbsSavedState(superState) {
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(state)
}
}
companion object {
/**
* The top sheet is dragging.
*/
const val STATE_DRAGGING = 1
/**
* The top sheet is settling.
*/
const val STATE_SETTLING = 2
/**
* The top sheet is expanded.
*/
const val STATE_EXPANDED = 3
/**
* The top sheet is collapsed.
*/
const val STATE_COLLAPSED = 4
/**
* The top sheet is hidden.
*/
const val STATE_HIDDEN = 5
private const val HIDE_THRESHOLD = 0.5f
private const val HIDE_FRICTION = 0.1f
/**
* A utility function to get the [TopSheetBehavior] associated with the `view`.
*
* @param view The [View] with [TopSheetBehavior].
* @return The [TopSheetBehavior] associated with the `view`.
*/
@Suppress("UNCHECKED_CAST")
fun <V : View?> from(view: V): TopSheetBehavior<V> {
val params = view!!.layoutParams
require(params is CoordinatorLayout.LayoutParams) { "The view is not a child of CoordinatorLayout" }
val behavior =
params
.behavior
require(behavior is TopSheetBehavior<*>) { "The view is not associated with TopSheetBehavior" }
return behavior as TopSheetBehavior<V>
}
fun constrain(amount: Int, low: Int, high: Int): Int {
return if (amount < low) low else if (amount > high) high else amount
}
private const val CORNER_ANIMATION_DURATION = 500
private val DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal
}
}

@ -45,6 +45,8 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.INFINITE_CHAR_PADDING_BOTTOM
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.MAX_VISIBLE_TABS
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.SO_MANY_TABS_OPEN
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.components.topsheet.TopSheetBehavior
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange
@ -68,12 +70,17 @@ class TabTrayView(
) : LayoutContainer, TabLayout.OnTabSelectedListener {
val lifecycleScope = lifecycleOwner.lifecycleScope
val view = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray, container, true)
val view = when (container.context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> LayoutInflater.from(container.context).inflate(R.layout.component_tabstray_bottom, container, true)
ToolbarPosition.TOP -> LayoutInflater.from(container.context).inflate(R.layout.component_tabstray, container, true)
}
private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
private val behavior = when (container.context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> TopSheetBehavior.from(view.tab_wrapper)
ToolbarPosition.TOP -> BottomSheetBehavior.from(view.tab_wrapper)
}
private val concatAdapter = ConcatAdapter(tabsAdapter)
private val tabTrayItemMenu: TabTrayItemMenu
@ -95,17 +102,33 @@ class TabTrayView(
init {
components.analytics.metrics.track(Event.TabsTrayOpened)
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
if (container.context.settings().toolbarPosition == ToolbarPosition.TOP) {
(behavior as BottomSheetBehavior).addBottomSheetCallback(object :
BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
}
}
}
})
})
} else {
(behavior as TopSheetBehavior).setTopSheetCallback(object :
TopSheetBehavior.TopSheetCallback() {
override fun onSlide(topSheet: View, slideOffset: Float, isOpening: Boolean?) {
}
override fun onStateChanged(topSheet: View, newState: Int) {
if (newState == TopSheetBehavior.STATE_HIDDEN) {
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
}
}
})
}
val selectedTabIndex = if (!isPrivate) {
DEFAULT_TAB_ID
@ -258,6 +281,9 @@ class TabTrayView(
fun updateTabsTrayLayout() {
view.tabsTray.apply {
val gridLayoutManager = GridLayoutManager(container.context, gridViewNumberOfCols(container.context))
if (container.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) {
gridLayoutManager.reverseLayout = true
}
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val numTabs = tabsAdapter.itemCount
@ -274,7 +300,11 @@ class TabTrayView(
}
fun expand() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
if (container.context.settings().toolbarPosition == ToolbarPosition.TOP) {
(behavior as BottomSheetBehavior).state = BottomSheetBehavior.STATE_EXPANDED
} else {
(behavior as TopSheetBehavior).state = TopSheetBehavior.STATE_EXPANDED
}
}
enum class TabChange {
@ -433,9 +463,15 @@ class TabTrayView(
if (multiselect) MULTISELECT_HANDLE_HEIGHT.dpToPx(displayMetrics) else NORMAL_HANDLE_HEIGHT.dpToPx(
displayMetrics
)
topMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_TOP_MARGIN.dpToPx(
displayMetrics
)
if (container.context.settings().toolbarPosition == ToolbarPosition.TOP) {
topMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_TOP_MARGIN.dpToPx(
displayMetrics
)
} else {
bottomMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_BOTTOM_MARGIN.dpToPx(
displayMetrics
)
}
}
view.tab_wrapper.setChildWPercent(
@ -508,7 +544,9 @@ class TabTrayView(
view.context.resources.getDimension(R.dimen.tab_tray_top_offset).toInt()
}
behavior.setExpandedOffset(topOffset)
if (container.context.settings().toolbarPosition == ToolbarPosition.TOP) {
(behavior as BottomSheetBehavior).setExpandedOffset(topOffset)
}
}
fun dismissMenu() {
@ -543,6 +581,7 @@ class TabTrayView(
private const val MULTISELECT_HANDLE_HEIGHT = 11
private const val NORMAL_HANDLE_HEIGHT = 3
private const val NORMAL_TOP_MARGIN = 8
private const val NORMAL_BOTTOM_MARGIN = 8
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
}
}

@ -12,13 +12,13 @@ import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import mozilla.components.browser.state.state.MediaState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.browser.tabstray.TabsTrayStyling
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.feature.media.ext.pauseIfPlaying
@ -35,8 +35,6 @@ import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.removeAndDisable
import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Do
import kotlin.math.max
@ -50,6 +48,8 @@ class TabTrayViewHolder(
private val metrics: MetricController = itemView.context.components.analytics.metrics
) : TabViewHolder(itemView) {
private val iconCard: CardView = itemView.findViewById(R.id.mozac_browser_tabstray_icon_card)
private val iconView: ImageView = itemView.findViewById(R.id.mozac_browser_tabstray_icon)
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
private val closeView: AppCompatImageButton =
itemView.findViewById(R.id.mozac_browser_tabstray_close)
@ -74,6 +74,7 @@ class TabTrayViewHolder(
// Basic text
updateTitle(tab)
updateIcon(tab)
updateCloseButtonDescription(tab.title)
// Drawables and theme
@ -147,6 +148,15 @@ class TabTrayViewHolder(
titleView.text = title
}
private fun updateIcon(tab: Tab) {
if (tab.icon != null) {
iconCard.visibility = View.VISIBLE
iconView.setImageBitmap(tab.icon)
} else {
iconCard.visibility = View.GONE
}
}
@VisibleForTesting
internal fun updateBackgroundColor(isSelected: Boolean) {
val color = if (isSelected) {

@ -4,7 +4,6 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:mozac="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_wrapper"
style="@style/BottomSheetModal"

@ -0,0 +1,177 @@
<?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/. -->
<androidx.constraintlayout.widget.ConstraintLayout 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/tab_wrapper"
style="@style/TopSheetModal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/foundation_normal_theme"
app:layout_behavior="org.mozilla.fenix.components.topsheet.TopSheetBehavior">
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginBottom="8dp"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.1" />
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_horizontal"
android:paddingBottom="80dp"
android:text="@string/no_open_tabs_description"
android:textColor="?secondaryText"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintBottom_toTopOf="@id/handle">
<ImageButton
android:id="@+id/exit_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
<TextView
android:id="@+id/multiselect_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
<TextView
android:id="@+id/collect_multi_select"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/tab_tray_save_to_collection"
android:textAllCaps="true"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_tab_collection"
app:drawableTint="@color/contrast_text_normal_theme"
app:fontFamily="@font/metropolis_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="0dp"
android:layout_height="80dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5"
app:tabGravity="fill"
app:tabIconTint="@color/tab_icon"
app:tabIndicatorColor="@color/accent_normal_theme"
app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem
android:id="@+id/default_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tab_header_label"
android:layout="@layout/tabs_tray_tab_counter"
app:tabIconTint="@color/tab_icon" />
<com.google.android.material.tabs.TabItem
android:id="@+id/private_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tabs_header_private_tabs_title"
android:icon="@drawable/ic_private_browsing" />
</com.google.android.material.tabs.TabLayout>
<ImageButton
android:id="@+id/tab_tray_new_tab"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_tab"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_new" />
<ImageButton
android:id="@+id/tab_tray_overflow"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu"
android:visibility="visible"
app:tint="@color/accent_normal_theme"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/topBar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:scrollbars="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -7,7 +7,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_item"
android:layout_width="match_parent"
android:layout_height="175dp"
android:layout_height="165dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:selectableItemBackground">
@ -16,8 +16,7 @@
android:id="@+id/play_pause_button"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="23dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/mozac_feature_media_notification_action_pause"
android:elevation="10dp"
@ -30,8 +29,8 @@
android:id="@+id/mozac_browser_tabstray_card"
android:layout_width="match_parent"
android:layout_height="@dimen/tab_tray_thumbnail_height"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="40dp"
android:layout_marginHorizontal="7dp"
android:layout_marginTop="30dp"
android:backgroundTint="?tabTrayThumbnailItemBackground"
app:cardBackgroundColor="@color/photonWhite"
app:layout_constraintStart_toStartOf="parent"
@ -74,24 +73,41 @@
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/mozac_browser_tabstray_icon_card"
android:layout_width="20dp"
android:layout_height="20dp"
app:layout_constraintStart_toStartOf="@id/mozac_browser_tabstray_card"
app:layout_constraintBottom_toTopOf="@id/mozac_browser_tabstray_card"
app:layout_constraintTop_toTopOf="parent"
app:cardCornerRadius="5dp" >
<ImageView
android:id="@+id/mozac_browser_tabstray_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/mozac_browser_tabstray_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:requiresFadingEdge="horizontal"
android:fadingEdgeLength="30dp"
android:fadingEdgeLength="25dp"
android:ellipsize="none"
android:singleLine="true"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp"
android:paddingHorizontal="7dp"
android:paddingVertical="5dp"
android:textColor="@color/tab_tray_item_text_normal_theme"
android:textSize="14sp"
android:visibility="visible"
tools:text="Webpage tile that is long"
app:layout_constraintEnd_toStartOf="@id/mozac_browser_tabstray_close"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/mozac_browser_tabstray_icon_card"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
@ -102,7 +118,6 @@
android:contentDescription="@string/close_tab"
app:layout_constraintEnd_toEndOf="@id/mozac_browser_tabstray_card"
app:layout_constraintBottom_toTopOf="@id/mozac_browser_tabstray_card"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_close"
app:tint="@color/tab_tray_item_text_normal_theme" />

@ -604,6 +604,24 @@
<style name="TabTrayDialogStyle" parent="TabTrayDialogStyleBase" />
<!-- Stuff to make the top sheet with round bottom borders -->
<style name="TopSheetShapeAppearance" parent="ShapeAppearance.MaterialComponents.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeBottomLeft">16dp</item>
<item name="cornerSizeBottomRight">16dp</item>
<item name="colorSurface">@color/photonPurple50</item>
<item name="android:backgroundTint">@color/photonPurple50</item>
<item name="android:colorBackground">@color/photonPurple50</item>
</style>
<style name="TopSheetModal" parent="Widget.Design.BottomSheet.Modal">
<item name="shapeAppearance">@style/TopSheetShapeAppearance</item>
<item name="behavior_fitToContents">false</item>
<item name="behavior_expandedOffset">80</item>
<item name="behavior_skipCollapsed">false</item>
<item name="behavior_halfExpandedRatio">0.4</item>
</style>
<!-- Stuff to make the bottom sheet with round top borders -->
<style name="BottomSheetShapeAppearance" parent="ShapeAppearance.MaterialComponents.LargeComponent">
<item name="cornerFamily">rounded</item>

Loading…
Cancel
Save