别裁伪体亲风雅,转益多师是汝师。这篇文章主要讲述在Android上仿百度贴吧客户端Loading图标小球相关的知识,希望能为你提供帮助。
文章图片
封面预览
前言
使用百度贴吧客户端的时候发发现加载的小动画挺有意思的,于是自己动手写写看。想学习自定义View以及自定义动画的小伙伴一定不要错过哦。
读者朋友需要有最基本的canvas绘图功底,比如画笔Paint的简单使用、Path如何画直线等简单的操作,不熟悉也没关系,下文带大家撸代码的时候会简单的讲一下。
此篇文章用到如下知识点:
- 1)、自定义View的测量
- 2)、自定义View属性的自定义及使用
- 3)、Path绘制贝塞尔曲线
- 4)、Canvas的裁剪
- 5)、用ValueAnimator控制动画
- 6)、Canvas文字居中
好了,开始正文!
一、准备工作
1、效果图
文章图片
loading小球
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 endon 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);
- }
但是每次都需要指定具体的控制点和结束点既麻烦又容易出错,那么就需要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);
画波浪线的技术难点解决了那么如何让波浪动起来呢,想动起来肯定需要波浪在水平方向移动,那么我们需要画一个很长很长的波浪让他移动,这样就实现了上下起伏效果,但是这样需要画无数多条贝塞尔曲线,肯定不行,这时就用到万能的数学理论——周期函数了,如果我们绘制两个周期的贝塞尔曲线,每次只让它显示一个周期,然后等第二周期显示结束的时候再从头开始,这样就造成了无限周期的假象,如下图
初始位置为1,向右前进,当走到2位置的时候重置成3的位置,即1原始的位置,如此往复就成了绵绵不绝的波浪了
文章图片
绵延原理
做成效果如下:黄色区域就是要显示的区域,蓝色竖线是波浪线两个周期的总长度
文章图片
连绵不绝的波浪线
2)、不规则的文字
我们可以看到圆球里的“贴”字在波浪区域显示的是白色,波浪区域之外显示的是蓝色,Android并不支持给文字部分区域着色的功能,那么我们只能靠控制显示区域让文字只显示特定形状,强大的Canvas正好有画布裁剪功能,通过裁剪画布就能控制绘制区域,画布的裁剪可以用Canvas.clipPath(Path path)实现,传入一个闭合的Path既可以随心所欲裁剪画布,裁剪示意图如下
文章图片
裁剪文字
利用波浪形闭合路径讲画布裁剪成波浪形,那么在此接下来的Canvas绘制操的内容只能在这个波浪形区域里显示,这样就解决了文字的部分区域显示问题。那么接下来我们只用在相同位置绘制相同字体、字号不同色的文字即可实现一个文字显示两种颜色了(注意:实际操作的时候,被裁剪的文字要盖在未被裁减的文字的上边,即先在画布裁剪之前绘制蓝色的“贴”字,然后再裁剪画布再在裁剪后的画布上绘制白色的“贴”)
3)、控件显示部分限制成圆形
经过2)的分析,将显示部分限制在圆形区域里不是易如反掌吗,使用一个圆形的Path裁剪画布即可。感兴趣的同学也可以尝试BitmapShader或者Xfermode来将显示区域变成圆形
好了,最主要的步骤都分析完了,上一张图更直观地展示一下绘制流程
文章图片
整体分析图
图中可以看出波浪形的闭合Path有两个作用,一个是负责裁剪画布,一个是负责绘制蓝色,其实只用第一个功能即可,此处只是方便分解步骤。
二、代码实现
文章只贴出主要代码,完整代码文末提供链接
既然是自定义控件,那就要有通用性比如下边的效果:
文章图片
各种颜色的球球
loading小球需文字和颜色都可以改变,所以我们要给自己的控件添加这两个属性。首先在“res/values/”路径下新建一个attrs.xml文件,在里边定义如下属性:
- < declare-styleable name="Wave">
- < attr name="color" format="color"/>
- < attr name="text" format="string"/>
- < /declare-styleable>
复写三个构造函数,将单参数和双参数的构造函数的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了
- @Override
- protected void onDraw(Canvas canvas) {
- //底部的字
- textPaint.setColor(color);
- drawCenterText(canvas, textPaint, text);
- //上层的字
- textPaint.setColor(Color.WHITE);
- //生成闭合波浪路径
- path = getActionPath(currentPercent);
- canvas.clipPath(path);
- //裁剪成圆形
- canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, mPaint);
- drawCenterText(canvas, textPaint, text);
- }
细心的朋友一定看到了一个函数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)那个蓝色的点并不是在文字的左上角,而是左下角,这个点所在的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.top和FontMetrics.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();
- //文字框最高点距离baseline的距离(负数)
- float top = fontMetrics.top;
- //文字框最低点距离baseline的距离(正数)
- float bottom = fontMetrics.bottom;
- int centerY = (int) (rect.centerY() - top / 2 - bottom / 2);
- canvas.drawText(text, rect.centerX(), centerY, textPaint);
- }
本文中用到一个巧妙的定时器ValueAnimator 大家常说的属性动画ObjectAnimator就是它的一个子类,使用它来作为动画的引擎再方便不过了,从字面翻译"ValueAnimator"那就是“值动画者”直译虽然low但是恰恰更好理解,就是让数值动起来,从什么值动到什么值呢?
- ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- float percent = animation.getAnimatedFraction();
- }
- });
- animator.setDuration(1000);
- animator.setRepeatCount(ValueAnimator.INFINITE);
- animator.setRepeatMode(ValueAnimator.RESTART);
animator.start();
上效果
【在Android上仿百度贴吧客户端Loading图标小球】
文章图片
鬼畜版
WTF!这是什么鬼,为什么鬼畜地慢几拍?
打印出来横坐标看看
- 07-09 18:18:47.308E/Jcs: getActionPath: -21
- 07-09 18:18:47.326E/Jcs: getActionPath: -15
- 07-09 18:18:47.342E/Jcs: getActionPath: -10
- 07-09 18:18:47.359E/Jcs: getActionPath: -5
- 07-09 18:18:47.375E/Jcs: getActionPath: -2
- 07-09 18:18:47.392E/Jcs: getActionPath: 0
- 07-09 18:18:47.409E/Jcs: getActionPath: 0
- animator.setInterpolator(new LinearInterpolator());
文章图片
百度Loading小球Github源码
三、结语
第一次写文章,不免有些疏漏之处,望多多指教!后续我会不定期更新新的内容,争取把写文章当成自己生活的一部分。
后记(2017年7月27日15:02:39)
有不少读者问到关于小球和边缘锯齿的问题,我分别用如下方式实现loading小球
1、Canvas的clip方式限制波浪边界(本文提到的方法)
2、使用Xfermode方式限制波浪和圆形的边界
3、用Xfermode方式限制白色文字,用shader方式限制圆形的边界
下边是效果预览图,代码已经提交到github上了,讲解部分尽快补到此文中
文章图片
三种方式对比
推荐阅读
- 检测空值,以及会不会出现mapping类型不一致的问题
- Android线性渐变
- appium===出错时截图的方法,自动截图
- cordova 跨平台APP版本升级
- 解决Azure Stack App Service部署报错一例
- android如何判断控件的显示或者隐藏
- Java PushbackInputStream类
- Java PrintWriter类
- Java PrintStream类