交友軟體探探首頁波紋掃描效果的實現
閒著也是閒著的時候,開啟探探劃一劃,挺多男同胞會這樣吧。這不,我也是這樣,看到首頁探探的效果還是挺吸引人的。之前仿照實現了一個,效果還差一點,正好今天沒事完善一下,寫下來,希望看到能有收穫。
實現的效果
首先看看實現後的效果,先不多說。當然跟探探的原版還是有差距的,沒有在細節上面優化的更多。不過花時間調一調還是可以的,現在的效果可以看到,我在下面加了幀數的顯示,在真機上顯示還是很流暢的,模擬器上由於效能不行還是有點卡。

實現的分析
通過效果圖可以看到,整體的實現可以分為以下四步:
-
波紋漣漪的效果
-
漸變掃描的效果和中間的鏤空
-
旋轉
-
點選頭像的動畫
把以上步驟分別加以實現,就可以做到了。具體實現方法也不止一種,我這裡選擇的實現還算是簡單易懂,易於實現的。以下分解各個步驟,並對關鍵的細節詳加解釋。
如何實現
因為有頭像,並且涉及到載入網路圖片。理論上來說我們可以直接繼承ImageView來實現,可是這樣太複雜了,是不可取的。所以頭像跟我們現在所要實現效果是分開的。然後在跟頭像組合在一起,這裡可以使自定義一個ViewGroup把兩者結合,我這裡圖省事,這裡就沒有去做了,而是直接在使用的時候,在佈局裡面組合在一起。
1、所以第一步先不考慮頭像而是實現TanTanRippleView.接下來看水波紋的實現:
我們需要的是,波紋是動態新增的,通過點選頭像新增,所以需要暴露介面。並且波紋是有漸變的,越到邊緣透明度越低,直到消失。每一個波紋都是一個圓,透明度通過改變Paint的顏色即可,透明度跟圓的半徑也是有規律可循的。所以我這裡把每個波紋做了封裝。
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)
隨著時間的增長,透明度越低,半徑越大。
怎麼使用封裝的RippleCircle。我們的要求是可以動態新增,並且消失之後需要移除,所以通過ArrayList來作為容器。但這裡涉及到對集合的新增和刪除操作,如果同時進行會發生異常。解決如下,使用CopyOnWriteArrayList,並且移除通過:
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呢,因為定義該大小作為波紋圓開始的半徑。到這裡第一步就算完成了。
2、掃描的效果是關鍵的部分,而且效率直接影響是否可用。仔細看效果,其實也是一個圓只不過添加了 shader
。所以重點就是 shader
的實現。android中預設提供了幾種Shader給我們使用。 SweepGradient
就是我們需要的,掃描漸變。然後選擇了之後,就是調整引數了,看一下SweepGradient的用法:
建構函式:
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();
有興趣檢視原始碼: https://github.com/hewking/TanTanRippleView
檢視更多細節。