这个是在项目中运用的自定义View的第一篇,按照字母序,第一篇首先先讲BezierBottomBar。这个控件是我从郭霖公众号之前的一篇推送上学习来的,所以有些代码是照搬的之前那篇推送,这篇文章也有很多地方直接引用了这篇推送的一些内容,下面是那篇的推送地址
先看效果
这个是结合了ViewPager后的效果。
这个控件总共分为三个部分,可以把这部分看做是一个VVM模式
- 控件本体——BezierBottomBarView
- 控制View行为的——BezierBottomBarControl
- 定制的ViewPager
BezierBottomBarView
测量View大小
由于我们是做的是一个底边栏,所以我们要在onMeasure中设定最大高度,以防止控件占高度太高。所以在getHeight方法中,将控件最大高度设置为屏幕的1/8.
private int getHeight(int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
DisplayMetrics dm = getResources().getDisplayMetrics();
int height = (heightSize > dm.heightPixels) ? heightSize : dm.heightPixels;
return (height / 8);
}
对于单个圆的存放
在那篇推送中作者是将这个控件写成了ViewGroup+ImgView的形式,但是在我们的应用中我们想要加入手势控制动画,所以如果仍然使用作者的思路就会导致动画实现起来相对来说比较的麻烦,故而我改用纯View进行绘制。
所以我们就需要一类数据来表示单个圆的各种参数,在这里我将get和set方法省略,如果有需要可以去查看源码。
public static class BarTag {
private int icon;
private String tag;
private int color;
private float centerX;
private float centerY;
private float radius;
private RectF rectF;
private RectF dst;
private RectF tagRectF;
}
在这里预留了tag和图标的color方法,在里面并没有使用,如果未来有需要的话可以自行添加。这个类中主要持有的方法就是icon,radius和dst,dst表示圆的位置。
确定圆的摆放位置
在原文中,确定摆放位置是在onLayout中设置的,但是原文是将控件当做一个ViewGroup去写的,我在这里却是将其当做View去写,所以我选择在onSizeChanged中去确定圆的位置,并且存储起来,在之后的onDraw方法后用canvas去绘制外面的圆形。
在这里有一点补充:
继承与View和继承与现有控件都是下面的顺序,但是控件的大小是生成之后就固定的,不会再次改变。
onMeasure()→onSizeChanged()→onLayout()→onMeasure()→onLayout()→onDraw()
所以在onSizeChanged中我们可以这么确定一个圆的位置,interval表示两个圆之间的间隔。
interval = (width - 2 * tabNum * radius) / (tabNum + 1);
for (int i = 0; i < tabNum; i++) {
float cx = interval + radius + i * (interval + 2 * radius);
RectF rectF = new RectF(cx - radius, startY - radius, cx + radius, startY + radius);
barTags.get(i).setRectF(rectF);
RectF dst = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY - scale * radius / g2),
(int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY + scale * radius / g2));
barTags.get(i).setDst(dst);
barTags.get(i).setRadius(radius);
}
在onSizeChanged方法中计算并且将圆的位置进行存储
对静止圆的绘制
在计算完各个圆的位置之后,我们就可以在onDraw方法中进行绘制。可以看到,如果当前圆被选中,那么就会有一个颜色填充。
所以大概就是这样
for (int i = 0; i < tabNum; i++) {
float cx = barTags.get(i).centerX;
float cy = barTags.get(i).centerY;
canvas.drawCircle(cx, cy, radius, linePaint);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), barTags.get(i).icon);
canvas.drawBitmap(bitmap, null, barTags.get(i).dst, fillPaint);
}
动画的绘制
(有关贝塞尔曲线及其内容请直接参考开始给出的博客,我觉得讲的不错)
我们可以仔细 的观察到,在这个动画当中,中间移动的圆分成了6个状态
这里是起始的三个状态,分别是圆,开始向左移动,向左移动一定距离,还欠缺的是准备结束向左移动,向左移动的过量和向左移动的回弹
这里只能说是三个状态
那么我们可以假设动画时间currentTime(0~1),所以我们可以将整个移动状态分为以下几个区间段:
-
状态1,圆
currentTime = 0 -
状态2,即向左移动
0 < currentTime <= 0.2 -
状态3,即开始进入中间状态
0.2 < currentTime <= 0.5 -
状态4,即准备结束这段运动,是状态2的镜面对称
0.5 < currentTime <= 0.8 -
状态5,结束这段运动时的回弹开始
0.8 < currentTime <= 0.9 -
状态6,结束这段运动时候的回弹结束
0.9 < currentTime < 1 -
状态1,圆
currentTime = 1
由之前那篇博客我们可以知道,使用二阶贝塞尔曲线去画一个圆,受制于p1,p2,p3,p4四个点,所以我们就可以通过改变这四个点的参数去改变这个圆的参数
这张图是原博客中的一张图,我就直接拿来用了
那么我们可以将这六个部分的代码变成这个样子
- 状态1
if (currentTime == 0) {
resetP();
canvas.drawCircle(interval + radius + (currentPos) * (interval + 2 * radius), startY, 0, clickPaint);
fillPaint.setColor(startColor);
canvas.translate(startX, startY);
if (toPos > currentPos) {
p2.setX(radius);
} else {
p4.setX(-radius);
}
}
- 状态2
if (currentTime > 0 && currentTime <= 0.2) {
direction = toPos > currentPos ? true : false;
if (animating) {
canvas.drawCircle(interval + radius + (toPos) * (interval + 2 * radius),
startY,
radius * 1.0f * 5 * currentTime,
clickPaint);
}
canvas.translate(startX, startY);
if (toPos > currentPos) {
p2.setX(radius + 2 * 5 * currentTime * radius / 2);
} else {
p4.setX(-radius - 2 * 5 * currentTime * radius / 2);
}
}
- 状态3
if (currentTime > 0.2 && currentTime <= 0.5) {
float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
canvas.translate(cx, startY);
if (toPos > currentPos) {
p1.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);
p2.setX(2 * radius);
p3.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);
p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
} else {
p1.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
p3.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
p4.setX(-2 * radius);
p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
}
}
- 状态4
if (currentTime > 0.5 && currentTime <= 0.8) {
float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
canvas.translate(cx, startY);
if (toPos > currentPos) {
p1.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p3.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
} else {
p1.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p3.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
}
}
- 状态5
if (currentTime > 0.8 && currentTime <= 0.9) {
p2.setMc(mc);
p4.setMc(mc);
float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
canvas.translate(cx, startY);
if (toPos > currentPos) {
p4.setX(-radius + 1.6f * radius * (currentTime - 0.8f) / 0.1f);
} else {
p2.setX(radius - 1.6f * radius * (currentTime - 0.8f) / 0.1f);
}
}
- 状态6
if (currentTime > 0.9 && currentTime < 1) {
if (toPos > currentPos) {
p1.setX(radius);
p3.setX(radius);
canvas.translate(startX + distance, startY);
p4.setX(0.6f * radius - 0.6f * radius * (currentTime - 0.9f) / 0.1f);
} else {
p1.setX(-radius);
p3.setX(-radius);
canvas.translate(startX + distance, startY);
p2.setX(-0.6f * radius + 0.6f * radius * (currentTime - 0.9f) / 0.1f);
}
}
View的hide和show
show
可以看到,这个show的动画有一个稍微过一些然后再回弹的效果(虽然可能真的不太明显)并且圆是依次上升的,所以就需要让它有一个上升的次序。
我们可以看到,在sin图像中,在顶点下任取一个y值,都会有两个x值使得sin(x) = y(→_→好像讲的有点啰嗦)那么我们亦可以通过给不同的圆设置不同的初始值,来实现阶梯式上升的效果。
void show() {
if (!valueRunning && !hideRunning) {
float cy = (startY + radius) / (float) Math.sin(Math.toRadians(angle));
for (int i = 0; i < tabNum; i++) {
hideHeight = cy + startY / (float) Math.sin(Math.toRadians(angle));
angles[i] = i * (-10);
dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY - scale * radius / g2) + cy,
(int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY + scale * radius / g2) + cy);
}
animState = AnimState.Show;
handler.postDelayed(showRunnable, showTime);
}
}
hide
hide方法和show方法相同,也需要几个圆依次向下。所以思路也同show——赋予几个圆不同的负向初始量,然后在handle中对其进行改变
void hide() {
if (!valueRunning && !showRunning) {
hideSpeed = (startY + radius + (tabNum - 1) * (radius * 2 / tabNum)) / hideTime;
for (int i = 0; i < tabNum; i++) {
changeHeight[i] = i * (-10);
dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY - scale * radius / g2),
(int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY + scale * radius / g2));
}
animState = AnimState.Hide;
handler.postDelayed(hideRunnable, hideTime);
}
}
控件的单击事件
在这个控件中,我们有两个手势操作,以及单击操作。但是手势操作我们需要在整个屏幕,也就是在Activity中去操作这个全局手势,所以我们就只需要在控件的onTouchEvent中处理单击操作即可。
由于控件有show和hide两种状态,所以我们只需要让其在show的时候处理Event即可,在hide的时候我们可以选择无视。
所以在这部分就可以这么去处理TouchEvent
if (x > interval + 2 * radius && x < (interval + 2 * radius) * tabNum) {
if (animator != null) {
animator.cancel();
}
int toPos = (int) (x / (interval + 2 * radius));
if (toPos != currentPos && toPos <= tabNum) {
startAniTo(currentPos, toPos);
}
} else if (x > interval && x < interval + 2 * radius) {
if (animator != null) {
animator.cancel();
}
if (currentPos != 0)
startAniTo(currentPos, 0);
}
我们可以看到,在这里就只需要判断单击的x,y值即可。
BezierBottomBarControl
由于需要在全局设置一个手势操作,并且需要在一定时间过后对BottomBar进行一个隐藏,所以需要一个ViewControl来统一的对View的状态进行操作。
设置手势操作
由于这个控件在App中仅存在于MainActivity中,所以只需要在MainActivity的onTouchEvent中将MotionEvent传入control就可以了。在Control中对其进行处理。由于我们并不需要实时的使得控件对于TouchEvent进行反馈,所以我们只需获取到Down和Up的坐标即可。
public void setTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (lastX == 0) {
lastX = viewPager.getLastX();
}
if (lastY == 0) {
lastY = viewPager.getLastY();
}
float deltaY = lastY - y;
if (deltaY > 0) {
if (bottomBar.getState() == BezierBottomBarView.AnimState.Hide) {
show();
}
}
if (deltaY < 0) {
if (bottomBar.getState() != BezierBottomBarView.AnimState.Hide) {
hide();
}
}
break;
}
}
show和hide
在control中起了一个定时器,当控件显示一段时间过后,就会自动调用hide方法,使得控件进行隐藏。
setViewPagerListener
由于我们的控件可以和ViewPager进行一个联动,在Control的构造方法中已经将ViewPager的实例传入,所以我们只需要设置ViewPager的Listener即可。
网友评论