高仿探探首頁波紋掃描效果

閒着也是閒着的時候,打開探探劃一劃,挺多男同胞會這樣吧。這不,我也是這樣,看到首頁探探的效果仍是挺吸引人的。以前仿照實現了一個,效果還差一點,正好今天沒事完善一下,寫下來,但願看到能有收穫。android

實現的效果

首先看看實現後的效果,先很少說。固然跟探探的原版仍是有差距的,沒有在細節上面優化的更多。不過花時間調一調仍是能夠的,如今的效果能夠看到,我在下面加了幀數的顯示,在真機上顯示仍是很流暢的,模擬器上因爲性能不行仍是有點卡。git

實現效果

實現的分析

經過效果圖能夠看到,總體的實現能夠分爲如下四步:github

  1. 波紋漣漪的效果
  2. 漸變掃描的效果和中間的鏤空
  3. 旋轉
  4. 點擊頭像的動畫

把以上步驟分別加以實現,就能夠作到了。具體實現方法也不止一種,我這裏選擇的實現還算是簡單易懂,易於實現的。如下分解各個步驟,並對關鍵的細節詳加解釋。canvas

如何實現

由於有頭像,而且涉及到加載網絡圖片。理論上來講咱們能夠直接繼承ImageView來實現,但是這樣太複雜了,是不可取的。因此頭像跟咱們如今所要實現效果是分開的。而後在跟頭像組合在一塊兒,這裏可使自定義一個ViewGroup把二者結合,我這裏圖省事,這裏就沒有去作了,而是直接在使用的時候,在佈局裏面組合在一塊兒。bash

  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)
複製代碼

隨着時間的增加,透明度越低,半徑越大。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呢,由於定義該大小做爲波紋圓開始的半徑。到這裏第一步就算完成了。

  1. 掃描的效果是關鍵的部分,並且效率直接影響是否可用。仔細看效果,其實也是一個圓只不過添加了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();
複製代碼

有興趣查看源碼我是源碼,查看更多細節。

相關文章
相關標籤/搜索