Use different layout for tabs tray when toolbar is at bottom
parent
e952589ef2
commit
adf0d3d84f
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
<?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:mozac="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:layout_marginBottom="5dp"
|
||||||
|
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="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/divider" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue