前言:公司在作的一個項目,要求在地圖上以水波氣泡的形式來顯示站點,而且氣泡要有水波的動態效果。好吧!既然有這樣的需求,那就手擼一款水波氣泡吧!java
最後完成的效果圖以下git
不想看文章的話,能夠點擊這裏,直接獲取源碼。github
在須要自定義view的時候,我首先要作的就是將最後要實現的效果來進行拆分,拆分紅許多小的步驟,而後一步步的來實現,最終達到想要的效果。canvas
能夠將文章開始的時候的效果圖拆分紅如下幾部分:markdown
拆解以後,就能夠按照拆解的步驟來一步步實現了。ide
這裏畫白色背景有如下兩種方式:oop
path
直接描述一個白色背景的形狀。path
描述一個三角形,而後在畫出一個圓形,即成最終的白色背景了。第一種方式以下圖的左圖,用path
直接描述出了白色背景,這種方式能夠用path.addArc()
來畫上部弧形,而後用path.moveTo()
和path.lineTo()
方法描述出下部分的尖角。post
第二種實現的方式以下圖的右圖,直接畫出一個圓,再用path.moveTo()
和path.lineTo()
方法來描述出下部分的尖角。動畫
本文采用的是第二種方式來實現的,具體代碼以下ui
//此處代碼是下部尖角的path mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2); mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4); mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2); //畫外部背景 canvas.drawPath(mBackgroundPath, mBackgroundPaint); canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint); 複製代碼
內部的氣泡的形狀其實就是縮小的外部背景,具體的代碼以下
//內部氣泡的尖角 mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5)); mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5)); mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5)); //畫圓 mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW); 複製代碼
到這裏已經將氣泡的基本形狀畫出來了,見下圖
咱們會發現氣泡內部的顏色是漸變色,那漸變色是怎麼設置的呢?其實自定義view就是將想要的效果經過畫筆畫在畫布上,實現顏色的漸變確定就是經過設置畫筆的屬性來實現的了,設置漸變色的代碼以下
//設置漸變色 Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"), Color.parseColor("#3831D4"), Shader.TileMode.CLAMP); mBubblesPaint.setShader(shader); 複製代碼
LinearGradient(float x0, float y0, float x1, float y1, @ColorInt int color0, @ColorInt int color1, @NonNull TileMode tile)
x0
y0
x1
y1
:漸變的兩個端點的位置color0
color1
是端點的顏色tile
:端點範圍以外的着色規則,類型是TileMode
。TileMode
一共有 3 個值可選:CLAMP
,MIRROR
和REPEAT
。通常用CLAMP
就能夠了。
氣泡內部的動畫是水波的形式,這裏畫水波用的是二階貝塞爾曲線,關於Android中貝塞爾曲線的知識能夠參考這裏。實現氣泡內部水波效果的代碼以下
/** * 核心代碼,計算path * * @return */ private Path getPath() { int itemWidth = waveWidth / 2;//半個波長 Path mPath = new Path(); mPath.moveTo(-itemWidth * 3, baseLine);//起始座標 Log.d(TAG, "getPath: " + baseLine); //核心的代碼就是這裏 for (int i = -3; i < 2; i++) { int startX = i * itemWidth; mPath.quadTo( startX + itemWidth / 2 + offset,//控制點的X,(起始點X + itemWidth/2 + offset) getWaveHeight(i),//控制點的Y startX + itemWidth + offset,//結束點的X baseLine//結束點的Y );//只須要處理完半個波長,剩下的有for循環自已就添加了。 } Log.d(TAG, "getPath: "); //下面這三句話是行程封閉的效果,不明白能夠將下面3句代碼註釋看下效果的變化 mPath.lineTo(width, height); mPath.lineTo(0, height); mPath.close(); return mPath; } //奇數峯值是正的,偶數峯值是負數 private float getWaveHeight(int num) { if (num % 2 == 0) { return baseLine + waveHeight; } return baseLine - waveHeight; } 複製代碼
上面的代碼畫出的水波以下圖
到這裏已經畫出了水波,但如今水波仍是靜止的,要讓水波不停的移動,就要添加屬性動畫,添加動畫的代碼以下
/** * 不斷的更新偏移量,而且循環。 */ public void updateXControl() { //設置一個波長的偏移 ValueAnimator mAnimator = ValueAnimator.ofFloat(0, waveWidth); mAnimator.setInterpolator(new LinearInterpolator()); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float animatorValue = (float) animation.getAnimatedValue(); offset = animatorValue;//不斷的設置偏移量,並重畫 postInvalidate(); } }); mAnimator.setDuration(1800); mAnimator.setRepeatCount(ValueAnimator.INFINITE); mAnimator.start(); } 複製代碼
修改一下onDraw
中的代碼,以下
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mBubblesPath.reset(); //設置漸變色 Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"), Color.parseColor("#3831D4"), Shader.TileMode.CLAMP); mBubblesPaint.setShader(shader); //此處代碼是下部尖角的path mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2); mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4); mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2); //內部氣泡的尖角 mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5)); mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5)); mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5)); //畫外部背景 canvas.drawPath(mBackgroundPath, mBackgroundPaint); canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint); Log.d(TAG, "cx: " + mResultWidth / 2); //畫水波 mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW); canvas.drawPath(getPath(), mBubblesPaint); } 複製代碼
好了,如今水波已經能夠移動了,看下效果
what!怎麼成這個樣子了呀,明顯不是我想要的效果呀,確定是哪裏出錯了,通過我仔細的推敲,總結了出現上面問題的緣由,緣由以下圖
出現上面問題的緣由就是由於下面三句代碼
mPath.lineTo(width, height); mPath.lineTo(0, height); mPath.close(); 複製代碼
知道是這三句代碼的緣由,那應該怎麼修改呢?這三句代碼好像不能動,否則就會出現波浪畫的不完整的狀況,額.....,那應該修改哪裏呢?靈光一閃,不是能夠裁剪畫布嘛,只要將畫布裁剪成想要的形狀,而後在畫波浪不久完美了。再修改onDraw
方法,修改後的代碼以下
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mBubblesPath.reset(); //設置漸變色 Shader shader = new LinearGradient(mResultWidth / 2, mResultWidth / 2 - mInnerRadius, mResultWidth / 2, mResultWidth / 2 + mInnerRadius, Color.parseColor("#9592FB"), Color.parseColor("#3831D4"), Shader.TileMode.CLAMP); mBubblesPaint.setShader(shader); //此處代碼是下部尖角的path mBackgroundPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2); mBackgroundPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4); mBackgroundPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2); //內部氣泡的尖角 mBubblesPath.moveTo(mResultWidth / 2 - mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5)); mBubblesPath.lineTo(mResultWidth / 2, mResultWidth / 2 + mOutRadius + mOutRadius / 4 - dp2px(getContext(), 5)); mBubblesPath.lineTo(mResultWidth / 2 + mOutRadius / 2, mResultWidth / 2 + mOutRadius / 2 - dp2px(getContext(), 5)); //畫外部背景 canvas.drawPath(mBackgroundPath, mBackgroundPaint); canvas.drawCircle(mResultWidth / 2, mResultWidth / 2, mOutRadius, mBackgroundPaint); Log.d(TAG, "cx: " + mResultWidth / 2); //切割畫布,畫水波 canvas.save(); mBubblesPath.addCircle(mResultWidth / 2, mResultWidth / 2, mInnerRadius, Path.Direction.CCW); //將畫布裁剪成內部氣泡的樣子 canvas.clipPath(mBubblesPath); canvas.drawPath(getPath(), mBubblesPaint); canvas.restore(); } 複製代碼
到這裏已經實現了文章開始時的效果了,文章也該結束了。
本文主要是講解怎樣實現水波氣泡,並無講到View的測量,貼出的也只是繪製氣泡的代碼,完整的代碼能夠點擊這裏獲取。
雖然已經擼出了這個效果,但最後項目中並無用這種動態的氣泡,由於氣泡多的時候是在是卡……。最後,喜歡此demo,就隨手給個star吧!
ps: 歷史文章中有乾貨哦!
本文已由公衆號「AndroidShared」首發