代碼地址以下:
http://www.demodashi.com/demo/12833.htmlhtml
最近在學習android的高級view的繪製,再結合值動畫的數據上的改變,本身擼了個360手機助手的下載按鈕。先看下原版的360手機助手的下載按鈕是長啥樣子吧:android
再來看看本身demo吧,大家盡情的吐槽吧,哈哈:
canvas
裏面的細節問題還會不斷地更改的,gif的動態圖是有些快的,這是由於簡書要求gif的大小了,這個也冒得辦法啊 。因此想看真是效果的筒子們,能夠去看demo哈。api
細心的朋友可能發現loading狀態下左邊幾個運動圓的最高點和最低點都越界了,這是由於在規定正弦函數的最高點時沒考慮圓的半徑的長度,所以近兩天作了點修改了,效果圖以下:app
我們的整個過程能夠分爲這麼幾個狀態,在這裏我用枚舉類進行了概括:ide
public enum Status { Normal, Start, Pre, Expand, Load, Complete; }
Normal(還沒進行開始的狀態,也就是咱們的默認狀態,也就是咱們還沒執行onTouch的時候了):函數
Start(點擊onTouch改變爲該狀態):學習
@Override public boolean onTouchEvent(MotionEvent event) { final int action = MotionEventCompat.getActionMasked(event); //擡起的時候去改變status if (action == MotionEvent.ACTION_UP) { status = Status.Start; startAnimation(collectAnimator); } return true; }
那我們再來看看collectAnimator作了些什麼呢:動畫
collectAnimator = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { currentLength = (int) (width - width * interpolatedTime); if (currentLength <= height) { currentLength = height; clearAnimation(); status = Status.Pre; angleAnimator.start(); } invalidate(); } }; collectAnimator.setInterpolator(new LinearInterpolator()); collectAnimator.setDuration(collectSpeed);
其實核心的就是在這個過程當中改變了全局變量currentLength而已,此時咱們回到onDraw裏面吧,看看在Start狀態下currentLength都作了些什麼:ui
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (status == Status.Normal || status == Status.Start) { float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2); canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint); Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float allHeight = fontMetrics.descent - fontMetrics.ascent; if (status == Status.Normal) { canvas.drawText("下載", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint); } } else if (status == Status.Pre) { canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint); canvas.save(); canvas.rotate(angle, (float) (width * 1.0 / 2), (float) (height * 1.0 / 2)); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.restore(); } else if (status == Status.Expand) { float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2); canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint); canvas.save(); canvas.translate(translateX, 0); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.restore(); } else if (status == Status.Load || status == Status.Complete) { float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2); bgPaint.setColor(progressColor); canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint); if (progress != 100) { //畫中間的幾個loading的點的狀況哈 if (fourMovePoint[0].isDraw) canvas.drawCircle(fourMovePoint[0].moveX, fourMovePoint[0].moveY, fourMovePoint[0].radius, textPaint); if (fourMovePoint[1].isDraw) canvas.drawCircle(fourMovePoint[1].moveX, fourMovePoint[1].moveY, fourMovePoint[1].radius, textPaint); if (fourMovePoint[2].isDraw) canvas.drawCircle(fourMovePoint[2].moveX, fourMovePoint[2].moveY, fourMovePoint[2].radius, textPaint); if (fourMovePoint[3].isDraw) canvas.drawCircle(fourMovePoint[3].moveX, fourMovePoint[3].moveY, fourMovePoint[3].radius, textPaint); } float progressRight = (float) (progress * width * 1.0 / 100); //在最上面畫進度 bgPaint.setColor(bgColor); canvas.save(); canvas.clipRect(0, 0, progressRight, height); canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint); canvas.restore(); if (progress != 100) { bgPaint.setColor(bgColor); canvas.drawCircle((float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint); canvas.save(); canvas.rotate(loadAngle, (float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2)); canvas.drawCircle(width - height + 25, getCircleY(width - height + 25), 5, textPaint); canvas.drawCircle(width - height + 40, getCircleY(width - height + 40), 7, textPaint); canvas.drawCircle(width - height + 60, getCircleY(width - height + 60), 9, textPaint); canvas.drawCircle(width - height + 90, getCircleY(width - height + 90), 11, textPaint); canvas.restore(); } //中間的進度文字 Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float allHeight = fontMetrics.descent - fontMetrics.ascent; canvas.drawText(progress + "%", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint); } }
爲了便於咱們分析每個狀態,咱們就看下每一個狀態下的繪製動做吧:
if (status == Status.Normal || status == Status.Start) { float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2); canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint); Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float allHeight = fontMetrics.descent - fontMetrics.ascent; if (status == Status.Normal) { canvas.drawText("下載", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint); } }
你們看到變量currentLength了沒,其實這裏就是去改變背景的right座標,正好上面動畫裏面也是從width減少的一個值,那麼此時的動畫你們腦海裏能想象得出來了吧:
Start狀態結束都就是進入到Pre狀態了:
上面collectAnimator動畫結束後啓動的動畫是:angleAnimator了,
咱們再去看看該動畫都作了些啥:
angleAnimator = ValueAnimator.ofFloat(0, 1); angleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { angle += 10; invalidate(); } });
改變的仍是全局的變量angle,再來看看該變量在onDraw
方法裏面都作了些啥吧:
else if (status == Status.Pre) { canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint); canvas.save(); canvas.rotate(angle, (float) (width * 1.0 / 2), (float) (height * 1.0 / 2)); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.restore(); }
畫了幾個圓,而後經過上面的angle變量來旋轉canvas,並且幾個圓的圓心都與view
的中心點有關,所以你們從示例圖中應該看出來了:
pre狀態結束後,就是Expand狀態了,你們能夠看pre狀態下動畫結束的代碼:
angleAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { status = Status.Expand; angleAnimator.cancel(); startAnimation(tranlateAnimation); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } });
能夠看出下一個動畫tranlateAnimation了,仍是同樣定位到該動畫的代碼吧,看看都作了些啥:
tranlateAnimation = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { currentLength = (int) (height + (width - height) * interpolatedTime); translateX = (float) ((width * 1.0 / 2 - height * 1.0 / 2) * interpolatedTime); invalidate(); } };
能夠看出此時改變的全局變量有兩個:currentLength和translateX,想必你們知道currentLength是什麼做用了吧,下面就來看看onDraw
吧:
else if (status == Status.Expand) { float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2); canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint); canvas.save(); canvas.translate(translateX, 0); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint); canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint); canvas.restore(); }
一個是改變背景的right座標,再個就是canvas.translate
幾個中心點的圓了:
expand狀態結束後就是正式進入到下載狀態了,這裏的枚舉我定義是Load,
看下expand結束的動畫代碼吧:
tranlateAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { clearAnimation(); status = Status.Load; clearAnimation(); loadRotateAnimation.start(); movePointAnimation.start(); } @Override public void onAnimationRepeat(Animation animation) { } });
你們能夠看到該處有兩個動畫的啓動了(loadRotateAnimation.start()和movePointAnimation.start()),說明此處有兩個動畫在同時執行罷了,先來看loadRotateAnimation動畫裏面都作了些啥吧:
loadRotateAnimation = ValueAnimator.ofFloat(0, 1); loadRotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { loadAngle += rightLoadingSpeed; if (loadAngle > 360) { loadAngle = loadAngle - 360; } invalidate(); } }); loadRotateAnimation.setDuration(Integer.MAX_VALUE);
仍是一個角度改變的動畫啊,那就看看loadAngle是改變誰的動畫吧,仍是照常咱們進入到onDraw
方法吧:
if (progress != 100) { bgPaint.setColor(bgColor); canvas.drawCircle((float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint); canvas.save(); canvas.rotate(loadAngle, (float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2)); canvas.drawCircle(width - height + 25, getCircleY(width - height + 25), 5, textPaint); canvas.drawCircle(width - height + 40, getCircleY(width - height + 40), 7, textPaint); canvas.drawCircle(width - height + 60, getCircleY(width - height + 60), 9, textPaint); canvas.drawCircle(width - height + 90, getCircleY(width - height + 90), 11, textPaint); canvas.restore(); }
仍是一個圓的旋轉啊,其實這幾個點是有規律去繪製的,他們幾個圓心應該是內圓的弧度上的,而且半徑是依次增大的。這裏調了getCircleY()
方法,該方法就是算圓弧上幾個點的y座標。
/** * 根據x座標算出圓的y座標 * * @param cx:點的圓心x座標 * @return */ private float getCircleY(float cx) { float cy = (float) (height * 1.0 / 2 - Math.sqrt((height * 1.0 / 2 - dp2px(7)) * (height * 1.0 / 2 - dp2px(7)) - ((width - height * 1.0 / 2) - cx) * ((width - height * 1.0 / 2) - cx))); return cy; }
這裏看似方法很複雜,其實就是初中定義圓的方程式:(x-cx)^2+(y-cy)^2=r^2
下面再來看看movePointAnimation動畫都作了些啥吧:
fourMovePoint[0] = new MovePoint(dp2px(4), (float) ((width - height / 2) * 0.88), 0); fourMovePoint[1] = new MovePoint(dp2px(3), (float) ((width - height / 2) * 0.85), 0); fourMovePoint[2] = new MovePoint(dp2px(2), (float) ((width - height / 2) * 0.80), 0); fourMovePoint[3] = new MovePoint(dp2px(5), (float) ((width - height / 2) * 0.75), 0); movePointAnimation = ValueAnimator.ofFloat(0, 1); movePointAnimation.setRepeatCount(ValueAnimator.INFINITE); movePointAnimation.setInterpolator(new LinearInterpolator()); movePointAnimation.setDuration(leftLoadingSpeed); movePointAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = animation.getAnimatedFraction(); fourMovePoint[0].moveX = fourMovePoint[0].startX - fourMovePoint[0].startX * value; if (fourMovePoint[0].moveX <= height / 2) { fourMovePoint[0].isDraw = false; } fourMovePoint[1].moveX = fourMovePoint[1].startX - fourMovePoint[0].startX * value; if (fourMovePoint[1].moveX <= height / 2) { fourMovePoint[1].isDraw = false; } fourMovePoint[2].moveX = fourMovePoint[2].startX - fourMovePoint[0].startX * value; if (fourMovePoint[2].moveX <= height / 2) { fourMovePoint[2].isDraw = false; } fourMovePoint[3].moveX = fourMovePoint[3].startX - fourMovePoint[0].startX * value; if (fourMovePoint[3].moveX <= height / 2) { fourMovePoint[3].isDraw = false; } fourMovePoint[0].moveY = drawMovePoints(fourMovePoint[0].moveX); fourMovePoint[1].moveY = drawMovePoints(fourMovePoint[1].moveX); fourMovePoint[2].moveY = drawMovePoints(fourMovePoint[2].moveX); fourMovePoint[3].moveY = drawMovePoints(fourMovePoint[3].moveX); Log.d("TAG", "fourMovePoint[0].moveX:" + fourMovePoint[0].moveX + ",fourMovePoint[0].moveY:" + fourMovePoint[0].moveY); } }); movePointAnimation.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { fourMovePoint[3].isDraw = true; fourMovePoint[2].isDraw = true; fourMovePoint[1].isDraw = true; fourMovePoint[0].isDraw = true; } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { fourMovePoint[3].isDraw = true; fourMovePoint[2].isDraw = true; fourMovePoint[1].isDraw = true; fourMovePoint[0].isDraw = true; } });
這裏首先定義了四個MovePoint
,分別定義了他們的半徑,圓心,而後在該動畫裏面不斷地改變四個point的圓心,其實這裏核心就是如何求出四個點運行的軌跡了,把軌跡弄出來一切就都呈現出來了,能夠看看該動畫的onAnimationUpdate
方法裏面調用的drawMovePoints
方法:
/** * 這裏是在load狀況下獲取幾個點運動的軌跡數學函數 * * @param moveX * @return */ private float drawMovePoints(float moveX) { float moveY = (float) (height / 2 + (height / 2 - fourMovePoint[3].radius) * Math.sin(4 * Math.PI * moveX / (width - height) + height / 2)); return moveY; }
這裏就是一個數學裏面常常用的正弦函數了,求出週期、x軸上的偏移量、y軸上的便宜量、頂點,還有一個注意點,該處求頂點的時候,須要減去這幾個圓中的最大半徑,以前我就是沒注意到這點,最後出來的軌跡就是一個圓會跑到view
的外面了。效果圖以下:
最後一個狀態就是Complete了,也就是當前的進度到了100,可見代碼:
/** * 進度改變的方法 * * @param progress(當前進度) */ public void setProgress(int progress) { if (status != Status.Load) { throw new RuntimeException("your status is not loading"); } if (this.progress == progress) { return; } this.progress = progress; if (onProgressUpdateListener != null) { onProgressUpdateListener.onChange(this.progress); } invalidate(); if (progress == 100) { status = Status.Complete; this.stop = false; clearAnimation(); loadRotateAnimation.cancel(); movePointAnimation.cancel(); } }
這裏要作的就是改變狀態,中止一切動畫了,到此代碼的講解就到這裏了,趕快start起來吧。
屬性也沒怎麼整理,就抽取出了一些比較經常使用的幾個了:
代碼使用:
/** * 進度改變的方法 * @param progress */ public void setProgress(int progress) { if (status != Status.Load) { throw new RuntimeException("your status is not loading"); } if (this.progress == progress) { return; } this.progress = progress; if (onProgressUpdateListener != null) { onProgressUpdateListener.onChange(this.progress); } invalidate(); if (progress == 100) { status = Status.Complete; this.stop = false; clearAnimation(); loadRotateAnimation.cancel(); movePointAnimation.cancel(); } } /** * 暫停或繼續的方法 * * @param stop(true:表示暫停,false:繼續) */ public void setStop(boolean stop) { if (this.stop == stop) { return; } this.stop = stop; if (stop) { loadRotateAnimation.cancel(); movePointAnimation.cancel(); } else { loadRotateAnimation.start(); movePointAnimation.start(); } } /** *設置狀態的方法 * @param status(Down360Loading.Status.Normal:直接取消的操做) */ public void setStatus(Status status) { if (this.status == status) { return; } this.status = status; if (this.status == Status.Normal) { progress = 0; this.stop = false; clearAnimation(); loadRotateAnimation.cancel(); movePointAnimation.cancel(); } invalidate(); }
項目結構
仿360手機助手下載按鈕
代碼地址以下:
http://www.demodashi.com/demo/12833.html
注:本文著做權歸做者,由demo大師代發,拒絕轉載,轉載須要做者受權