Open Gelembjuk opened 1 year ago
you can use it
import android.content.Context
import android.content.res.Resources.NotFoundException
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.annotation.ColorInt
import androidx.annotation.IdRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.size
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.shape.MaterialShapeDrawable
import com.mymro.rail.pda.R
import com.mymro.rail.pda.databinding.BottomNavBarBinding
class BottomNavBar(context: Context, attrs: AttributeSet?) : CoordinatorLayout(context, attrs) {
private val binding by lazy { BottomNavBarBinding.inflate(LayoutInflater.from(getContext())) }
private var onBottomNavigationListener: OnBottomNavigationListener? = null
/**
* Sets callback for bottom nav menu item selection.
* @param onBottomNavigationListener bottomNavigation bar callback
*/
fun setBottomNavigationListener(onBottomNavigationListener: OnBottomNavigationListener?) {
this.onBottomNavigationListener = onBottomNavigationListener
}
/**
* Interface definition for a callback to be invoked when user select a menu item on the bottomNav bar
*/
interface OnBottomNavigationListener {
/**
* Fires when user select BottomNav menu item
*
* @param menuItem menuItem selected by user
*/
fun onNavigationItemSelected(menuItem: MenuItem?): Boolean
}
companion object {
private val TAG = BottomNavBar::class.java.simpleName
}
init {
// inflate(getContext(), R.layout.bottom_nav_bar, this)
addView(
binding.root,
LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
)
// initialize the bottom nav and fab views
// bottomNavigationView = findViewById(R.id.nav_view)
// fab = findViewById(R.id.fab)
val styleAttrs = context.theme.obtainStyledAttributes(attrs, R.styleable.BottomNavBar, 0, 0)
val curveMargin = styleAttrs.getDimension(R.styleable.BottomNavBar_bn_curve_margin, 0f)
val roundedCornerRadius =
styleAttrs.getDimension(R.styleable.BottomNavBar_bn_curve_rounded_corner_radius, 0f)
val cradleVerticalOffset =
styleAttrs.getDimension(R.styleable.BottomNavBar_bn_curve_vertical_offset, 0f)
@ColorInt val backgroundColor = styleAttrs.getColor(
R.styleable.BottomNavBar_bn_background_color,
binding.navView.solidColor
) // binding.navView.getItemBackground()
@IdRes val menuResID = styleAttrs.getResourceId(R.styleable.BottomNavBar_bn_menu, -1)
val fabMenuIndex = styleAttrs.getInt(R.styleable.BottomNavBar_bn_fab_menu_index, -1)
val menuItemColorState =
styleAttrs.getColorStateList(R.styleable.BottomNavBar_bn_item_color)
val fabBackgroundColor =
styleAttrs.getColorStateList(R.styleable.BottomNavBar_bn_fab_background_color)
val fabIconColor = styleAttrs.getColorStateList(R.styleable.BottomNavBar_bn_fab_icon_color)
val fabDiameter: Float
val fabSize: Int
val bnFabSize = styleAttrs.getString(R.styleable.BottomNavBar_bn_fab_size)
if (bnFabSize == null || bnFabSize.equals("normal", ignoreCase = true)) {
// set fab param to default fab parameters
fabDiameter = resources.getDimension(R.dimen.fab_size_normal)
fabSize = FloatingActionButton.SIZE_NORMAL
} else if (bnFabSize.equals("mini", ignoreCase = true)) {
fabDiameter = resources.getDimension(R.dimen.fab_size_mini)
fabSize = FloatingActionButton.SIZE_MINI
} else {
Log.e(TAG, "bnFabSize should be either 'mini' or 'normal'")
throw IllegalArgumentException("bnFabSize '$bnFabSize' does not match any of the fab size values")
}
// check if no menu has been added to BottomNav view
if (menuResID == -1) {
Log.e(TAG, "menuResID should not be null.")
throw NotFoundException("Menu must be added to BottomNav.")
}
// add the menu to the bottom-nav
binding.navView.inflateMenu(menuResID)
binding.navView.itemIconTintList = menuItemColorState
binding.navView.itemTextColor = menuItemColorState
// set listener on the bottomNavigationView
binding.navView.setOnNavigationItemSelectedListener { item ->
onBottomNavigationListener != null && onBottomNavigationListener!!.onNavigationItemSelected(
item
)
}
// set listener on the fab
binding.fab.setOnClickListener {
binding.navView.selectedItemId =
binding.navView.menu.getItem(fabMenuIndex).itemId
}
// check if fab is not added
if (fabMenuIndex == -1) {
binding.fab.visibility = GONE
Log.w(TAG, "No fab is added")
}
// check if fab position does not match any menu item position
if (fabMenuIndex >= binding.navView.menu.size()) {
binding.fab.visibility = GONE
Log.e(
TAG,
"fabMenuIndex is out of bound. fabMenuIndex only accepts values from 0 to " + (binding.navView.menu.size() - 1)
)
throw IllegalArgumentException("fabMenuIndex does not match any menu item position")
}
// show fab
binding.fab.visibility = VISIBLE
// set fav size
binding.fab.size = fabSize
// set the fab background tint
binding.fab.backgroundTintList = fabBackgroundColor
// set the fab icon tint
binding.fab.imageTintList = fabIconColor
// get the icon from menu item and set it to the fab
binding.fab.setImageDrawable(binding.navView.menu.getItem(fabMenuIndex).icon)
// remove the icon from the item in the bottom-nav
binding.navView.menu.getItem(fabMenuIndex).icon = null
// get an instance of the 1st menu item, as we need it to
val bottomNavigationMenuView = binding.navView.getChildAt(0) as BottomNavigationMenuView
val bottomNavigationItemView =
bottomNavigationMenuView.getChildAt(1) as BottomNavigationItemView
binding.navView.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
// kill the global layout listener so it does not keep calling the layout
bottomNavigationItemView.viewTreeObserver.removeOnGlobalLayoutListener(this)
// calculate the distance between the fab center and the bottom_nav's left edge
val leftEdge =
(width - bottomNavigationItemView.measuredWidth * binding.navView.menu.size) / 2
val fabMarginLeft =
leftEdge + fabMenuIndex * bottomNavigationItemView.measuredWidth + bottomNavigationItemView.measuredWidth / 2 - binding.fab.measuredWidth / 2
//get the fab layout params
val mp = binding.fab.layoutParams as MarginLayoutParams
// set the fab margins
mp.setMargins(fabMarginLeft, 0, 0, 0)
// apply changes and initial the fab view
binding.fab.requestLayout()
val topEdgeTreatment = TopEdgeTreatment(
leftEdge,
bottomNavigationItemView.measuredWidth,
fabMenuIndex,
curveMargin,
roundedCornerRadius,
cradleVerticalOffset
)
topEdgeTreatment.setFabDiameter(fabDiameter)
val materialShapeDrawable = MaterialShapeDrawable()
val shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
.toBuilder().setTopEdge(topEdgeTreatment).build()
materialShapeDrawable.setTint(backgroundColor)
materialShapeDrawable.shapeAppearanceModel = shapeAppearanceModel
ViewCompat.setBackground(binding.navView, materialShapeDrawable)
}
})
}
}
TopEdgeTreatment
import com.google.android.material.shape.EdgeTreatment
import com.google.android.material.shape.ShapePath
import kotlin.math.atan
import kotlin.math.sqrt
/**
* This is a Top edge treatment for the bottom navigation bar, (customized from BottomAppBarTopEdgeTreatment.java)
* which "cradles" a circular {@link FloatingActionButton}.
*
* <p>This edge features a downward semi-circular cutout from the edge line. The two corners created
* by the cutout can optionally be rounded. The circular cutout can also support a vertically offset
* FloatingActionButton; i.e., the cut-out need not be a perfect semi-circle, but could be an arc of
* less than 180 degrees that does not start or finish with a vertical path. This vertical offset
* must be positive.
*/
class TopEdgeTreatment(
private val leftEdge: Int,
private val menuItemWidth: Int,
private val fabMenuIndex: Int,
private val fabMargin: Float,
private val roundedCornerRadius: Float,
private val cradleVerticalOffset: Float
) : EdgeTreatment() {
private var fabDiameter: Float = 0f
//private val horizontalOffset: Float? = null
init {
if (cradleVerticalOffset < 0.0f) throw IllegalArgumentException("cradleVerticalOffset must be positive.")
}
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
super.getEdgePath(length, center, interpolation, shapePath)
if (fabDiameter == 0f) { // There is no cutout to draw
shapePath.lineTo(length, 0f)
return
}
val cradleDiameter: Float = fabMargin * 2.0f + fabDiameter
val cradleRadius = cradleDiameter / 2.0f
val roundedCornerOffset: Float = interpolation * this.roundedCornerRadius
val fabPositionX: Float = (leftEdge + fabMenuIndex * menuItemWidth + menuItemWidth / 2).toFloat()
//val middle: Float = center + horizontalOffset
val verticalOffset: Float =
interpolation * cradleVerticalOffset + (1.0f - interpolation) * cradleRadius
val verticalOffsetRatio = verticalOffset / cradleRadius
if (verticalOffsetRatio >= 1.0f) {
// Vertical offset is so high that there's no curve to draw in the edge, i.e., the fab is
// actually above the edge so just draw a straight line.
shapePath.lineTo(length, 0.0f)
return // Early exit.
}
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
val distanceBetweenCenters = cradleRadius + roundedCornerOffset
val distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters
val distanceY = verticalOffset + roundedCornerOffset
val distanceX = sqrt((distanceBetweenCentersSquared - distanceY * distanceY).toDouble())
.toFloat()
// Calculate the x position of the rounded corner circles.
// Calculate the x position of the rounded corner circles.
val leftRoundedCornerCircleX = fabPositionX - distanceX
val rightRoundedCornerCircleX = fabPositionX + distanceX
// Calculate the arc between the center of the two circles.
// Calculate the arc between the center of the two circles.
val cornerRadiusArcLength =
Math.toDegrees(atan((distanceX / distanceY).toDouble())).toFloat()
val cutoutArcOffset = 90.0f - cornerRadiusArcLength
// Draw the starting line up to the left rounded corner.
// Draw the starting line up to the left rounded corner.
shapePath.lineTo(leftRoundedCornerCircleX - roundedCornerOffset, 0.0f)
// Draw the arc for the left rounded corner circle. The bounding box is the area around the
// circle's center which is at `(leftRoundedCornerCircleX, roundedCornerOffset)`.
// Draw the arc for the left rounded corner circle. The bounding box is the area around the
// circle's center which is at `(leftRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
leftRoundedCornerCircleX - roundedCornerOffset,
0.0f,
leftRoundedCornerCircleX + roundedCornerOffset,
roundedCornerOffset * 2.0f,
270.0f,
cornerRadiusArcLength
)
// Draw the cutout circle.
// Draw the cutout circle.
shapePath.addArc(
fabPositionX - cradleRadius,
-cradleRadius - verticalOffset,
fabPositionX + cradleRadius,
cradleRadius - verticalOffset,
180.0f - cutoutArcOffset,
cutoutArcOffset * 2.0f - 180.0f
)
// Draw an arc for the right rounded corner circle. The bounding box is the area around the
// circle's center which is at `(rightRoundedCornerCircleX, roundedCornerOffset)`.
// Draw an arc for the right rounded corner circle. The bounding box is the area around the
// circle's center which is at `(rightRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
rightRoundedCornerCircleX - roundedCornerOffset,
0.0f,
rightRoundedCornerCircleX + roundedCornerOffset,
roundedCornerOffset * 2.0f,
270.0f - cornerRadiusArcLength,
cornerRadiusArcLength
)
// Draw the ending line after the right rounded corner.
// Draw the ending line after the right rounded corner.
shapePath.lineTo(length, 0.0f)
}
/**
* Returns current fab diameter in pixels.
*/
fun getFabDiameter(): Float {
return fabDiameter
}
/**
* Sets the fab diameter the size of the fab in pixels.
*/
fun setFabDiameter(fabDiameter: Float) {
this.fabDiameter = fabDiameter
}
}
Hi, The lib is nice. But i had no success with using it. For some reason the center button is not aligned correctly
For now i tested only with one of my emulators.
My code is
` <com.zagori.bottomnavbar.BottomNavBar android:id="@+id/bottom_nav_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom"
How can i make it to work?