高仿探探首页波纹扫描效果

闲着也是闲着的时候,打开探探划一划,挺多男同胞会这样吧。这不,我也是这样,看到首页探探的效果还是挺吸引人的。之前仿照实现了一个,效果还差一点,正好今天没事完善一下,写下来,希望看到能有收获。

实现的效果

首先看看实现后的效果,先不多说。当然跟探探的原版还是有差距的,没有在细节上面优化的更多。不过花时间调一调还是可以的,现在的效果可以看到,我在下面加了帧数的显示,在真机上显示还是很流畅的,模拟器上由于性能不行还是有点卡。

实现效果
实现的分析

通过效果图可以看到,整体的实现可以分为以下四步:

  1. 波纹涟漪的效果
  2. 渐变扫描的效果和中间的镂空
  3. 旋转
  4. 点击头像的动画

把以上步骤分别加以实现,就可以做到了。具体实现方法也不止一种,我这里选择的实现还算是简单易懂,易于实现的。以下分解各个步骤,并对关键的细节详加解释。

如何实现

因为有头像,并且涉及到加载网络图片。理论上来说我们可以直接继承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呢,因为定义该大小作为波纹圆开始的半径。到这里第一步就算完成了。

  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();

有兴趣查看源码我是源码,查看更多细节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容

  • 我真的不知道该怎么和男生交往,我不想搞暧昧,我只想像普通朋友那样慢慢了解。有时候想要像对待同性朋友那样对待异性,又...
    刘洞洞666阅读 181评论 1 0
  • 2017年的最后一天,微信运动上的数据显示,我走了16112步,这对一向有些宅的我来说,今天是超额完成了我每天10...
    如月如月阅读 321评论 15 12
  • 上次的文章中,我发现了武侠里的领衔男女主角都是年轻人。今天我就这一现象找找原因吧。 其实大多小说影视作品中,主人公...
    落英王阅读 902评论 0 0
  • 活着,会有千万种原因不能尽如人意,磕磕绊绊的琐碎,晴天霹雳的倒霉,任何一个不经意都可能给生活下个脚绊,跌个狗吃屎般...
    他说夜已深阅读 280评论 0 1