閒着也是閒着的時候,打開探探劃一劃,挺多男同胞會這樣吧。這不,我也是這樣,看到首頁探探的效果仍是挺吸引人的。以前仿照實現了一個,效果還差一點,正好今天沒事完善一下,寫下來,但願看到能有收穫。android
首先看看實現後的效果,先很少說。固然跟探探的原版仍是有差距的,沒有在細節上面優化的更多。不過花時間調一調仍是能夠的,如今的效果能夠看到,我在下面加了幀數的顯示,在真機上顯示仍是很流暢的,模擬器上因爲性能不行仍是有點卡。git
經過效果圖能夠看到,總體的實現能夠分爲如下四步:github
把以上步驟分別加以實現,就能夠作到了。具體實現方法也不止一種,我這裏選擇的實現還算是簡單易懂,易於實現的。如下分解各個步驟,並對關鍵的細節詳加解釋。canvas
由於有頭像,而且涉及到加載網絡圖片。理論上來講咱們能夠直接繼承ImageView來實現,但是這樣太複雜了,是不可取的。因此頭像跟咱們如今所要實現效果是分開的。而後在跟頭像組合在一塊兒,這裏可使自定義一個ViewGroup把二者結合,我這裏圖省事,這裏就沒有去作了,而是直接在使用的時候,在佈局裏面組合在一塊兒。bash
inner class RippleCircle {
// 4s * 60 frms = 240
private val slice = 150
var startRadius = 0f
var endRadius = 0f
var cx = 0f
var cy = 0f
private var progress = 0
fun draw(canvas: Canvas) {
if (progress >= slice) {
// remove
post {
rippleCircles.remove(this)
}
return
}
progress++
ripplePaint.alpha = (1 - progress.div(slice * 1.0f)).times(255).toInt()
val radis = startRadius + (endRadius - startRadius).div(slice).times(progress)
canvas.drawCircle(cx, cy, radis, ripplePaint)
}
}
複製代碼
看到以上代碼可能對slice這個屬性有疑惑,這是定義波紋持續時間的,若是60幀每秒,那麼持續4s,總共是240幀。這裏默認取150幀,因此在60幀持續的時間是2.5s.透明度和半徑都跟slice有關:網絡
ripplePaint.alpha = (1 - progress.div(slice * 1.0f)).times(255).toInt()
val radis = startRadius + (endRadius - startRadius).div(slice).times(progress)
複製代碼
隨着時間的增加,透明度越低,半徑越大。app
怎麼使用封裝的RippleCircle。咱們的要求是能夠動態添加,而且消失以後須要移除,因此經過ArrayList來做爲容器。但這裏涉及到對集合的添加和刪除操做,若是同時進行會發生異常。解決以下,使用CopyOnWriteArrayList,而且移除經過:ide
post {
rippleCircles.remove(this)
}
複製代碼
而後在onDraw中,值得一提的是爲了防止被掃描的部分擋住,這裏的代碼須要寫在onDraw方法的後部分。函數
for (i in 0 until rippleCircles.size) {
rippleCircles[i].draw(canvas)
}
複製代碼
在startRipple()方法中添加RippleCircle:佈局
rippleCircles.add(RippleCircle().apply {
cx = width.div(2).toFloat()
cy = height.div(2).toFloat()
val maxRadius = Math.min(width, height).div(2).toFloat()
startRadius = maxRadius.div(3)
endRadius = maxRadius
})
複製代碼
startRipple也是暴露出去調用添加波紋的方法。點擊頭像而後添加。涉及到自定義View固然測量是很關鍵的一部分。不過如今直接使用默認就能夠,而後去寬高的最小值,除以2做爲半徑。在這裏爲何startRadius要處以3呢,由於定義該大小做爲波紋圓開始的半徑。到這裏第一步就算完成了。
SweepGradient(float cx, float cy,
@NonNull @ColorInt int colors[], @Nullable float positions[])
複製代碼
重點在於positions 的理解。按照文檔解釋以及代碼。 好比跟colors 的值一一對應,還必須是單調遞增的,防止出現嚴重異常。 positions 對應每個顏色的位置,固然是再圓的位置。順時針,0爲0°,0.5爲180°,1爲360°。 若是要像探探同樣,最開始是一根線顏色很深。說明第一種顏色很深佔比很小,第二種顏色淺佔比很大,以下
val colors = intArrayOf(getColor(R.color.pink_fa758a),getColor(R.color.pink_f5b8c2),getColor(R.color.top_background_color),getColor(R.color.white))
SweepGradient(width.div(2).toFloat(), height.div(2).toFloat(), colors, floatArrayOf(0f,0.001f,0.9f,1f))
複製代碼
因此設置對了參數,整個掃描漸變的效果就差很少了。而後在對畫筆設置shader,在drawCircle。
backPaint.setShader(SweepGradient(width.div(2).toFloat(), height.div(2).toFloat(), colors, floatArrayOf(0f, 0.001f, 0.9f, 1f)))
canvas.drawCircle(width.div(2).toFloat(), height.div(2).toFloat(), radius, backPaint)
複製代碼
當作完上面的操做以後,整個掃面的範圍是整個圓,而須要的效果是中間有鏤空的校園,這裏又涉及到對xfermode的操做了。進行xfermode操做,必需要對canvas設置layer。若是不設置會有問題,鏤空的校園是黑色的。詳細的解釋在我之間的文章中有高仿QQ 發送圖片高亮HaloProgressView一文中作過闡述。setLayer須要設置範圍,那麼咱們的範圍就是覆蓋整個大圓的矩形
val rectF = RectF(width.div(2f) - radius
, height.div(2f) - radius
, width.div(2f) + radius
, height.div(2f) + radius)
val sc = canvas.saveLayer(rectF, backPaint, Canvas.ALL_SAVE_FLAG)
複製代碼
而後再drawCircle以後在設置xfermode
backPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.DST_OUT))
複製代碼
這裏採起DST_OUT,爲何採用這種模式,在以前文章中能夠詳細查看Paint Xfermode 詳解.到這裏掃描漸變和鏤空都實現了,只差最後一步,轉動起來。 轉動直接經過canvas的rotate方法是很適合如今的場景。由於整個View都是圓。涉及到canvas操做,須要save,而後再restore
canvas.save()
canvas.rotate(sweepProgress.toFloat(), width.div(2f), height.div(2f))
...
canvas.restore()
複製代碼
能夠看到sweepProgress是轉動的關鍵,經過動畫控制是很方便的。
private val renderAnimator by lazy {
ValueAnimator.ofInt(0, 60)
.apply {
interpolator = LinearInterpolator()
duration = 1000
repeatMode = ValueAnimator.RESTART
repeatCount = ValueAnimator.INFINITE
addUpdateListener {
postInvalidateOnAnimation()
fps++
sweepProgress++
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationRepeat(animation: Animator?) {
super.onAnimationRepeat(animation)
fps = 0
}
})
}
}
複製代碼
能夠看到參數設置一秒60次執行。也就是60幀。再經過到了360°,置0便可。到這裏已經完成了TanTanRippleView的實現。接着實現頭像的動畫。在頭像的點擊事件裏面直接添加:
((TanTanRippleView)findViewById(R.id.ripple)).startRipple();
AnimatorSet set = new AnimatorSet();
set.setInterpolator(new BounceInterpolator());
set.playTogether(
ObjectAnimator.ofFloat(v,"scaleX",1.2f,0.8f,1f),
ObjectAnimator.ofFloat(v,"scaleY",1.2f,0.8f,1f));
set.setDuration(1100).start();
複製代碼
有興趣查看源碼我是源碼,查看更多細節。