按照慣例,反手就是一個超連接: github地址git
本文要實現的View效果以下圖:程序員
從效果圖容易看出,圖中的功能主要分爲兩個部分:github
不難發現左側動畫效果主要由三部分組成:canvas
拇指的縮放各位客觀想必也是心中有數的,無非就是兩種方式:api
// 處理拇指縮放效果
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
move = event.getY();
animate().scaleY(0.8f).scaleX(0.8f).start();
break;
case MotionEvent.ACTION_UP:
getHandler().postDelayed(new Runnable() {
@Override
public void run() {
animate().cancel();
setScaleX(1);
setScaleY(1);
}
}, 300);
...
// 省略無關代碼
break;
}
return super.onTouchEvent(event);
}
複製代碼
#### 3.1.1 圓圈擴散 沒錯,就是畫圈圈。一樣,仔細的同志應該已經發現了些什麼,冥冥之中彷佛有些什麼不可告人的祕密。 是的,這裏有兩個須要注意的地方:bash
// 測量View寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
switch (widthSpecMode) {
...
case MeasureSpec.AT_MOST:
widthMeasureSpec = mDrawable.getIntrinsicWidth();
break;
...
}
switch (heightSpecMode) {
...
// wrap_content
case MeasureSpec.AT_MOST:
heightMeasureSpec = mDrawable.getIntrinsicHeight();
break;
...
}
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
initDrawable(mDrawable, widthMeasureSpec, heightMeasureSpec);
initPointFs(1.3f);
}
// drawable的大小爲view的0.6
private void initDrawable(Drawable drawable, int width, int height) {
mCircleCenter.x = width / 2f;
mCircleCenter.y = height / 2;
mDrawable = drawable;
// drawable的邊長爲view的0.6
float diameter = (float) ((width > height ? height : width) * 0.6);
int left = (int) ((width - diameter)/2);
int top = (int)(height - diameter)/2;
int right = (int) (left + diameter);
int bottom = (int) (top + diameter);
Rect drawableRect = new Rect(left, top, right, bottom);
mDrawable.setBounds(drawableRect);
requestLayout();
}
複製代碼
由此計算出了view和drawable的大小,從而能夠去畫他了。這樣咱們就確認了圈圈該畫在哪裏,接下來的擴散效果,只須要控制圈圈的半徑便可,依舊看代碼:ide
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDrawable.draw(canvas);
drawEffect(canvas);
}
private void drawEffect(Canvas canvas) {
// 畫圓
if (mRadius > 0)
canvas.drawCircle(mCircleCenter.x, mCircleCenter.y, mRadius, mPaint);
if (drawLines == 1) {
// 劃線
...
}
public void animation() {
final float radius = getInitRadius(mDrawable);
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "radius", radius, radius * 1.5f, radius * 3.0f);
animator.setInterpolator(new AnticipateInterpolator());
animator.setDuration(500);
// 畫線
// ...
set.start();
}
複製代碼
至此咱們完成了拇指的縮放和波紋效果,內心美滋滋有木有 #### 3.1.2 線段效果 線段怎麼去畫呢?中小學老師告訴咱們,兩點確認一條線段。問題隨之轉換:佈局
/**
* 用於計算 線條的長度
* @param scale 外圓半徑爲內圓半徑的scale倍數
*/
private void initPointFs(float scale) {
mPointList.clear();
float radius = getInitRadius(mDrawable);
int base = -60;
int factor = -20;
for (int i = 0; i < 4; i++) {
int result = base + factor * i;
// 點p1爲mDrawable外接圓上的點
PointF p1 = new PointF(
mCircleCenter.x + (float) (radius * Math.cos(Math.toRadians(result))),
mCircleCenter.y + (float) (radius * Math.sin(Math.toRadians(result)))
);
// 點p1爲mDrawable外接圓scale倍上的點
PointF p2 = new PointF(
mCircleCenter.x + (float) (scale * radius * Math.cos(Math.toRadians(result))),
mCircleCenter.y + (float) (scale * radius * Math.sin(Math.toRadians(result)))
);
mPointList.add(p1);
mPointList.add(p2);
}
}
複製代碼
經過代碼註解不難發現,這裏咱們巧妙的利用同心圓和角度的方式來肯定了4條線段,8個點集合的值(豆豆不由感嘆,數學對程序員的重要性)。這樣作的好處就是足夠靈活,不管View大小如何變,線段的間隔和長短都是適宜的。 至此左側的拇指動畫效果,算是告一段落了。post
右邊的數字翻牌效果,乍看起來很簡單,無非就是drawText()累加以後從新drawText();原理上是這樣的沒錯,不過值得注意的是:動畫
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
···
switch (widthSpecMode) {
···
case MeasureSpec.AT_MOST:
int width = (int) mPaint.measureText("0", 0, 1) * mCurrentString.length();
widthMeasureSpec = width;
break;
···
}
switch (heightSpecMode) {
···
case MeasureSpec.AT_MOST:
mTextHeight = mPaint.getFontSpacing();
heightMeasureSpec = (int) (mTextHeight * 3);
break;
case MeasureSpec.EXACTLY:
mPaint.setTextSize(heightSpecSize / 4);
mTextHeight = (int) mPaint.getFontSpacing();
heightMeasureSpec = heightSpecSize;
break;
}
pointY = 2 * mTextHeight;
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
複製代碼
在測量出View的寬高以後,便要着手去畫view的內容了,而內容很簡單,就是一系列的String值。到這裏都比較容易實現,而難點則是,肯定上一個和下一個值,以及他們的位置。 細心的朋友可能已經發如今measure的時候,咱們有一個mTextHeigh記錄了文字的高度,pointY記錄了兩倍文字的高度,沒錯這裏就是利用mTextHeight來控制三個可能要畫出來的string值的位置的。
這裏有必要提一下的是,drawText(@NonNull String text, float x, float y, @NonNull Paint paint)這個方法中的float y對應的是baseLine的y值,簡單的理解的話就是一串String的bottom的位置,畫出來的內容是在bottom之上的。這也是爲何咱們要用pointY = 2 * mTextHeight的理由。至此不難想到,咱們的lastNum, currentNum, NextNum畫的位置,分別對應mTextHeight, 2 * mTextHeight和3 * mTextHeight。至此三個值的位置便算是肯定好了。
先看加1的處理,上代碼:
public void addOne() {
mCurrentString = String.valueOf(mCurrentNum);
mCurrentNum++;
mNextString = String.valueOf(mCurrentNum);
mStatus = ADD;
// 數字位數進1
if (mCurrentString.length() < mNextString.length()) {
mCurrentString = " " + mCurrentString;
requestLayout();
}
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "pointY", 2 * mTextHeight, mTextHeight);
ObjectAnimator alphaAnim = ObjectAnimator.ofInt(this, "paintAlpha", 255, 0);
AnimatorSet set = new AnimatorSet();
set.playTogether(alphaAnim, animator);
set.start();
}
複製代碼
代碼比較簡單,無非是作了移動和透明度的動畫效果,這裏便解決了「上下翻動時前一個數字會漸漸隱掉」的需求,須要注意的點是,數字位進1時的利用空格佔位的處理,不作該處理,當數字進位後,動畫效果會差強人意,有興趣的朋友能夠去試試看。 結合onDraw方法再來看看:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mStatus == NONE) {
canvas.drawText(mCurrentString, 0, pointY, mPaint);
} else if (mStatus == ADD) {
for (int i = mNextString.length() - 1; i >= 0; i--) {
String next = String.valueOf(mNextString.charAt(i));
String current = String.valueOf(mCurrentString.charAt(i));
// i位置須要改變
if (!next.equals(current)) {
mPaint.setAlpha(mPaintAlpha);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, pointY, mPaint);
// mPaintAlpha : 255 - 0 遞減
mPaint.setAlpha(255 - mPaintAlpha);
canvas.drawText(next, mPaint.measureText("0", 0, 1) * i, mTextHeight + pointY, mPaint);
// i位置不須要改變
} else {
mPaint.setAlpha(255);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight * 2, mPaint);
}
}
} else if (mStatus == REDUCE) {
// pointY是累加的,所以有個往下滑動效果
for (int i = mCurrentString.length() - 1; i >= 0; i--) {
String last = String.valueOf(mLastString.charAt(i));
String current = String.valueOf(mCurrentString.charAt(i));
// i位置須要改變
if (!last.equals(current)) {
mPaint.setAlpha(mPaintAlpha);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight + pointY, mPaint);
// mPaintAlpha : 255 - 0 遞減
mPaint.setAlpha(255 - mPaintAlpha);
canvas.drawText(last, mPaint.measureText("0", 0, 1) * i, pointY, mPaint);
// i位置不須要改變
} else {
mPaint.setAlpha(255);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight * 2, mPaint);
}
}
}
}
複製代碼
這裏即是核心所在了:如何無需變化的數位上的值不會被翻動? onDraw方法中給出了咱們答案,思路很簡單:
至此gif圖中的兩部分效果都已經實現
以上是分開獨立的兩個view,爲了更方便的使用這個效果,咱們須要將兩個view的功能整合在一塊兒,起到一個聯動效果,也就須要引入一個ViewGroup去肯定這兩個view(PraiseView和RecordView)的佈局,這部分主要涉及到layout,以及viewgroup測繪的時候,使用的是match_parent寬高時,如何控制子view的顯示,有興趣的朋友能夠直接去看代碼,這裏暫不作贅述了。
行文至此,我不由點了根黃鶴樓,望着那嫋嫋的煙,一擡手摸着了天... 天邊飄來一個: github地址
附贈優惠禮包自取: 阿里雲飛機票