仿小米应用商店底部导航栏(一)

2,476 阅读3分钟

前言

先看看小米的效果,没有录屏,两张图凑合看一下(因为录屏、转化、压缩太麻烦了),主要特点是点击后相应的位置会凸起,并伴随着一个动画,

image.png

image.png

下面是仿照效果。

录屏_选择区域_20211010145925.gif

贝塞尔曲线

主要还是通过二次贝塞尔曲线曲线完成的,如下图,p0和p2是起点和终点,p1是个控制点,通过这三个参数就可以绘制一个半圆,上面慢慢凸起的效果其实也就是利用valueAnimator不停更改p1的y坐标。

其余就是绘制图标和文字。

201603282025087391.gif

阴影绘制

小米商店还有个阴影效果,这里是通过setShadowLayer来绘制的,但是要注意的是,如果在绘制矩形的时候,y为0,那么这个阴影其实是被裁剪调的,除非在父布局中增加clipChildren=false,否则y要设置的稍微大一点,这个值刚好能显示出所有阴影即可。

   var paint = Paint(Paint.ANTI_ALIAS_FLAG);
   paint.setShadowLayer(25f, -10f, 2f, Color.parseColor("#BDBFBEBE"));
   paint.setColor(Color.WHITE)
   canvas.drawRect(
       0f,
       BODY_MARGIN_TOP,
       width.toFloat() ,
       measuredHeight.toFloat(),
       paint
   );

quadTo

quadTo有四个参数,第1、2是控制点的坐标,3、4是终点的坐标,而起点的坐标是通过moveTo来设置的。

接下来就是计算各个item中间坐标x,有了中间坐标x,其他的都好计算。

我是这样算的,因为这里有5个item,屏幕宽/5在我手机上是216,所以每个item的宽就是216,而凸起的位置位于每个item的中间,所以每个item的贝塞尔曲线的控制点x就是取各个item的开始x和结束x的中间数,比如第一个项起点x是0,终点x是216,取中间数的办法是start + (end - start) / 2,结果是108。

而起点x和终点x就是在这个中间数上减/加一个固定的大小,y都是不变的。

所以如第一个item的所有坐标是

起点x=108-圆大小,起点y=固定值如40。.
终点x=108+圆大小,终点y=固定值如40。.
控制点x=108,控制点y=不停变化。

完整代码


@RequiresApi(Build.VERSION_CODES.P)
class BottomNavigationView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    val NAVAGATION_TEXT = arrayListOf<String>("首页", "娱乐", "汇率", "日期", "我的")
    val NAVAGATION_ICON = arrayListOf<Int>(
        R.drawable.ic_home,
        R.drawable.ic_game,
        R.drawable.ic_money,
        R.drawable.ic_data,
        R.drawable.ic_me
    )
    val BODY_MARGIN_TOP: Float = 40f
    var ICON_MAX_TOP = 45f
    var ICON_DEFAULT_TOP = 60f

    var CIRCLE_SIZE: Float = 55f
    var currentIndex: Int = 0;

    var bezierMaxHeight: Float = -15f
    var bezierHeightMap =
        mutableMapOf(
            0 to BODY_MARGIN_TOP,
            1 to BODY_MARGIN_TOP,
            2 to BODY_MARGIN_TOP,
            3 to BODY_MARGIN_TOP,
            4 to BODY_MARGIN_TOP
        );
    var itemWidth = 0
    var iconHeightMap = mutableMapOf(
        0 to ICON_DEFAULT_TOP,
        1 to ICON_DEFAULT_TOP,
        2 to ICON_DEFAULT_TOP,
        3 to ICON_DEFAULT_TOP,
        4 to ICON_DEFAULT_TOP
    );

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        itemWidth = measuredWidth / 5;
    }

    init {
        showTabForIndex(0)
    }

    fun loadBitmap(id: Int): Bitmap {
        var options = BitmapFactory.Options()
        val dm = resources.displayMetrics
        options.inDensity = dm.densityDpi
        return BitmapFactory.decodeResource(resources, id, options)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        var paint = Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setShadowLayer(25f, -10f, 2f, Color.parseColor("#BDBFBEBE"));
        paint.setColor(Color.WHITE)
        canvas.drawRect(
            0f,
            BODY_MARGIN_TOP,
            width.toFloat() ,
            measuredHeight.toFloat(),
            paint
        );

        for (i in 0..4) {
            drawIcon(loadBitmap(NAVAGATION_ICON[i]), i, canvas);
            drawText(NAVAGATION_TEXT[i], i, canvas)

            var itemRect = getItemRect(i)
            var path = Path()
            path.moveTo(itemRect.mid - CIRCLE_SIZE, BODY_MARGIN_TOP)
            path.quadTo(
                itemRect.mid.toFloat(),
                bezierHeightMap[i]!!.toFloat(),
                itemRect.mid + CIRCLE_SIZE,
                BODY_MARGIN_TOP
            )
            canvas.drawPath(path, Paint().apply { color = Color.WHITE })
        }


    }

    fun drawText(text: String, index: Int, canvas: Canvas) {
        var itemRect = getItemRect(index)
        var paint = Paint()
        paint.textSize = 30f
        paint.color = Color.BLACK
        var measureText = paint.measureText(text);
        canvas.drawText(text, itemRect.mid - measureText / 2, measuredHeight - 25f, paint)
    }

    fun drawIcon(bitmap: Bitmap, index: Int, canvas: Canvas) {
        var width = bitmap.width
        var itemRect = getItemRect(index)
        canvas.drawBitmap(
            bitmap,
            (itemRect.mid - width / 2).toFloat(),
            iconHeightMap[index]!!,
            Paint()
        )
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            var index = floor(event.x / itemWidth);
            showTabForIndex(index.toInt());
        }
        return true
    }

    fun getItemRect(index: Int): ItemRect {
        var start: Int = itemWidth * index;
        var end: Int = (itemWidth * index) + itemWidth;
        var mid = start + (end - start) / 2
        return ItemRect(start, end, mid)
    }

    fun showTabForIndex(index: Int) {
        var start = bezierHeightMap[index]!!.toFloat()
        var end = bezierMaxHeight;
        var valueAnimator = ValueAnimator.ofFloat(start, end)

        valueAnimator.addUpdateListener {
            var fl = it.animatedValue as Float
            bezierHeightMap[currentIndex] = (bezierMaxHeight + BODY_MARGIN_TOP - fl)
            bezierHeightMap[index] = fl
            var progress = 1 - (fl + ((abs(min(start, end))))) / (abs(start) + abs(end));

            iconHeightMap[currentIndex] =
                ICON_MAX_TOP + progress * (ICON_DEFAULT_TOP - ICON_MAX_TOP)
            iconHeightMap[index] = ICON_DEFAULT_TOP - progress * (ICON_DEFAULT_TOP - ICON_MAX_TOP)
            invalidate()
        }
        valueAnimator.addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                currentIndex = index
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationRepeat(animation: Animator?) {
            }
        })
        valueAnimator.start()
    }

    inner class ItemRect(var start: Int, var end: Int, var mid: Int)
}
<?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="#f5f5f5"
    tools:context=".MainActivity">

    <com.hxl.kotlindemo.BottomNavagationView
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="true"
        android:layout_width="match_parent"
        android:layout_height="70dp">

    </com.hxl.kotlindemo.BottomNavagationView>
</RelativeLayout>

第二章内容

juejin.cn/post/701801…