前言

本篇文章记录Android下实现自定义百度贴吧的水波纹Loading效果,主要涉及到的知识点是画布Canvas、画布上绘制圆drawCircle 、绘制文字drawText、绘制直线drawLine、贝塞尔曲线lineTo , quadTo,如何实现水波纹效果,如何实现蓝色文字和白色文字叠加的显示效果,下面,梳理下详细的自定义过程。

说明

1、实现效果

实现的效果如下,水波纹上方显示的是蓝色的字,水波纹的下方显示的是白色的文字。

android 很多button android button loading_自定义



2、实现步骤

下面先梳理下实现的步骤,看上面的动画效果中,有两种颜色的“贴”字,首先是蓝色,白色“贴”从视觉上看是在水波纹之上的,那么绘制的顺序如下:

  • 绘制蓝色“贴”
  • 绘制水波纹
  • 绘制白色“贴”

绘制文字很好处理,这里主要注意一下,文字如何居中显示。那么主要看下水波纹如何实现的,下图是将动画过程拆分出的两帧。

  • 首先绘制一个圆,以圆的直径为长度绘制两个周期的正弦波
  • 将正弦波向右平移,最大平移长度为圆的直径长度后,重置绘制为图1的状态。
  • 如何只保留实际的效果呢?这里就要用的Canvas的裁剪方法clipPath,裁剪的路径就是黄色和圆中蓝色的区域。
  • 两个周期的正弦波路径是已知的,黄色区域底部最大的距离是圆的半径,将P1 、P2、P3、P4四个点连接起来形成要剪裁的路径。

android 很多button android button loading_自定义View_02

实现

通过上面的分析,应该对效果有了个直观的概念,下面用代码实现。

1、初始化用到的画笔、以及声明自定义View的属性,文字内容和颜色(同水波纹颜色)可以自定义。

values下新建attrs.xml,新建名称为TieBaView的属性声明文件

<declare-styleable name="TieBaView">
       <!--字体和圆的颜色-->
       <attr name="textColor" format="color"/>

       <!--文字内容-->
       <attr name="text" format="color"/>
   </declare-styleable>
//声明变量
    private var mWidth = 0
    private var mHeight = 0
    private var centerPointX = 0f
    private var centerPointY = 0f
    private var textRect: Rect = Rect()
    private var textX = 0f
    private var textY = 0f
    private var radius = 200f
    private var fraction = 0.0f
    private var text:String = "贴"
    private var textColor:Int = 0
   
   
    /**
     * 底部贴字
     */
    private val bottomTextPaint = Paint().apply{
        textSize = 180f
        isDither = true
        isAntiAlias = true
        style = Paint.Style.FILL
    }
    
    /**
     * 顶部贴字
     */
    private val topTextPaint = Paint().apply {
        textSize = 180f
        color = context.getColor(R.color.colorWhite)
        isDither = true
        isAntiAlias = true
        style = Paint.Style.FILL
    }

    /**
     * 圆
     */
    private val circlePaint = Paint().apply {
        strokeWidth = 20f
        isDither = true
        isAntiAlias = true
        style = Paint.Style.FILL
    }

    init {
        initAttrs(attributeSet)
        initAnimator()
    }

    /**
     * 初始化属性
     */
    private fun initAttrs(attr:AttributeSet) {
        val ta = context.obtainStyledAttributes(attr,R.styleable.TieBaView)
        text = ta.getString(R.styleable.TieBaView_text).toString()
        textColor = ta.getColor(R.styleable.TieBaView_textColor,context.getColor(R.color.colorBlue))
        ta.recycle()

        bottomTextPaint.color = textColor
        circlePaint.color = textColor
    }

2、重写onSizeChanged方法,计算圆的中心点坐标、以及文字居中显示处理

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = w
        mHeight = h
        //自定义view中心点x坐标
        centerPointX = mWidth / 2f
        //自定义view中心点u坐标
        centerPointY = mHeight / 2f
        //获取文字的基线
        topTextPaint.getTextBounds(text,0,text.length,textRect)
        //计算文字摆放位置
        calculateTextPos()
    }

   /**
     * 计算文字绘制坐标
     */
    private fun calculateTextPos() {
        textX = -abs(textRect.right - textRect.left) / 2f
        textY = -abs(textRect.bottom - textRect.top) / 2f
    }

3、重写onDraw方法绘制处理

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //将画布平移到屏幕中央
        canvas.withTranslation(centerPointX,centerPointY) {
            //绘制底部文字
            canvas.drawText(text, - (textRect.right - textRect.left) / 2f,- (textRect.bottom + textRect.top) / 2f,bottomTextPaint)
            //计算路径,canvas剪裁保留计算的路径
            clipPath(generateWavePath(percent))
            //绘制圆形
            drawCircle(0f,0f,radius,circlePaint)
            //绘制顶部文字
            drawText(text, - (textRect.right - textRect.left) / 2f,- (textRect.bottom + textRect.top) / 2f,topTextPaint)
        }
    }

    /**
     * 平移正弦波,获取对应的路径path
     */
    private fun generateWavePath(fraction:Float):Path{

        val clipPath = Path()
        val startX = - 3 * radius + 2 * radius * percent
        clipPath.moveTo(startX,0f)
        //控制点的距离上下起点宽度
        val quadWidth = radius / 2
        //控制点的高度,是曲线的振幅,这里可以自己定义
        val quadHeight = radius * 2 / 6
        clipPath.apply {
            //绘制第一段曲线
            quadTo(startX + quadWidth , - quadHeight ,startX + 2 * quadWidth ,0f)
            moveTo(startX + 2 * quadWidth ,0f)
            quadTo( startX + 3 * quadWidth ,  quadHeight , startX + 4 * quadWidth,0f)

            // 绘制第二段曲线
            moveTo( startX + 4 * quadWidth,0f)
            quadTo(startX + 5 * quadWidth ,-quadHeight ,startX + 6 * quadWidth  , 0f)
            moveTo(startX + 6 * quadWidth  , 0f)
            quadTo(startX + 7 * quadWidth  ,quadHeight ,  startX + 8 * quadWidth, 0f)

            lineTo(startX + 8 * quadWidth,quadHeight * 10)
            lineTo(startX ,quadHeight * 10)
            lineTo(startX,0f)
        }
        return clipPath
    }

下面看下CanvasclipPath方法

/**
     * 裁剪指定相交的路径,这里传入我们计算好生成的路径即可
     * 
     * Intersect the current clip with the specified path.
     * @param path The path to intersect with the current clip
     * @return     true if the resulting clip is non-empty
     */
    public boolean clipPath(@NonNull Path path) {
        return clipPath(path, Region.Op.INTERSECT);
    }

4、使用动画,生成正弦波平移的系数,让正弦波平移起来

上面在调用平移正弦波,生成对应路径方法generateWavePath(fraction:Float)中传入了系数fraction,上面已经分析了,正弦波向右平移的最大宽度为圆的直径,只要fraction的范围在0-1之间变化即可,当平移了圆的直径后立刻fraction立刻置为0,这里使用属性动画中的ValueAnimator来实现。

/**
     * 初始化动画
     */
    private fun initAnimator() {
        val animator = ValueAnimator.ofFloat(0f,1f).apply {
            duration = 1500
            repeatMode = ValueAnimator.RESTART
            repeatCount = ValueAnimator.INFINITE
            interpolator = LinearInterpolator()
        }
        
        animator.addUpdateListener { a ->
            //获取当前动画的进度
            fraction = a.animatedFraction
            //重新绘制
            invalidate()
        }
        animator.start()
    }

5、XML中使用

<com.xn.customview.widget.TieBaView
     android:id="@+id/tbView"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:text="@string/tie"
      app:textColor="@color/colorBlue" />



总结

本篇自定义贴吧水波纹Loading效果,主要熟悉下画布的有关特性和Path路径的有关方法,画布在自定义View中是最重要的角色之一,也是要着重掌握的知识。