晨鳴的博客--神奇的水滴效果導航欄-BezierIndicator
很早之前就看見過這樣一個特效
心怡很久,卻一直恐于自定義View這座大山。最近在突擊自定義View的技能,學習貝塞爾曲線的繪制,前面搞了個很簡單的MagicButton,甚是興奮?? 所以斗膽來試試看實現這個特效。
分析
找了半天終于找到當初看見的這個特效的原博客 --三次貝塞爾曲線練習之彈性的圓
另外在評論中發現竟然有人已經實現了這個自定義View了--自定義View之炫酷的水滴ViewPageIndicator,效果很不錯,借鑒之??
關于最核心的貝塞爾小球動效的繪制,博主進行了很詳細的解析及描述,并且提供了一個demo,萬分感謝??
這里簡單回顧一下這個小球的繪制過程:
為了控制小球的不同形態,我們這里使用三階貝塞爾曲線cubicTo
來繪制小球。
而小球一共可以分成5個狀態來繪制
狀態1
狀態2
狀態3
狀態4
狀態5
繪制
計算控件寬高
作為一個導航控件,我暫時不考慮寬度設置為warp_content
的狀態,設置wrap_content
一律計算為屏幕的最大寬高.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
WindowManager wm = (WindowManager) getContext()
.getSystemService(Context.WINDOW_SERVICE);
/**
* 獲得此ViewGroup上級容器為其推薦的寬和高,以及計算模式
*/
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
width = sizeWidth;
} else {
width = wm.getDefaultDisplay().getWidth();
}
if (heightMode == MeasureSpec.EXACTLY) {
height = sizeHeight;
} else {
height = wm.getDefaultDisplay().getHeight();
}
if (getChildCount() != 0) {
childSideLength = (width - getPaddingRight() - getPaddingLeft()) / getChildCount() > height - getPaddingBottom() - getPaddingTop() ? height - getPaddingBottom() - getPaddingTop() : (width - getPaddingLeft() - getPaddingRight()) / getChildCount();
// //計算出所有的ChildView的寬和高
// measureChildren(widthMeasureSpec, heightMeasureSpec);
bezierCircular = new BezierCircular(childSideLength / 2);
}
setMeasuredDimension(width, height);
}
計算子控件的位置
為了方便管理,子View的大小統一計算為一個正方形區域,設置一個子View的padding值childPadding
,可以通過childPadding
值控制我們添加的子view呈現出的大小,也就是效果圖中小圖標在白色圓環中的大小。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
if (childCount == 0) {
return;
}
//相鄰兩個子View中心點的間距
float childDis = (width - getPaddingLeft() - getPaddingRight() - 2 * defaultLeftRightGap - childSideLength) / (childCount - 1);
float cWidth = childSideLength - 2 * childPadding;
float cHeight = cWidth;
anchorList.clear();
//計算子控件的位置,強制將子View控制繪制在均分的幾個錨點上
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
PointF anchorPoint = new PointF((childDis * i + defaultLeftRightGap + childSideLength / 2 + getPaddingLeft()), getPaddingTop() + childSideLength / 2);
anchorList.add(anchorPoint);
childView.layout((int) (anchorPoint.x - cWidth / 2), (int) (anchorPoint.y - cHeight / 2), (int) (anchorPoint.x + cWidth / 2), (int) (anchorPoint.y + cHeight / 2));
}
PointF pointF = anchorList.get(0);
bezierCircular.setCenter(pointF.x, pointF.y);
bezierCircular.initControlPoint();
}
繪制貝塞爾小球
將貝塞爾小球的一些參數及計算封裝成一個對象BezierCircular
,因為剛開始只是看了原博客的思路就動手了,繪制貝塞爾小球使用了最原始的方法,定義了4個數據點和8個控制點,在進行五個狀態的繪制計算的時候太麻煩了,后面看了博客中的Demo,發現自己的計算太原始笨重了,博客中的demo中關于小球的繪制更加面向對象,更加簡潔。不過既然是原創,還是要貼出自己的代碼,僅供參考??
public class BezierCircular {
private static final String TAG = "BezierCircular";
private static final float C = 0.551915024494f; //常量
//圓中心坐標
float centerX;
float centerY;
//圓半徑
float radius;
private PointF currentPoint;
private PointF targetPoint;
private float mDifference;
private float stretchDistance;
private float cDistance;
private float moveDistance;
private float[] mData = new float[8]; //順時針記錄繪制圓形的四個數據點
private float[] mCtrl = new float[16]; //順時針記錄繪制圓形的八個控制點
public BezierCircular(float radius) {
this.radius = radius;
stretchDistance = radius / 3 * 2;
mDifference = radius * C;
cDistance = mDifference * 0.45f;
}
public void setCenter(float centerX, float centerY) {
this.centerX = centerX;
this.centerY = centerY;
}
public void initControlPoint() {
//初始化數據點
mData[0] = centerX;
mData[1] = centerY + radius;
mData[2] = centerX + radius;
mData[3] = centerY;
mData[4] = centerX;
mData[5] = centerY - radius;
mData[6] = centerX - radius;
mData[7] = centerY;
//初始化控制點
mCtrl[0] = mData[0] + mDifference;
mCtrl[1] = mData[1];
mCtrl[2] = mData[2];
mCtrl[3] = mData[3] + mDifference;
mCtrl[4] = mData[2];
mCtrl[5] = mData[3] - mDifference;
mCtrl[6] = mData[4] + mDifference;
mCtrl[7] = mData[5];
mCtrl[8] = mData[4] - mDifference;
mCtrl[9] = mData[5];
mCtrl[10] = mData[6];
mCtrl[11] = mData[7] - mDifference;
mCtrl[12] = mData[6];
mCtrl[13] = mData[7] + mDifference;
mCtrl[14] = mData[0] - mDifference;
mCtrl[15] = mData[1];
}
public void setCurrentAndTarget(PointF currentPoint, PointF targetPoint) {
this.currentPoint = currentPoint;
this.targetPoint = targetPoint;
float distance = targetPoint.x - currentPoint.x;
moveDistance = distance > 0 ? distance - 2 * stretchDistance : distance + 2 * stretchDistance;
}
public void setProgress(float progress) {
if ((progress > 0 && progress <= 0.2) || (progress < 0 && progress >= -0.2)) {
model1(progress);
} else if ((progress > 0.2 && progress <= 0.5) || (progress < -0.2 && progress >= -0.5)) {
model2(progress);
} else if ((progress > 0.5 && progress <= 0.8) || (progress < -0.5 && progress >= -0.8)) {
model3(progress);
} else if ((progress > 0.8 && progress <= 0.9) || (progress < -0.8 && progress >= -0.9)) {
model4(progress);
} else if ((progress > 0.9 && progress < 1) || (progress < -0.9 && progress > -1)) {
model5(progress);
}
// } else if (progress >= 1 || progress <= -1) {
// Log.i(TAG,"-------------------------------------------");
//// centerX = targetPoint.x;
//// centerY = targetPoint.y;
//// initControlPoint();
// }
}
public void model1(float progress) {
if (progress > 0)
mData[2] = centerX + radius + stretchDistance * progress * 5;
if (progress < 0)
mData[6] = centerX - radius + stretchDistance * progress * 5;
mCtrl[2] = mData[2];
if (progress > 0)
mCtrl[3] = mData[3] + mDifference + cDistance * progress * 5;
mCtrl[4] = mData[2];
if (progress > 0)
mCtrl[5] = mData[3] - mDifference - cDistance * progress * 5;
mCtrl[10] = mData[6];
if (progress < 0)
mCtrl[11] = mData[7] - mDifference + cDistance * progress * 5;
mCtrl[12] = mData[6];
if (progress < 0)
mCtrl[13] = mData[7] + mDifference - cDistance * progress * 5;
}
public void model2(float progress) {
model1(progress > 0 ? 0.2f : -0.2f);
progress = progress > 0 ? (progress - 0.2f) * (10f / 3) : (progress + 0.2f) * (10f / 3);
//初始化數據點
mData[0] = centerX + stretchDistance * progress;
if (progress > 0)
mData[2] = centerX + radius + stretchDistance * (1 + progress);
else
mData[2] = centerX + radius;
mData[4] = centerX + stretchDistance * progress;
if (progress < 0)
mData[6] = centerX - radius - stretchDistance + stretchDistance * progress;
else
mData[6] = centerX - radius;
//初始化控制點
mCtrl[0] = mData[0] + mDifference;
mCtrl[2] = mData[2];
if (progress > 0)
mCtrl[3] = mData[3] + mDifference + cDistance;
else
mCtrl[3] = mData[3] + mDifference - cDistance * progress;
mCtrl[4] = mData[2];
if (progress > 0)
mCtrl[5] = mData[3] - mDifference - cDistance;
else
mCtrl[5] = mData[3] - mDifference + cDistance * progress;
mCtrl[6] = mData[4] + mDifference;
mCtrl[8] = mData[4] - mDifference;
mCtrl[10] = mData[6];
if (progress > 0)
mCtrl[11] = mData[7] - mDifference - cDistance * progress;
else
mCtrl[11] = mData[7] - mDifference - cDistance;
mCtrl[12] = mData[6];
if (progress > 0)
mCtrl[13] = mData[7] + mDifference + cDistance * progress;
else
mCtrl[13] = mData[7] + mDifference + cDistance;
mCtrl[14] = mData[0] - mDifference;
}
public void model3(float progress) {
model2(progress > 0 ? 0.5f : -0.5f);
progress = progress > 0 ? (progress - 0.5f) * (10f / 3) : (progress + 0.5f) * (10f / 3);
//初始化數據點
if (progress > 0)
mData[0] = centerX + moveDistance * progress + stretchDistance;
else
mData[0] = centerX - moveDistance * progress - stretchDistance;
if (progress > 0)
mData[2] = centerX + moveDistance * progress + radius + 2 * stretchDistance;
else
mData[2] = centerX - moveDistance * progress + radius;
if (progress > 0)
mData[4] = centerX + moveDistance * progress + stretchDistance;
else
mData[4] = centerX - moveDistance * progress - stretchDistance;
if (progress > 0)
mData[6] = centerX + moveDistance * progress - radius;
else
mData[6] = centerX - moveDistance * progress - radius - 2 * stretchDistance;
//初始化控制點
mCtrl[0] = mData[0] + mDifference;
mCtrl[2] = mData[2];
mCtrl[3] = mData[3] + mDifference + cDistance;
mCtrl[4] = mData[2];
mCtrl[5] = mData[3] - mDifference - cDistance;
mCtrl[6] = mData[4] + mDifference;
mCtrl[8] = mData[4] - mDifference;
mCtrl[10] = mData[6];
mCtrl[11] = mData[7] - mDifference - cDistance;
mCtrl[12] = mData[6];
mCtrl[13] = mData[7] + mDifference + cDistance;
mCtrl[14] = mData[0] - mDifference;
}
public void model4(float progress) {
model3(progress > 0 ? 0.8f : -0.8f);
progress = progress > 0 ? (progress - 0.8f) * 10 : (progress + 0.8f) * 10;
//初始化數據點
if (progress > 0)
mData[0] = centerX + moveDistance + stretchDistance + stretchDistance * progress;
else
mData[0] = centerX + moveDistance - stretchDistance + stretchDistance * progress;
if (progress > 0)
mData[2] = centerX + moveDistance + radius + 2 * stretchDistance;
else
mData[2] = centerX + moveDistance + radius + stretchDistance * progress;
if (progress > 0)
mData[4] = centerX + moveDistance + stretchDistance + stretchDistance * progress;
else
mData[4] = centerX + moveDistance - stretchDistance + stretchDistance * progress;
if (progress > 0)
mData[6] = centerX + moveDistance - radius + stretchDistance * progress;
else
mData[6] = centerX + moveDistance - radius - 2 * stretchDistance;
//初始化控制點
mCtrl[0] = mData[0] + mDifference;
mCtrl[2] = mData[2];
if (progress > 0)
mCtrl[3] = mData[3] + mDifference + cDistance - cDistance * progress;
else
mCtrl[3] = mData[3] + mDifference + cDistance;
mCtrl[4] = mData[2];
if (progress > 0)
mCtrl[5] = mData[3] - mDifference - cDistance + cDistance * progress;
else
mCtrl[5] = mData[3] - mDifference - cDistance;
mCtrl[6] = mData[4] + mDifference;
mCtrl[8] = mData[4] - mDifference;
mCtrl[10] = mData[6];
if (progress > 0)
mCtrl[11] = mData[7] - mDifference - cDistance;
else
mCtrl[11] = mData[7] - mDifference - cDistance - cDistance * progress;
mCtrl[12] = mData[6];
if (progress > 0)
mCtrl[13] = mData[7] + mDifference + cDistance;
else
mCtrl[13] = mData[7] + mDifference + cDistance + cDistance * progress;
mCtrl[14] = mData[0] - mDifference;
}
public void model5(float progress) {
model4(progress > 0 ? 0.9f : -0.9f);
progress = progress > 0 ? (progress - 0.9f) * 10 : (progress + 0.9f) * 10;
//初始化數據點
if (progress > 0)
mData[0] = centerX + moveDistance + 2 * stretchDistance;
else
mData[0] = centerX + moveDistance - 2 * stretchDistance;
if (progress > 0)
mData[2] = centerX + moveDistance + radius + 2 * stretchDistance;
else
mData[2] = (float) (centerX + moveDistance + radius - stretchDistance - (Math.sin(Math.PI * 3 / 2 * Math.abs(progress) - Math.PI / 2) + 1) * stretchDistance);
if (progress > 0)
mData[4] = centerX + moveDistance + 2 * stretchDistance;
else
mData[4] = centerX + moveDistance - 2 * stretchDistance;
if (progress > 0)
mData[6] = (float) (centerX + moveDistance - radius + stretchDistance + (Math.sin(Math.PI * 3 / 2 * progress - Math.PI / 2) + 1) * stretchDistance);
else
mData[6] = centerX + moveDistance - radius - 2 * stretchDistance;
//初始化控制點
mCtrl[0] = mData[0] + mDifference;
mCtrl[2] = mData[2];
if (progress < 0)
mCtrl[3] = mData[3] + mDifference + cDistance + cDistance * progress;
mCtrl[4] = mData[2];
if (progress < 0)
mCtrl[5] = mData[3] - mDifference - cDistance - cDistance * progress;
mCtrl[6] = mData[4] + mDifference;
mCtrl[8] = mData[4] - mDifference;
mCtrl[10] = mData[6];
if (progress > 0)
mCtrl[11] = mData[7] - mDifference - cDistance + cDistance * progress;
mCtrl[12] = mData[6];
if (progress > 0)
mCtrl[13] = mData[7] + mDifference + cDistance - cDistance * progress;
mCtrl[14] = mData[0] - mDifference;
}
public void drawCircle(Canvas canvas, Paint mPaint) {
Path path = new Path();
path.moveTo(mData[0], mData[1]);
path.cubicTo(mCtrl[0], mCtrl[1], mCtrl[2], mCtrl[3], mData[2], mData[3]);
path.cubicTo(mCtrl[4], mCtrl[5], mCtrl[6], mCtrl[7], mData[4], mData[5]);
path.cubicTo(mCtrl[8], mCtrl[9], mCtrl[10], mCtrl[11], mData[6], mData[7]);
path.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15], mData[0], mData[1]);
canvas.drawPath(path, mPaint);
}
public void resetCircular(PointF pointF) {
setCenter(pointF.x, pointF.y);
initControlPoint();
}
}
確定子View點擊位置
通過OnTouchEvent 方法計算觸摸點在哪個子View的繪制范圍內,確定點擊位置
float touchX = 0;
float touchY = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchX = event.getX();
touchY = event.getY();
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "touchX: " + touchX + " touchY: " + touchY);
for (int i = 0; i < anchorList.size(); i++) {
PointF pointF = anchorList.get(i);
if (touchX > (pointF.x - childSideLength / 2) && touchX < (pointF.x + childSideLength / 2) && touchY > (pointF.y - childSideLength / 2) && touchY < (pointF.y + childSideLength / 2)) {
onClickIndex(i);
}
}
break;
}
return true;
}
private void onClickIndex(int position) {
if (!isAnimatorStart && !isViewPagerScoll && position != currentPosition) {
targetPosition = position;
isAnimatorStart = true;
startAnimator(); //開始動畫
clickAnimator(); //點擊效果
if (viewPager != null) {
viewPager.setCurrentItem(position);
}
// currentPosition = position;
Log.i(TAG, "點擊了第 " + position + " 項!");
}
}
點擊切換動畫
通過ValueAnimator
動態更改貝塞爾小球的繪制進度
/**
* 切換動畫
*/
private void startAnimator() {
bezierCircular.setCurrentAndTarget(anchorList.get(currentPosition), anchorList.get(targetPosition));
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, targetPosition > currentPosition ? 1 : -1);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
bezierCircular.setProgress((Float) animation.getAnimatedValue());
bezierPaint.setColor(circularColors.size() > 0 ? setCircularColor(Math.abs((Float) animation.getAnimatedValue())) : circularColor);
postInvalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
currentPosition = targetPosition;
bezierPaint.setColor(circularColors.size() > 0 ? circularColors.get(currentPosition) : circularColor);
bezierCircular.resetCircular(anchorList.get(currentPosition));
isAnimatorStart = false;
postInvalidate();
super.onAnimationEnd(animation);
}
});
int count = Math.abs(targetPosition - currentPosition);
if (count == 0) {
return;
}
int duration = 600;
valueAnimator.setDuration(duration);
valueAnimator.start();
}
與ViewPager聯動
與ViewPager的聯動這一塊挺頭疼的,ViewPager滾動過程中設置滑動監聽 void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
回調方法中的 positionOffset 參數,在從左往右滑是0~1逐漸增大,但是最后又會突變到0。而且 void onPageSelected(int position)
回調方法并不是在ViewPager滑動結束的時候調用,而是在你的手指離開時調用,有可能ViewPager還在慣性滑動的時候void onPageSelected(int position)
方法已經調用了,所以也沒辦法通過這個回調來確定 currentPositon
與targetPosition
。
通過觀察,ViewPager的滑動監聽 void onPageScrollStateChanged(int state)
回調方法中有三個狀態
- state == 1 表示正在滑動
- state == 2 表示滑動結束
- state == 0 表示什么都沒有做
這里的滑動指的是手指在屏幕上的滑動,而當ViewPager慣性滑動結束時 state == 0,所以最后決定在void onPageScrollStateChanged(int state)
方法中進行相關處理。
public void setViewPager(ViewPager viewPager) {
this.viewPager = viewPager;
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (anchorList != null && anchorList.size() > 0 && !isAnimatorStart) {
isViewPagerScoll = true;
updateDrop(position, positionOffset, positionOffsetPixels);
}
// 頁面正在滾動時不斷調用
Log.d(TAG, "onPageScrolled————>" + " position:" + position + " positionOffest:" + positionOffset + " positionOffsetPixels:" + positionOffsetPixels);
}
@Override
public void onPageSelected(int position) {
Log.e(TAG, "onPagerSelected————> position:" + position);
isSelected = true;
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == 0 && isSelected && !isAnimatorStart) {
// Log.e(TAG, "onPageScrollStateChanged————> 設置狀態:");
isSelected = false;
isViewPagerScoll = false;
bezierCircular.setProgress(direction ? 1.0f : -1.0f);
currentPosition = targetPosition;
// Log.i(TAG, "currentPosition::::" + currentPosition);
bezierPaint.setColor(circularColors.size() > 0 ? circularColors.get(currentPosition) : circularColor);
bezierCircular.resetCircular(anchorList.get(currentPosition));
postInvalidate();
}
Log.i(TAG, "onPageScrollStateChanged————> state:" + state);
}
});
}
float lastProgress = 0;
float currentProgress = 0;
//滑動ViewPager時更新指示器的動畫
private void updateDrop(int position, float positionOffset, int positionOffsetPixels) {
if ((position + positionOffset) - currentPosition > 0) {
direction = true;
} else if ((position + positionOffset) - currentPosition < 0) {
direction = false;
}
//防止數組越界
if ((!direction && currentPosition - 1 < 0) || (direction && currentPosition + 1 > getChildCount() - 1)) {
return;
}
if (direction) targetPosition = currentPosition + 1;
else targetPosition = currentPosition - 1;
currentProgress = positionOffset;
// Log.e(TAG, "direction:::" + direction + " currentPosition:::" + currentPosition + " targetPosition:::" + targetPosition);
bezierCircular.setCurrentAndTarget(anchorList.get(currentPosition), anchorList.get(targetPosition));
if (currentProgress == 0 && lastProgress > 0.9) {
if (lastProgress > 0.9) {
currentProgress = 1;
}
if (lastProgress < 0.1) {
currentProgress = 0;
}
}
bezierCircular.setProgress(direction ? currentProgress : currentProgress - 1);
bezierPaint.setColor(circularColors.size() > 0 ? setCircularColor(direction ? currentProgress : 1 - currentProgress) : circularColor);
invalidate();
lastProgress = currentProgress;
}
onDraw(Canvas canvas)
onDraw方法中代碼就很少了
@Override
protected void onDraw(Canvas canvas) {
drawChildBg(canvas);
bezierCircular.drawCircle(canvas, bezierPaint);
drawClick(canvas);
super.onDraw(canvas);
}
附上子View背景繪制,及點擊效果繪制代碼
//繪制子View的背景
private void drawChildBg(Canvas canvas) {
if (anchorList == null || anchorList.size() == 0) {
Log.i(TAG, "錨點位置為空");
return;
}
for (int i = 0; i < anchorList.size(); i++) {
PointF pointF = anchorList.get(i);
canvas.drawCircle(pointF.x, pointF.y, (childSideLength - 4) / 2, childBgPaint);
}
}
//繪制點擊效果
private void drawClick(Canvas canvas) {
PointF pointF = anchorList.get(targetPosition);
canvas.drawCircle(pointF.x, pointF.y, clickRadius, clickPaint);
}
效果
最終效果如下,可能與原概念圖有些差距,但也算小有成就吧??
附上github地址:https://github.com/lichenming0516/BezierIndicator
小結
通過這兩次自定義View的學習嘗試,讓自己對自定義View的繪制流程有了更深刻的了解,一些常見方法onMeasure()
、onLayout()
、onDraw()
以及自定義屬性的解析理解的更清晰一點。對于自定義View這座大山應該能算的上爬上半山腰了吧 ??