要使其正常工作,您需要扩展 BottomNavigationView 以便能够拥有多个侦听器。最初的实现只允许一个,但您需要另一个能够告诉指标何时移动。
class CustomBottomNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BottomNavigationView(context, attrs, defStyleAttr), OnNavigationItemSelectedListener {
private val onNavigationItemSelectedListeners =
mutableListOf<OnNavigationItemSelectedListener>()
init {
super.setOnNavigationItemSelectedListener(this)
itemIconTintList = null
}
override fun setOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener?) {
if (listener != null) addOnNavigationItemSelectedListener(listener)
}
fun addOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener) {
onNavigationItemSelectedListeners.add(listener)
}
fun addOnNavigationItemSelectedListener(listener: (Int) -> Unit) {
addOnNavigationItemSelectedListener(OnNavigationItemSelectedListener {
for (i in 0 until menu.size()) if (menu.getItem(i) == it) listener(i)
false
})
}
override fun onNavigationItemSelected(item: MenuItem) = onNavigationItemSelectedListeners
.map { it.onNavigationItemSelected(item) }
.fold(false) { acc, it -> acc || it }
}
然后创建一个指标项:
class BottomNavigationViewIndicator @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
val size = 8f
private val targetId: Int
private var target: BottomNavigationMenuView? = null
private var rect = Rect()
// val drawPaint = Paint()
private val backgroundDrawable: Drawable
private var index = 0
private var animator: AnimatorSet? = null
init {
if (attrs == null) {
targetId = NO_ID
backgroundDrawable = ColorDrawable(Color.TRANSPARENT)
} else {
with(context.obtainStyledAttributes(attrs, BottomNavigationViewIndicator)) {
targetId =
getResourceId(BottomNavigationViewIndicator_targetBottomNavigation, NO_ID)
val clippableId =
getResourceId(BottomNavigationViewIndicator_clippableBackground, NO_ID)
backgroundDrawable = if (clippableId != NO_ID) {
androidx.appcompat.content.res.AppCompatResources.getDrawable(
context,
clippableId
) ?: ColorDrawable(Color.TRANSPARENT)
} else {
ColorDrawable(
getColor(
BottomNavigationViewIndicator_clippableBackground,
Color.TRANSPARENT
)
)
}
recycle()
}
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (targetId == NO_ID) return attachedError("invalid target id $targetId, did you set the app:targetBottomNavigation attribute?")
val parentView =
parent as? View ?: return attachedError("Impossible to find the view using $parent")
val child = parentView.findViewById<View?>(targetId)
if (child !is CustomBottomNavigationView) return attachedError("Invalid view $child, the app:targetBottomNavigation has to be n ListenableBottomNavigationView")
for (i in 0 until child.childCount) {
val subView = child.getChildAt(i)
if (subView is BottomNavigationMenuView) target = subView
}
elevation = child.elevation
child.addOnNavigationItemSelectedListener { updateRectByIndex(it, true) }
post { updateRectByIndex(index, false) }
}
private fun attachedError(message: String) {
Log.e("BNVIndicator", message)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
target = null
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.clipRect(rect)
// canvas.drawCircle(size,size,size, drawPaint)
backgroundDrawable.draw(canvas)
}
private fun updateRectByIndex(index: Int, animated: Boolean) {
this.index = index
target?.apply {
if (childCount < 1 || index >= childCount) return
val reference = getChildAt(index)
val start = reference.left + left
val end = reference.right + left
backgroundDrawable.setBounds(left, top, right, bottom)
val newRect = Rect(start, 0, end, height)
if (animated) startUpdateRectAnimation(newRect) else updateRect(newRect)
}
}
private fun startUpdateRectAnimation(rect: Rect) {
animator?.cancel()
animator = AnimatorSet().also {
it.playTogether(
ofInt(this, "rectLeft", this.rect.left, rect.left),
ofInt(this, "rectRight", this.rect.right, rect.right),
ofInt(this, "rectTop", this.rect.top, rect.top),
ofInt(this, "rectBottom", this.rect.bottom, rect.bottom)
)
it.interpolator = FastOutSlowInInterpolator()
it.duration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
it.start()
}
}
private fun updateRect(rect: Rect) {
this.rect = rect
postInvalidate()
}
@Keep
fun setRectLeft(left: Int) = updateRect(rect.apply { this.left = left })
@Keep
fun setRectRight(right: Int) = updateRect(rect.apply { this.right = right })
@Keep
fun setRectTop(top: Int) = updateRect(rect.apply { this.top = top })
@Keep
fun setRectBottom(bottom: Int) = updateRect(rect.apply { this.bottom = bottom })
}
将一些属性放入attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BottomNavigationViewIndicator">
<attr name="targetBottomNavigation" format="reference"/>
<attr name="clippableBackground" format="reference|color"/>
</declare-styleable>
</resources>
创建一个在指示器移动时将被剪裁的背景可绘制对象:
my_orange_background.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="24dp"
android:height="24dp"
android:viewportWidth="360"
android:viewportHeight="4">
<path
android:fillType="nonZero"
android:pathData="M0,0h359.899v3.933H0z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="360"
android:endY="2"
android:startX="0"
android:startY="2"
android:type="linear">
<item
android:color="@color/orange"
android:offset="0" />
<item
android:color="@color/orange"
android:offset="0.55" />
</gradient>
</aapt:attr>
</path>
</vector>
创建您的活动/片段布局:
<?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">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="@+id/bottom_nav"
app:layout_constraintTop_toTopOf="parent" />
<com.example.app.view.CustomBottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?attr/colorSurface"
app:labelVisibilityMode="unlabeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/menu_bottom_nav" />
<com.example.app.view.BottomNavigationViewIndicator
android:layout_width="0dp"
android:layout_height="4dp"
app:clippableBackground="@drawable/my_orange_background"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/bottom_nav"
app:targetBottomNavigation="@+id/bottom_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
来源:Medium articleGitHub project