這個是在項目中運用的自定義View的第一篇,按照字母序,第一篇首先先講BezierBottomBar。這個控件是我從郭霖公眾號之前的一篇推送上學(xué)習(xí)來的,所以有些代碼是照搬的之前那篇推送,這篇文章也有很多地方直接引用了這篇推送的一些內(nèi)容,下面是那篇的推送地址
先看效果

這個是結(jié)合了ViewPager后的效果。
這個控件總共分為三個部分,可以把這部分看做是一個VVM模式
- 控件本體——BezierBottomBarView
- 控制View行為的——BezierBottomBarControl
- 定制的ViewPager
BezierBottomBarView
測量View大小
由于我們是做的是一個底邊欄,所以我們要在onMeasure中設(shè)定最大高度,以防止控件占高度太高。所以在getHeight方法中,將控件最大高度設(shè)置為屏幕的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的形式,但是在我們的應(yīng)用中我們想要加入手勢控制動畫,所以如果仍然使用作者的思路就會導(dǎo)致動畫實現(xiàn)起來相對來說比較的麻煩,故而我改用純View進(jìn)行繪制。
所以我們就需要一類數(shù)據(jù)來表示單個圓的各種參數(shù),在這里我將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;
}
在這里預(yù)留了tag和圖標(biāo)的color方法,在里面并沒有使用,如果未來有需要的話可以自行添加。這個類中主要持有的方法就是icon,radius和dst,dst表示圓的位置。
確定圓的擺放位置
在原文中,確定擺放位置是在onLayout中設(shè)置的,但是原文是將控件當(dāng)做一個ViewGroup去寫的,我在這里卻是將其當(dāng)做View去寫,所以我選擇在onSizeChanged中去確定圓的位置,并且存儲起來,在之后的onDraw方法后用canvas去繪制外面的圓形。
在這里有一點補(bǔ)充:
繼承與View和繼承與現(xiàn)有控件都是下面的順序,但是控件的大小是生成之后就固定的,不會再次改變。
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方法中計算并且將圓的位置進(jìn)行存儲
對靜止圓的繪制
在計算完各個圓的位置之后,我們就可以在onDraw方法中進(jìn)行繪制??梢钥吹?,如果當(dāng)前圓被選中,那么就會有一個顏色填充。
所以大概就是這樣
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);
}
動畫的繪制
(有關(guān)貝塞爾曲線及其內(nèi)容請直接參考開始給出的博客,我覺得講的不錯)
我們可以仔細(xì) 的觀察到,在這個動畫當(dāng)中,中間移動的圓分成了6個狀態(tài)
這里是起始的三個狀態(tài),分別是圓,開始向左移動,向左移動一定距離,還欠缺的是準(zhǔn)備結(jié)束向左移動,向左移動的過量和向左移動的回彈

那么我們可以假設(shè)動畫時間currentTime(0~1),所以我們可以將整個移動狀態(tài)分為以下幾個區(qū)間段:
-
狀態(tài)1,圓
currentTime = 0 -
狀態(tài)2,即向左移動
0 < currentTime <= 0.2 -
狀態(tài)3,即開始進(jìn)入中間狀態(tài)
0.2 < currentTime <= 0.5 -
狀態(tài)4,即準(zhǔn)備結(jié)束這段運動,是狀態(tài)2的鏡面對稱
0.5 < currentTime <= 0.8 -
狀態(tài)5,結(jié)束這段運動時的回彈開始
0.8 < currentTime <= 0.9 -
狀態(tài)6,結(jié)束這段運動時候的回彈結(jié)束
0.9 < currentTime < 1 -
狀態(tài)1,圓
currentTime = 1
由之前那篇博客我們可以知道,使用二階貝塞爾曲線去畫一個圓,受制于p1,p2,p3,p4四個點,所以我們就可以通過改變這四個點的參數(shù)去改變這個圓的參數(shù)

那么我們可以將這六個部分的代碼變成這個樣子
- 狀態(tài)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);
}
}
- 狀態(tài)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);
}
}
- 狀態(tài)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);
}
}
- 狀態(tài)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);
}
}
- 狀態(tài)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);
}
}
- 狀態(tài)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(→_→好像講的有點啰嗦)那么我們亦可以通過給不同的圓設(shè)置不同的初始值,來實現(xiàn)階梯式上升的效果。
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——賦予幾個圓不同的負(fù)向初始量,然后在handle中對其進(jìn)行改變
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兩種狀態(tài),所以我們只需要讓其在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
由于需要在全局設(shè)置一個手勢操作,并且需要在一定時間過后對BottomBar進(jìn)行一個隱藏,所以需要一個ViewControl來統(tǒng)一的對View的狀態(tài)進(jìn)行操作。
設(shè)置手勢操作
由于這個控件在App中僅存在于MainActivity中,所以只需要在MainActivity的onTouchEvent中將MotionEvent傳入control就可以了。在Control中對其進(jìn)行處理。由于我們并不需要實時的使得控件對于TouchEvent進(jìn)行反饋,所以我們只需獲取到Down和Up的坐標(biāo)即可。
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中起了一個定時器,當(dāng)控件顯示一段時間過后,就會自動調(diào)用hide方法,使得控件進(jìn)行隱藏。
setViewPagerListener
由于我們的控件可以和ViewPager進(jìn)行一個聯(lián)動,在Control的構(gòu)造方法中已經(jīng)將ViewPager的實例傳入,所以我們只需要設(shè)置ViewPager的Listener即可。