Android自定义View - 元素按钮的

作者: Android架构 | 来源:发表于2019-01-31 21:46 被阅读13次

    Android自定义View之元素按钮

    之前在dribbble看到的三个元素的按钮,参考了设计的创意,添加了自己定义的动画效果来实现.先看效果

    效果图

    分别是水火电三个元素的按钮实现.其中电的实现最简单,水的次之,火的实际还并不满意,没有火焰扰动的感觉,尝试过几次但是效果都不理想,最后只保留了自下向上的扇形遮罩.如果有好的效果再优化实现.

    闪电篇

    设计过程

    通过闪电的位置将整体划分成七个部分(七个部分的主要原因是最初设计了一个中部放大的透镜效果,但是没能实现),从右上角进入,在中心点附近运动,直到停在中心点.闪电本身可以看做一个中心对称的图形,整理就简化成了现将canvas旋转一定角度,然后绘制中心对称的闪电形状,最后在x轴上运动就可以了.

    黄线是旋转后的坐标

    黄线是旋转后的坐标,可以看出简化后实现起来很简单.

    代码实现

    • 背景部分
    //绘制闪电背景
    private fun  drawBaseButton(canvas: Canvas , index: Float) {
        //设置画笔
        val paint = Paint()
    
        //添加闪电移动到指定位置时的背景颜色设置
        if ((index <= 0.45F && index >= 0.35F) || (index >= 0.65F && index <= 0.75F)) {
            paint.color = Color.parseColor("#ACADAC")
        }else{
            paint.color = Color.parseColor("#595A59")
        }
    
        paint.style = Paint.Style.FILL
    
        //绘制闪电背景
        canvas.drawArc(RectF(-baseR, -baseR, baseR, baseR), 0F , 360F,true , paint)
    
    }
    
    • 闪电部分
    private fun  drawLighting(canvas: Canvas , index: Float) {
        val baseR = baseR * coefficient
        var index = index
        var changeR = 0F
    
        //将整个闪电的运动拆成七个部分
        if (index <= 0.25){
            changeR  = this.baseR + baseR
            changeR = (changeR * (1 - index / 0.25)).toFloat()
        }else if (index <= 0.4){
            index = index - 0.25F
            changeR  = this.baseR
            changeR = -(changeR * (index / (0.4F - 0.25F)))
        }else if (index <= 0.6F){
            index = index - 0.4F
            changeR = this.baseR
            changeR = -changeR *  (1 - index / 0.2F)
        }else if (index <= 0.7F){
            index = index - 0.6F
            changeR = baseR
            changeR = changeR * index / 0.1F
        }else if (index <= 0.8F){
            index = index - 0.7F
            changeR = baseR
            changeR = baseR - changeR * index / 0.1F
        }else if (index <= 0.9F){
            index = index - 0.8F
            changeR = baseR
            changeR = -changeR * index / 0.1F
        }else if (index <= 1F){
            index = index - 0.9F
            changeR = baseR
            changeR = -changeR + changeR * (index / 0.1F)
        }
    
        //设置画笔
        val path = Path()
        val paint = Paint()
        paint.strokeWidth = 5F
        paint.style = Paint.Style.FILL
        paint.color = viewBackgroundColor
    
        val points :MutableList<Point> = ArrayList()
        //设置绘制闪电的路径点
        points.add(pointFactory(60 , baseR))
        points.add(pointFactory(-45 , baseR / 2F))
        points.add(pointFactory(-45 - 90 , baseR / 5F))
        points.add(pointFactory(-30 - 90 , baseR))
        points.add(pointFactory(45 + 90 , baseR / 2F))
        points.add(pointFactory(45 , baseR / 5F))
        points.add(pointFactory(60 , baseR))
    
        //设置闪电的偏移量(模拟运动情况)
        //原本还想实现一个中心放大的透镜效果,但是效果很僵硬,只能移除了
        for (i in 0..points.size - 1){
            points.set(i , Point(points[i].x + changeR , points[i].y))
        }
    
        path.moveTo(points[0].x , points[0].y)
    
        for (index in 1..points.size - 1){
            path.lineTo(points[index].x , points[index].y)
        }
    
        canvas.drawPath(path , paint)
    
        //闪电绘制辅助坐标系
    //        val paint2 = Paint()
    //        paint2.strokeWidth = 5F
    //        paint2.color = Color.YELLOW
    //        canvas.drawLine(1000F , 0F ,-1000F , 0F , paint2)
    //        canvas.drawLine( 0F ,-1000F , 0F , 1000F , paint2)
    }
    

    闪电的实现还是很简单的,因为不涉及到图形的变化,只有一个简单的位移效果

    霜(水)之哀伤篇

    设计思路

    水滴的实现相对对于闪电来说麻烦一些,一是水滴本身不是很好绘制,又因为水滴在下落的过程中存在变化,最后选择通过贝塞尔曲线实现.二是水滴和背景之间的交互,在水滴未完全下落到背景中的时候,水滴背景的上部有个向下凹陷的过程,这个不是闪电背景的简单变化可能做到的.最后也是使用贝塞尔曲线绘制的一个圆弧的区域遮盖来实现.

    整理需要变化的元素是水滴及顶部的遮盖.都是使用贝塞尔曲线实现的.顶部的凹陷随着水滴的下落不断凹陷,直至水滴脱离顶部后再渐渐回落.主要是找到水滴完全脱离的时间当做顶部凹陷的关键点就好.水滴下落的过程中是需要变化,最开始可能稍微瘦长一些,然后相对变扁.

    代码实现

    • 水滴背景的实现
    //绘制水滴背景
        private fun  drawBaseButton(canvas: Canvas , index: Float) {
            //计算水滴半进入区间(确定水滴背景上部变化范围)
            val waterRand = (baseR * 1.25 * coefficient) / ((baseR * 1.25 * coefficient) + baseR)
    
            //设置画笔
            val paint = Paint()
            paint.color = Color.parseColor("#45AAE1")
            paint.style = Paint.Style.FILL
    
            //绘制水滴背景下半部分的(此部分不需要变换)
            canvas.drawArc(RectF(-baseR, -baseR, baseR, baseR), 180F , 180F,true , paint)
    
            //设置点list 顺序存储相关路径及关键点
            val points : MutableList<Point> = ArrayList()
            points.add(Point(-baseR , 0F))
            points.add(Point(-baseR , baseR * C))
            points.add(Point(-baseR * C , baseR ))
    
            var baseButtonTop : Float
            //根据index判断上部的形态
            if (index <= waterRand){
                baseButtonTop = baseR - (baseR * coefficient * index) * 2
            }else{
                baseButtonTop = baseR - (baseR * coefficient) * 2 + (baseR * coefficient * index) * 2
                if (baseButtonTop > baseR){
                    baseButtonTop = baseR
                }
            }
    
            points.add(Point(0F, baseButtonTop))
    
            points.add(Point(baseR * C , baseR))
            points.add(Point(baseR , baseR * C ))
            points.add(Point(baseR , 0F))
    
            val path = Path()
            //画笔移动到指定位置(不移动的话通过贝塞尔绘制的图形会有误差)
            path.moveTo(points[0].x , points[0].y)
            //设置贝塞尔曲线
            path.cubicTo(
                    points[1].x , points[1].y ,
                    points[2].x , points[2].y ,
                    points[3].x , points[3].y)
    
            path.cubicTo(
                    points[4].x , points[4].y ,
                    points[5].x , points[5].y ,
                    points[6].x , points[6].y)
            //绘制
            canvas.drawPath(path, paint)
        }
    
    • 水滴的实现
    private fun  drawDrops(canvas: Canvas , index: Float) {
            //设置水滴半径
            val baseR = baseR * coefficient
            val index = 1 - index
    
            //根据index将画布中心移动到对应位置
            canvas.translate( 0F , (this.baseR * 1.125F + baseR)* index - this.baseR / 8)
    
            //设置画笔
            val paint = Paint()
            paint.style = Paint.Style.FILL
            paint.color = viewBackgroundColor
            //存储关键点坐标
            val points : MutableList<Point> = ArrayList()
            points.add(Point(-baseR , 0F))
    
            //水滴顶部变换系数
            val topCoefficient = 1.5F
    
            points.add(Point(-baseR , baseR * C))
            points.add(Point(-baseR * C , baseR ))
            points.add(Point(0F, (1.5 * baseR + baseR * topCoefficient * index).toFloat()))
    
            points.add(Point(baseR * C , baseR))
            points.add(Point(baseR , baseR * C ))
            points.add(Point(baseR , 0F))
            //水滴底部变换系数
            //这两个变换系数使得水滴在下落的过程中渐渐变扁
            val bottomCoefficient = 0.3F
            val tempBaseR = (baseR - baseR * bottomCoefficient * index)
            points.add(Point(baseR , -tempBaseR * C))
            points.add(Point(baseR * C , -tempBaseR ))
            points.add(Point(0F, -tempBaseR))
    
            points.add(Point(-baseR * C , -tempBaseR))
            points.add(Point(-baseR , -tempBaseR * C ))
            points.add(Point(-baseR , 0F))
    
            //设置四个部分(90°一个部分)的贝塞尔曲线
            //关于贝塞尔曲线的事情...  感觉可以再做点记录
            val path = Path()
            path.moveTo(points[0].x , points[0].y)
            path.cubicTo(
                    points[1].x , points[1].y ,
                    points[2].x , points[2].y ,
                    points[3].x , points[3].y)
    
            path.cubicTo(
                    points[4].x , points[4].y ,
                    points[5].x , points[5].y ,
                    points[6].x , points[6].y)
    
            path.cubicTo(
                    points[7].x , points[7].y ,
                    points[8].x , points[8].y ,
                    points[9].x , points[9].y)
    
            path.cubicTo(
                    points[10].x , points[10].y ,
                    points[11].x , points[11].y ,
                    points[12].x , points[12].y)
    
            //绘制图形
            canvas.drawPath(path, paint)
        }
    

    偷懒的原因所以直接使用背景色做的一个简单的遮盖,没有使用遮罩(其实闪电的部分也是).

    相对来说水滴的实现最为满意,主要的预期效果都成功的实现出来了,整体看来效果还是可以的

    火之高兴篇

    设计思路

    虽然整体看来,应该是一个难度中等的动画,但是在设计的过程中经历了空手用贝塞尔画火焰(最开始的想法本是火焰本身也是会动的),火焰扰动效果的实现(这个最为艰难,主要是不知道怎么控制火焰扰动的效果,其次是遮罩层的使用,具体的坑会另开文字来讲解),最后只能简单做了个底部向上的遮罩层来当做火焰的扰动情况

    所以其实就是绘制一个火焰的形状,然后再用个遮罩层来遮盖实现火焰的扰动

    代码实现

    因为背景没有什么特效,就不贴背景的代码了

    • 整体火焰效果控制

    因为火焰需要展示绘制完成的火焰和遮罩层中相交的部分,要使用PorterDuffXfermode相关的方法,所以在绘制中将原图层和遮罩层分开设计

    private fun  drawFires(canvas: Canvas , index: Float) {
        //设置火焰半径
    
        //设置原图层(火焰绘制)
        val srcB = makeSrc(2 * baseR.toInt(), 2 * baseR.toInt(), index)
        //设置遮罩层
        val dstB = makeDst(2 * baseR.toInt(), 2 * baseR.toInt(), index)
    
        val paint = Paint()
        canvas.saveLayer(-baseR, -baseR, baseR , baseR, null, Canvas.ALL_SAVE_FLAG)
    
        //绘制遮罩层
        canvas.drawBitmap(dstB,  -baseR/2,  -baseR/2, paint)
        //设置遮罩模式为SRC_IN显示原图层中原图层与遮罩层相交部分
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        canvas.drawBitmap(srcB, -baseR/2, -baseR/2, paint)
        paint.xfermode = null
    
    }
    
    • 绘制原图层(火焰本身的绘制)
    fun makeSrc(w: Int, h: Int , index :Float): Bitmap {
           val bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
           val canvas = Canvas(bm)
    
           canvas.translate(baseR / 2F, baseR / 2F)   // 将坐标系移动到画布中央
    
           val index = index * 0.5F + 0.5F
           val baseR = baseR * coefficient * index
    
           //设置画笔
           val paint = Paint()
           paint.style = Paint.Style.FILL
           paint.color = viewBackgroundColor
           paint.strokeWidth = 10F
           //存储关键点坐标
           val points : MutableList<Point> = ArrayList()
    
           //整体火焰是由六个贝塞尔曲线绘制成的
           points.add(pointFactory( 190F , baseR))
           points.add(pointFactory( 280F , baseR / 3F * 4))
           points.add(pointFactory( 320F ,  baseR / 6F))
           points.add(pointFactory( 350F , baseR))
    
           points.add(pointFactory( 10F , baseR))
           points.add(pointFactory( 30F , baseR / 3F* 2))
           points.add(pointFactory( 50F , baseR / 3F ))
    
           points.add(pointFactory( 60F , baseR / 6F * 3))
           points.add(pointFactory( 60F , baseR / 6F * 4))
           points.add(pointFactory( 50F , baseR / 6F * 5))
    
           points.add(pointFactory( 85F , baseR / 6F * 5))
           points.add(pointFactory( 120F , baseR / 6F * 5))
           points.add(pointFactory( 150F , baseR ))
    
           points.add(pointFactory( 160F , baseR / 9F * 7))
           points.add(pointFactory( 170F , baseR / 9F * 5))
           points.add(pointFactory( 180F , baseR / 9F * 3))
    
           points.add(pointFactory( 200F , baseR / 3F))
           points.add(pointFactory( 195F , baseR / 3F * 2))
           points.add(pointFactory( 190F , baseR ))
    
           val path = Path()
           path.moveTo(points[0].x , points[0].y)
    
           for (index in 0..((points.size - 1) / 3 - 1) ){
               path.cubicTo(
                       points[3 * index + 1].x , points[3 * index + 1].y ,
                       points[3 * index + 2].x , points[3 * index + 2].y ,
                       points[3 * index + 3].x , points[3 * index + 3].y)
           }
    
           //绘制图形
           canvas.drawPath(path, paint)
    
           return bm
       }
    
    • 绘制遮罩层(火焰的扰动效果)
    fun makeDst(w: Int, h: Int, index :Float): Bitmap {
        val bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bm)
        canvas.translate(baseR / 2F, 0F)
    
        val paint = Paint()
        paint.color = Color.YELLOW
    
        val dstLength = baseR * coefficient * index * 2
    
        val rectf = RectF(-dstLength, -dstLength, dstLength, dstLength)
    
        //没找到合适的扰动效果,只能简单实现一个遮罩效果
        canvas.drawArc(rectf , 0F , 360F , true, paint)
    
        return bm
    }
    

    火焰来说,虽然需要的效果代码都实现了,但是缺少设计,整体的效果到时不尽如人意.针对效果来说还有很多的优化空间
    【附录】

    资料图

    需要资料的朋友可以加入Android架构交流QQ群聊:513088520

    点击链接加入群聊【Android移动架构总群】:加入群聊

    获取免费学习视频,学习大纲另外还有像高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)等Android高阶开发资料免费分享。

    相关文章

      网友评论

        本文标题:Android自定义View - 元素按钮的

        本文链接:https://www.haomeiwen.com/subject/akwlsqtx.html