Reputation: 2980
How to make the bottom navigation view to a specific shape?
I'd like to have a bottom navigation view of this shape:
I have tried setting it as background of my bottom nav view as:
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigationBottomView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_nav_bar"
app:itemHorizontalTranslationEnabled="true"
app:itemIconTint="@drawable/bottom_bar_selector"
app:itemTextColor="@drawable/bottom_bar_selector"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/nav_menu"/>
But it doesn't seem to work.
Any help will be appreciated. Thanks!
Upvotes: 2
Views: 2462
Reputation: 9113
The BottomNavigationView
by default has a background of MaterialShapeDrawable
so you can change its shape using the ShapeAppearanceModel
by defining a custom TopEdge EdgeTreatment
to draw the half-circle above the BottomNavigationView
. To be able to draw something above the BottomNavigationView
you need to have a parent which has the below attributes:
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="35dp"
An Xml sample will be like the below:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<RelativeLayout
android:id="@+id/bottomNavigationViewParentRL"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="35dp"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:backgroundTint="@color/white"
app:elevation="2dp"
app:labelVisibilityMode="labeled"
app:itemIconSize="25dp"
app:itemIconTint="@color/item_icon_tint_selector"
app:itemTextColor="@color/item_text_color_selector"
app:menu="@menu/bottom_nav_menu" />
</RelativeLayout>
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottomNavigationViewParentRL"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Then draw the shape like the below:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val materialShapeDrawable = bottomNavigationView.getBackground() as MaterialShapeDrawable
materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
.toBuilder()
.setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 10.toFloat()))
.build()
where CutoutCircleEdgeTreatment
is a subclass of EdgeTreatment
to draw the half-circle at the top which is similar code like the build-in BottomAppBarTopEdgeTreatment
class which draws a semi-circular cutout from the top edge to bottom:
class CutoutCircleEdgeTreatment(res: Resources, circleDiameterDp: Float, circleLeftRightOffsetDp: Float) : EdgeTreatment() {
private val fabDiameter: Float
private val offset: Float
init {
fabDiameter = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleDiameterDp, res.getDisplayMetrics())
offset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleLeftRightOffsetDp, res.getDisplayMetrics())
}
override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
if (fabDiameter == 0f) {
// There is no cutout to draw.
shapePath.lineTo(length, 0f)
return
}
val fabMargin = 0f
val cradleDiameter = fabMargin * 2 + fabDiameter
val cradleRadius = cradleDiameter / 2f
val roundedCornerRadius = 0f
val roundedCornerOffset = interpolation * roundedCornerRadius
val horizontalOffset = 0f
val middle = center + horizontalOffset
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
val cradleVerticalOffset = 0f
val verticalOffset =
interpolation * cradleVerticalOffset + (1 - 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, 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.
val fabCornerSize = -1f
val cornerSize = fabCornerSize * interpolation
val arcOffset = 0f
val distanceBetweenCenters = cradleRadius + roundedCornerOffset
val distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters
val distanceY = verticalOffset + roundedCornerOffset
val distanceX =
Math.sqrt((distanceBetweenCentersSquared - distanceY * distanceY).toDouble())
.toFloat()
// Calculate the x position of the rounded corner circles.
val leftRoundedCornerCircleX = middle - distanceX
val rightRoundedCornerCircleX = middle + distanceX
// Calculate the arc between the center of the two circles.
val cornerRadiusArcLength =
Math.toDegrees(Math.atan((distanceX / distanceY).toDouble())).toFloat()
val cutoutArcOffset = ARC_QUARTER - cornerRadiusArcLength + arcOffset
// Draw the starting line up to the left rounded corner.
shapePath.lineTo( /* x= */leftRoundedCornerCircleX, 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)`.
shapePath.addArc( /* left= */
leftRoundedCornerCircleX - roundedCornerOffset, 0f, /* right= */
leftRoundedCornerCircleX + roundedCornerOffset, /* bottom= */
roundedCornerOffset * 2, /* startAngle= */
ANGLE_UP.toFloat(), /* sweepAngle= */
cornerRadiusArcLength
)
// Draw the cutout circle.
shapePath.addArc( /* left= */
middle - (cradleRadius + offset), /* top= */
-cradleRadius - verticalOffset, /* right= */
middle + (cradleRadius + offset), /* bottom= */
cradleRadius - verticalOffset, /* startAngle= */
ANGLE_LEFT - cutoutArcOffset, /* sweepAngle= */
cutoutArcOffset * 2 + ARC_HALF
)
// 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( /* left= */
rightRoundedCornerCircleX - roundedCornerOffset, 0f, /* right= */
rightRoundedCornerCircleX + roundedCornerOffset, /* bottom= */
roundedCornerOffset * 2, /* startAngle= */
ANGLE_UP - cornerRadiusArcLength, /* sweepAngle= */
cornerRadiusArcLength
)
// Draw the ending line after the right rounded corner.
shapePath.lineTo( /* x= */length, 0f)
}
companion object {
private const val ARC_QUARTER = 90
private const val ARC_HALF = 180
private const val ANGLE_UP = 270
private const val ANGLE_LEFT = 180
}
}
From the above CutoutCircleEdgeTreatment
constructor you can pass the circleDiameterDp
which is the circle diameter in dp value (in the above example is set to 70dp so the parent RelativeLayout it should have paddingTop equal to the radius of the Circle which is 70/2 = 35dp) and the circleLeftRightOffsetDp
is used to draw the circle with a left/right offset in dp value. Of Course you can modify further the code based on your needs.
Result:
To overlap the semi circle with the fragment hosted
To make the semi circle overlap with the fragment hosted you have to change the order of fragment:nav_host_fragment_activity_main
with the RelativeLayout bottomNavigationViewParentRL
like in the below sample:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottomNavigationViewParentRL"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
<RelativeLayout
android:id="@+id/bottomNavigationViewParentRL"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="35dp"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:backgroundTint="@color/white"
app:elevation="2dp"
app:labelVisibilityMode="labeled"
app:itemIconSize="25dp"
app:itemIconTint="@color/item_icon_tint_selector"
app:itemTextColor="@color/item_text_color_selector"
app:menu="@menu/bottom_nav_menu" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
And also give in each of your fragments some bottom margin with the same height of the navigation bar to start at the point of semi circle like in the below sample:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
tools:context=".ui.dashboard.DashboardFragment">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_green_dark"
android:layout_marginBottom="55dp">
<TextView
android:id="@+id/text_dashboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="25dp"
android:textAlignment="center"
android:textColor="@color/black"
android:text="This is dashboard Fragment"
android:textSize="20sp"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
</RelativeLayout>
Result:
Another variation of CutoutCircleEdgeTreatment
class CutoutCircleEdgeTreatment(res: Resources, circleDiameterDp: Float, circleLeftRightOffsetDp: Float) : EdgeTreatment() {
private val fabDiameter: Float
private val offset: Float
init {
fabDiameter = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleDiameterDp, res.getDisplayMetrics())
offset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleLeftRightOffsetDp, res.getDisplayMetrics())
}
override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
if (fabDiameter == 0f) {
// There is no cutout to draw.
shapePath.lineTo(length, 0f)
return
}
val fabMargin = 0f
val cradleDiameter = fabMargin * 2 + fabDiameter
val cradleRadius = cradleDiameter / 2f
val roundedCornerRadius = 0f
val roundedCornerOffset = interpolation * roundedCornerRadius
val horizontalOffset = 0f
val middle = center + horizontalOffset
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
val cradleVerticalOffset = 0f
val verticalOffset =
interpolation * cradleVerticalOffset + (1 - 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, 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.
val fabCornerSize = -1f
val cornerSize = fabCornerSize * interpolation
val arcOffset = 0f
val distanceBetweenCenters = cradleRadius + roundedCornerOffset
val distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters
val distanceY = verticalOffset + roundedCornerOffset
val distanceX =
Math.sqrt((distanceBetweenCentersSquared - distanceY * distanceY).toDouble())
.toFloat()
// Calculate the x position of the rounded corner circles.
val leftRoundedCornerCircleX = middle - distanceX
val rightRoundedCornerCircleX = middle + distanceX
// Calculate the arc between the center of the two circles.
val cornerRadiusArcLength =
Math.toDegrees(Math.atan((distanceX / distanceY).toDouble())).toFloat()
val cutoutArcOffset = ARC_QUARTER - cornerRadiusArcLength + arcOffset
// Draw the cutout circle.
shapePath.addArc( /* left= */
middle - (cradleRadius + offset), /* top= */
-cradleRadius - verticalOffset, /* right= */
middle + (cradleRadius + offset), /* bottom= */
(cradleRadius - verticalOffset) * 2, /* startAngle= */
ANGLE_LEFT + 20.0f, /* sweepAngle= */
ARC_HALF - 40.0f
)
}
companion object {
private const val ARC_QUARTER = 90
private const val ARC_HALF = 180
private const val ANGLE_UP = 270
private const val ANGLE_LEFT = 180
}
}
Usage:
val materialShapeDrawable = bottomNavigationView.getBackground() as MaterialShapeDrawable
materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
.toBuilder()
.setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 20.toFloat()))
.build()
Result:
Upvotes: 4