当前位置:首页 > 芯闻号 > 充电吧
[导读]前言使用百度贴吧客户端的时候发发现加载的小动画挺有意思的,于是自己动手写写看。想学习自定义View以及自定义动画的小伙伴一定不要错过哦。   读者朋友需要有最基本的canvas绘图功底,比如画笔Pai

前言

使用百度贴吧客户端的时候发发现加载的小动画挺有意思的,于是自己动手写写看。想学习自定义View以及自定义动画的小伙伴一定不要错过哦。 
  读者朋友需要有最基本的canvas绘图功底,比如画笔Paint的简单使用、Path如何画直线等简单的操作,不熟悉也没关系,下文带大家撸代码的时候会简单的讲一下。 
  此篇文章用到如下知识点:

  1)、自定义View的测量 
  2)、自定义View属性的自定义及使用 
  3)、Path绘制贝塞尔曲线 
  4)、Canvas的裁剪 
  5)、用ValueAnimator控制动画 
  6)、Canvas文字居中

  好了,开始正文!


一、准备工作 1、效果图

2、动画拆解

  直观的看我们要实现三个方面 
  1)、波浪动画(蓝色部分) 
  2)、不规则的文字(白色的半个“贴”字) 
  3)、控件显示部分限制成圆形 
  

3、技术分析

  1)、波浪动画 
  要实现波浪动画,首先要绘制出波浪的形状,其次再让他动起来。波浪线看起来有点像是正弦或者余弦函数,但是Android的Path并没有提供绘制正余弦图形的函数,但是提供了一个功能更强大的曲线——贝塞尔曲线,贝塞尔曲线分为二阶、三阶及多阶,本案例里使用的是二次贝塞尔曲线,如下图所示,二阶贝塞尔曲线需要三个点才可以确定

  
我们来看一下Android里贝塞尔曲线的源码:

/* @param x1 The x-coordinate of the control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end  on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        native_quadTo(mNativePath, x1, y1, x2, y2);
    }

由注解可以看出来quadTo(float x1, float y1, float x2, float y2)的四个参数分别是控制点的x,y坐标,结束点的x,y坐标,少了一个开始点呀!不要着急,开始点是Path路径的上一次结束的点,如果你的Path没有绘制过路径,那么Path的最后一个点坐标就是(0,0)如果想自己定义起始点位置,就用Path.moveTo(float x, float y)即可。 
但是每次都需要指定具体的控制点和结束点既麻烦又容易出错,那么就需要rQuadTo(float dx1, float dy1, float dx2, float dy2)出马了,rQuadTo跟quadTo的区别在于rQuadTo使用的是相对起始点的坐标,而不是具体的坐标点,举个例子,如下代码效果等价:


 //使用quadTo
    Path path=new Path();
    path.moveTo(100,100);
    path.quadTo(150,0,200,100);

    //使用rQuadTo
    Path path=new Path();
    path.moveTo(100,100);
    path.rQuadTo(50,-100,100,0)

此时画笔最后的落点都为(200,100)。 
  画波浪线的技术难点解决了那么如何让波浪动起来呢,想动起来肯定需要波浪在水平方向移动,那么我们需要画一个很长很长的波浪让他移动,这样就实现了上下起伏效果,但是这样需要画无数多条贝塞尔曲线,肯定不行,这时就用到万能的数学理论——周期函数了,如果我们绘制两个周期的贝塞尔曲线,每次只让它显示一个周期,然后等第二周期显示结束的时候再从头开始,这样就造成了无限周期的假象,如下图 
  初始位置为1,向右前进,当走到2位置的时候重置成3的位置,即1原始的位置,如此往复就成了绵绵不绝的波浪了 
  
  做成效果如下:黄色区域就是要显示的区域,蓝色竖线是波浪线两个周期的总长度 
  

2)、不规则的文字

我们可以看到圆球里的“贴”字在波浪区域显示的是白色,波浪区域之外显示的是蓝色,Android并不支持给文字部分区域着色的功能,那么我们只能靠控制显示区域让文字只显示特定形状,强大的Canvas正好有画布裁剪功能,通过裁剪画布就能控制绘制区域,画布的裁剪可以用Canvas.clipPath(Path path)实现,传入一个闭合的Path既可以随心所欲裁剪画布,裁剪示意图如下 
 
利用波浪形闭合路径讲画布裁剪成波浪形,那么在此接下来的Canvas绘制操的内容只能在这个波浪形区域里显示,这样就解决了文字的部分区域显示问题。那么接下来我们只用在相同位置绘制相同字体、字号不同色的文字即可实现一个文字显示两种颜色了(注意:实际操作的时候,被裁剪的文字要盖在未被裁减的文字的上边,即先在画布裁剪之前绘制蓝色的“贴”字,然后再裁剪画布再在裁剪后的画布上绘制白色的“贴”)

3)、控件显示部分限制成圆形

经过2)的分析,将显示部分限制在圆形区域里不是易如反掌吗,使用一个圆形的Path裁剪画布即可。感兴趣的同学也可以尝试BitmapShader或者Xfermode来将显示区域变成圆形

好了,最主要的步骤都分析完了,上一张图更直观地展示一下绘制流程 
 
图中可以看出波浪形的闭合Path有两个作用,一个是负责裁剪画布,一个是负责绘制蓝色,其实只用第一个功能即可,此处只是方便分解步骤。

二、代码实现
既然是自定义控件,那就要有通用性比如下边的效果: 
 
loading小球需文字和颜色都可以改变,所以我们要给自己的控件添加这两个属性。首先在“res/values/”路径下新建一个attrs.xml文件,在里边定义如下属性:


接下来开始自定义View 
  复写三个构造函数,将单参数和双参数的构造函数的super方法都改为this,保证无论调用哪个构造方法都会跳到三个参数的构造方法中,这样就可以偷懒只用在三个参数的构造方法里初始化各种参数了

public class Wave extends View {
    public Wave(Context context) {
        this(context,null);
    }

    public Wave(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public Wave(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化参数
        init(context,attrs);
    }
}

接下来是初始化函数,在此处我们获取到自定义的颜色及文字参数,并初始化各种画笔,代码比较简单,看注释内容即可

private void init(Context context, AttributeSet attrs) {
        //获取自定义参数值
        TypedArray array =  context.obtainStyledAttributes(attrs, R.styleable.Wave);
        //自定义颜色和文字
        color = array.getColor(R.styleable.Wave_color, Color.rgb(41, 163, 254));
        text = array.getString(R.styleable.Wave_text);
        array.recycle();
        //图形及路径填充画笔(抗锯齿、填充、防抖动)
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(color);
        mPaint.setDither(true);
        //文字画笔(抗锯齿、白色、粗体)
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.WHITE);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        //闭合波浪路径
        path = new Path();
    }


接下来是生成波浪线的方法,示意图如下: 
 
  将Path起点移动到最左边粉色点处,然后绘制两个周期的长度的波形(一上一下是一个周期),每个周期在x轴的跨度为此控件的宽度控制点距波形的轴线的绝对高度是整个控件的3/20,当然想让波形波动幅度大的话这个比例可以随意调整,接下来就用前边讲到的rQuadTo( )来生成闭合的波浪图形,其中mWidth为控件的宽度,mHeight为控件的高度

private Path getActionPath(float percent) {
        Path path = new Path();
        int x = -mWidth;
        //当前x点坐标(根据动画进度水平推移,一个动画周期推移的距离为一个周期的波长)
        x += percent * mWidth;
        //波形的起点
        path.moveTo(x, mHeight / 2);
        //控制点的相对宽度
        int quadWidth = mWidth / 4;
        //控制点的相对高度
        int quadHeight = mHeight / 20 * 3;
        //第一个周期波形
        path.rQuadTo(quadWidth, quadHeight, quadWidth * 2, 0);
        path.rQuadTo(quadWidth, -quadHeight, quadWidth * 2, 0);
        //第二个周期波形
        path.rQuadTo(quadWidth, quadHeight, quadWidth * 2, 0);
        path.rQuadTo(quadWidth, -quadHeight, quadWidth * 2, 0);
        //右侧的直线
        path.lineTo(x + mWidth * 2, mHeight);
        //下边的直线
        path.lineTo(x, mHeight);
        //自动闭合补出左边的直线
        path.close();
        return path;
    }

上边代码所表示的闭合路径如下图 

接下来就是重头戏onDraw了

protected void onDraw(Canvas canvas) {
        //底部的字
        textPaint.setColor(color);
        drawCenterText(canvas, textPaint, text);
        //上层的字
        textPaint.setColor(Color.WHITE);
        canvas.save(Canvas.CLIP_SAVE_FLAG);
            //裁剪成圆形
            Path o = new Path();
            o.addCircle(mWidth / 2, mHeight / 2, mWidth / 2, Path.Direction.CCW);
            canvas.clipPath(o);
            //生成闭合波浪路径
            path = getActionPath(currentPersent);
            //画波浪
            canvas.drawPath(path, mPaint);
            //裁剪文字
            canvas.clipPath(path);
            drawCenterText(canvas, textPaint, text);
        canvas.restore();
    }

这里绘制思路是:在canvas上绘制蓝色的文字 ——>将画布裁剪成圆形 ——>绘制波浪 ——>裁剪画布成波浪形 ——>绘制文字,这里一定要注意绘制顺序,先绘制的在下部,后绘制的在上部。思路简单只用注意到两次clip即可。 
  细心的朋友一定看到了一个函数drawCenterText(canvas, textPaint, text)没错,这个函数就是讲文字绘于控件正中心的方法。有的读者可能一直在使用Canvas.drawText( String text, float x, float y, Paint paint) 这个方法,但是参数中的(x,y)到底是哪个坐标呢,是文字左上角的点的坐标吗?不是的,接下来我们用代码验证一下这个(x,y)到底在文字的哪个部位

        canvas.drawText(text,600,200,textPaint);
        canvas.drawCircle(600,200,3,paint);
        canvas.translate(600, 200);
        Rect bgRect=new Rect(0,0,1000,400);
        canvas.drawRect(bgRect,bgPaint);

        Rect textBound=new Rect();
        textPaint.getTextBounds(text,0,text.length(),textBound);
        paint.setColor(Color.RED);
        canvas.drawRect(textBound,paint);

        Paint.FontMetrics metrics=textPaint.getFontMetrics();
        paint.setColor(Color.RED);
        // ascent 橙色
        paint.setColor(Color.rgb(255,126,0));
        canvas.drawLine(0, metrics.ascent, 500,metrics.ascent, paint);
        // descent
        paint.setColor(Color.rgb(255,0,234));
        canvas.drawLine(0, metrics.descent, 500, metrics.descent, paint);
        // top
        paint.setColor(Color.DKGRAY);
        canvas.drawLine(0, metrics.top, 500, metrics.top, paint);
        // bottom
        paint.setColor(Color.GREEN);
        canvas.drawLine(0, metrics.bottom, 500, metrics.bottom, paint);

 首先是在画布的(600,200)处画上文字,为了方便观察(600,200)在文字的什么部位,我在(600,200)处画了一个半径3像素的圆圈。然后平移画布到(600,200)的地方然后依次画出了文字的边框图以及FontMetrics信息里的top、ascent、descent、bottom信息 
我把运行结果截图做了处理,方便大家看 
 
  从结果看(600,200)那个蓝色的点并不是在文字的左上角,而是左下角,这个点所在的y坐标即是大家常说的BaseLine的位置,那现在这个函数Canvas.drawText( String text, float x, float y, Paint paint)就可以理解为——将文字的基准点放在(x,y)处,那么这个基准点可以改变吗?答案是肯定的,可以通过绘制文字的画笔的setTextAlign(Align align)方法设置为Paint.Align.CENTER或者Paint.Align.RIGHT,如果不设置的话默认是Paint.Align.LEFT。读者朋友们有兴趣的话可以试试设置成CENTER之后(600,200)的蓝圈圈是不是跑到了文字的中部呢?从上图我们也可以看出,整个文字是介于FontMetrics.topFontMetrics.bottom之间。 
  好了,贴上文字居中的代码,相信认真看上边那段话的朋友一定能轻松读懂

  private void drawCenterText(Canvas canvas, Paint textPaint, String text) {
        Rect rect = new Rect(0, 0, mWidth, mHeight);
        textPaint.setTextAlign(Paint.Align.CENTER);

        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float top = fontMetrics.top;
        float bottom = fontMetrics.bottom;

        int centerY = (int) (rect.centerY() - top / 2 - bottom / 2);

        canvas.drawText(text, rect.centerX(), centerY, textPaint);
    }

分析好上边的代码 我们就能绘制出一个静态的小球了,动画既然要动,肯定就像汽车一样需要一个”引擎”,在上面说到的绘制波浪路径的函数中我们忽略了getActionPath(float percent)的参数percent,这个参数即是当前动画的进度,那么我们如何来制造这个进度呢?需要怎样把这个动画“引擎”点燃呢。我们可以通过各种手段计时,生成一个计时Thread或者自己写一个Handler等等,只要能均匀的生成进度即可。 
  本文中用到一个巧妙的定时器ValueAnimator 大家常说的属性动画ObjectAnimator就是它的一个子类,使用它来作为动画的引擎再方便不过了,从字面翻译”ValueAnimator”那就是“值动画者”直译虽然low但是恰恰更好理解,就是让数值动起来,从什么值动到什么值呢? 
ValueAnimator animator = ValueAnimator.ofFloat(0, 1); 
这句话就是定义一个值从0变化到1的一个animator,我们的percent值就是从0变化到1的中间过程值,那么怎么得到这个过程值呢?——监听器!对!

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float percent = animation.getAnimatedFraction();
            }
        });

那么数值从0变到1需要多久呢?怎么能无限重复呢?重复的时候是重头开始还是反转进行呢?别急下面三句话就是让动画无限重复,每次从头开始,一个周期1000毫秒

 animator.setDuration(1000);
 animator.setRepeatCount(ValueAnimator.INFINITE);
 animator.setRepeatMode(ValueAnimator.RESTART);

好了,引擎设置好了,发动 
animator.start(); 
上效果 
 
WTF!这是什么鬼,为什么鬼畜地慢几拍? 
打印出来横坐标看看

07-09 18:18:47.308  E/Jcs: getActionPath: -21
07-09 18:18:47.326  E/Jcs: getActionPath: -15
07-09 18:18:47.342  E/Jcs: getActionPath: -10
07-09 18:18:47.359  E/Jcs: getActionPath: -5
07-09 18:18:47.375  E/Jcs: getActionPath: -2
07-09 18:18:47.392  E/Jcs: getActionPath: 0
07-09 18:18:47.409  E/Jcs: getActionPath: 0

最后几拍的数值差好像不太对呀!拍拍脑门突然一想,我的动画不均匀是忘记设置一个均匀的插值器了!哎!

animator.setInterpolator(new LinearInterpolator());

补上一个线性插值器,整个世界都顺畅了 

百度Loading小球Github源码

三、结语

静态配图是用Illustrator配合ps做的,gif是在www.ezgif.com上生成的

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

罗德与施瓦茨与SmartViser携手开发了一种用于测试符合欧盟销售的智能手机和平板电脑的新Energy Efficiency Index(EEI)标签法规的解决方案。该解决方案的核心是R&S CMX500,这是...

关键字: 智能手机 Android iOS

(全球TMT2023年8月24日讯)2023年8月23日,时值实时3D引擎Unity在华设立合资公司Unity中国一周年之际,Unity中国正式推出Unity中国版引擎——团结引擎。Unity全球CEO John Ri...

关键字: UNITY CE Android 开发者

报告显示:全球电商 App 获客花费接近50亿美元 北京2023年8月23日 /美通社/ -- 全球营销衡量与体验管理平台 AppsFlyer 近日发布《2023 电商 App 营销现状报告》。尽管面临全球经...

关键字: APPS BSP iOS Android

数字机顶盒是一种数字技术下的多媒体娱乐中心,可以实现电视节目接收、播放、存储、网络应用等多种功能。随着科技的发展,数字机顶盒的设计方案也在不断进步和优化。本文将介绍数字机顶盒设计的几种实现方案。

关键字: 数字机顶盒 Android Linux

21ic 近日获悉,原小米 9 号创始员工李明在社交媒体平台公布了旗下首款产品乐天派桌面机器人,为全球首款 Android 桌面机器人,面向极客和发烧友的 AI + 机器人。据悉,李明两个月前宣布创业并进军 AI 领域,...

关键字: 小米 Android 桌面机器人 AI

尽管安装增长放缓,全球游戏 App 获客花费仍高达 267 亿美元 经济低迷导致 2023 游戏 App 营销优先考虑收入指标,用户增长次之 北京2023年3月9日 /美通社/ -- 今天,全球营销衡量与体验管理平台...

关键字: APPS iOS Android BSP

量子计算领域的新里程碑,来了! 谷歌科学家证明,通过增加量子比特的数量,就能降低量子计算的错误率。

关键字: 谷歌 Android Windows

「卫星通讯」正在被普及到每一台智能手机当中。普及的动机并非是消费市场的一个刚需,其实更像是将差异化的功能「抹平」成一个标配。时下,支持「卫星通讯」功能的智能手机只有苹果的 iPhone 14 系列与华为的 Mate 50...

关键字: 卫星通讯 Android 智能手机 iPhone

Android是Google开发的操作系统,支持多种指令集架构 (ISA),包括Arm和x86,多数使用Android的设备都采用Arm架构芯片组。新兴RISC-V架构是免费开放指令集架构,任何人都可用它设计芯片,且无需...

关键字: 谷歌 Android RISC-V架构

智能手机并非每年都取得重大进展,这导致越来越多的人将手机保留两年、三年或四年。不过,普通的 Android 手机能否在遇到问题之前使用那么久?

关键字: Android 安卓 谷歌 智能手机
关闭
关闭