前言
先看看小米的效果,没有录屏,两张图凑合看一下(因为录屏、转化、压缩太麻烦了),主要特点是点击后相应的位置会凸起,并伴随着一个动画,
下面是仿照效果。
贝塞尔曲线
主要还是通过二次贝塞尔曲线曲线完成的,如下图,p0和p2是起点和终点,p1是个控制点,通过这三个参数就可以绘制一个半圆,上面慢慢凸起的效果其实也就是利用valueAnimator不停更改p1的y坐标。
其余就是绘制图标和文字。
阴影绘制
小米商店还有个阴影效果,这里是通过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>
第二章内容