本篇文章已授权微信公众号 guolin_blog(郭霖)独家发布

效果预览

因GIF图压缩的原因动画看起来有些不流畅。

应用为加载框的效果:

使用方法

XML:

   <com.example.ccy.bounceballview.BounceBallViewandroid:id="@+id/bbv1"android:layout_width="match_parent"android:layout_height="140dp"android:background="#ffffff"app:anim_duration="3300"app:ball_count="15"app:ball_delay="220"app:bounce_count="3"/>

可用的属性:

属性名称 作用
bounce_count 小球弹跳次数
ball_color 小球颜色
ball_count 小球数量
ball_radius 小球半径
ball_delay 小球出现时间间隔(当小球数大于1时)
anim_duration 小球一次动画时长
physic_mode 开启物理效果(下落加速上升减速)
random_color 开启小球颜色随机
random_radius 开启小球大小随机(在基础大小上下浮动)
random_path 开启小球路径随机(在基础路径坐标上下浮动)

也可以在代码中进行配置:

bbv1 = (BounceBallView) findViewById(R.id.bbv1);
bbv.config().ballCount(15).bounceCount(3).ballDelay(220).duration(3300).radius(15).isPhysicMode(true).isRamdomPath(true).isRandomColor(true).isRandomRadius(true).apply();

最后开启动画:

 bbv1.start();

实现思路

源码地址:https://github.com/CCY0122/bounceballview

概况

该自定义控件主要是使用了Path和属性动画来完成的,比如小球的弹跳路径,是用Path和二次贝塞尔曲线来完成的,再比如这个小球“下落加速、上弹减速”的仿物理效果是利用了Path插值器(PathInterpolator)和三次贝塞尔曲线完成的,另外诸如颜色随机、小球大小随机这些都是通过监听属性动画来实现的。接下来会讲解下这些主要的实现思路
对于自定义View的其他基本流程,如属性的获取与设置、onMeasure的重写等本文不会多讲,想了解完整流程的话可以查看源码

小球路径的实现

通过效果图可以看到,只要确定了控件大小和小球弹跳次数(bounce_count),那么就能确定小球总体的弹跳路径。因此第一个核心点就是实现小球路径的Path。假设bounce_count值被设置为3,即弹跳3次,那么路径Path的效果图应该如下图所示:

要绘制出上述这样的路径,可以利用多个二次贝塞尔曲线拼接的方式实现。关于贝塞尔曲线,若想课后系统的学习,我很推荐徐医生的这篇文章:贝塞尔曲线开发的艺术
对于二次贝塞尔曲线,这里引用一张经典图:
一张图真是胜过千言万语啊~

在path中二次贝塞尔对应的方法是 path.quadTo (float x1, float y1, float x2, float y2),其中,x1/y1对应上图P1点,称做控制点,x2/y2对应P2点,就叫他终点把,P0点么就是当前path所在的起点。
由此可见,上图小球弹跳路径就是通过拼接4个二次贝塞尔曲线完成的,对于代码中对起点、终点、控制点的确定,为了方便理解,在看代码前,先附上一张图:

一张图真是胜过千言万语啊~

path的实现代码如下:

 /*** 初始化球体弹跳的路径*/private void initPath() {path.reset();float intervalX = (viewWidth - 2 * defaultPadding) / (bounceCount + 1); //每次弹跳的间距PointF start = new PointF();//起点位置PointF control = new PointF(); //贝塞尔控制点PointF end = new PointF(); //贝塞尔结束点start.x = defaultPadding;start.y = viewHeight - defaultPaddingBottom;float controlOffsetY = viewHeight * 0.6f;  //控制点向上偏移量,0.6为调试值float deltaY = (1.2f * viewHeight + controlOffsetY) / (bounceCount + 1); //控制点高度递减值,1.2为调试值PathMeasure tempPathMeasure = new PathMeasure();segmentLength = new float[bounceCount + 1];for (int i = 0; i <= bounceCount; i++) {control.x = start.x + intervalX * (i + 0.5f);control.y = -controlOffsetY + deltaY * i;end.x = start.x + intervalX * (i + 1);end.y = start.y;if (i == 0) {path.moveTo(start.x, start.y);}if (i == bounceCount) {end.y = viewHeight;}path.quadTo(control.x, control.y, end.x, end.y);tempPathMeasure.setPath(path, false);if (i == 0) { //第一次弹跳的上升阶段不画,记录弹跳一半长度(为效果更好,实际取值0.45)skipLength = tempPathMeasure.getLength() * 0.45f;}segmentLength[i] = tempPathMeasure.getLength();}pathMeasure.setPath(path, false);//以下是仿物理效果的动画插值器的创建。if (interCreater == null) {interCreater = new MultiDecelerateAccelerateInterpolator();}physicInterpolator = interCreater.createInterpolator(segmentLength);}

配合图片看代码,其中PathMeasure是一个用于测量path各种数据的帮助类,通过它我们可以获取一段path的长度、path上某一点的坐标等等,非常有用。关于它的详细使用,可参考PathMeasure之迷径追踪

在代码中我们还记录了一个变量 skipLength 即对于上图中的AB段,因为我们的小球是直接从最高点开始下落的,所以AB段我们是要省略掉的。另外关键的一点是还记录了segmentLength[]数组,它的作用是用来后面创建插值器时用到的。对于上图来讲,segmentLength中分别存储着AC、AD、AE、AF四段路径长度。
有了弹跳路径,在onDraw中我们就可以利用PathMeasure来取出路径上某一个点的坐标,作为小球的绘制坐标了,而具体要取哪个点呢?我们可以通过属性动画来控制一个数值在一定时间内从0.0开始变化到1.0结束,将这个值作为比例值乘上路径总长度,就能得到当前时间要取的坐标点了,代码如下:

@Overrideprotected void onDraw(Canvas canvas) {drawBounceBall(canvas);}private void drawBounceBall(Canvas canvas) {for (int i = 0; i < ballCount; i++) {canvas.save();if (translateFraction[i] < (skipLength / pathMeasure.getLength())) {continue;}//根据当前动画进度获取path上对应点的坐标和正切pathMeasure.getPosTan(pathMeasure.getLength() * translateFraction[i],pos,tan);//路径随机if (isRandomBallPath) {pos[0] *= randomTransRatioX[i];pos[1] *= randomTransRatioY[i];}//颜色随机已在makeRandom里被应用canvas.drawCircle(pos[0],pos[1],isRandomRadius ? randomRadius[i] : radius,paint[i]);canvas.restore();}}

在onDraw中,translateFraction[i] 记录着第i个小球当前的属性动画值。通过pathMeasure.getPosTan(pathMeasure.getLength()*translateFraction[i],pos,tan); 方法获取了当前比例值下路径上对应点的坐标值(存放在pos[2]数组里)和正切值(存放在tan[2]数组里),获取到了这个坐标点后,再经过一步路径随机的判断处理后,我们就可以通过canvas.drawCircle来画出小球了。
那么接下来我们的重点就是属性动画了。

小球动画的实现

通过上述分析可知,小球的动画其实是对一个比例值做动画,让这个值在一定时间内从0.0开始变化到1.0,并且无限循环。同时通过监听动画,在动画过程中可以对小球的透明度、颜色、路径、大小做相应的改变,当然还有最重要的就是在动画监听过程中不断的调用invalidate()来重绘小球的坐标位置。创建动画的代码如下:

 private void createAnim(int duration) {for (int i = 0; i < ballCount; i++) {createTranslateAnim(i, duration, i * ballDelay);}}private void createTranslateAnim(final int index, int duration, final int delay) {if (translateAnim[index] == null) {translateAnim[index] = ValueAnimator.ofFloat(0.0f, 1.0f);translateAnim[index].setDuration(duration);translateAnim[index].setRepeatCount(ValueAnimator.INFINITE);translateAnim[index].setStartDelay(delay);if (isPhysicsMode) {translateAnim[index].setInterpolator(physicInterpolator);} else {translateAnim[index].setInterpolator(defaultInterpolator);}translateAnim[index].addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationStart(Animator animation) {super.onAnimationStart(animation);makeRandom(index);}@Overridepublic void onAnimationRepeat(Animator animation) {super.onAnimationRepeat(animation);makeRandom(index);}});translateAnim[index].addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {//获取当前动画进度的比例值translateFraction[index] = animation.getAnimatedFraction();//以下是对小球透明度的处理if (dealFromAlphaAnim(translateFraction[index]) != -1) {paint[index].setAlpha(dealFromAlphaAnim(translateFraction[index]));} else if (dealToAlphaAnim(translateFraction[index]) != -1) {paint[index].setAlpha(dealToAlphaAnim(translateFraction[index]));} else {paint[index].setAlpha(255);}//实时重绘invalidate();}});}}

有多少个小球,就创建了多少个动画,并存储在translateAnim[]数组里。如果开启了仿物理效果,就会给动画设置一个插值器setInterpolator(physicInterpolator) ,后面会讲解这个插值器的实现。另外监听了动画开始和动画循环时的监听回调,在里面调用了makeRandom(index)方法,即对一些数据做随机处理,代码如下:

    /*** 数据随机化** @param index*/private void makeRandom(int index) {if (isRandomBallPath) {   //坐标是在ondraw里才获得的,故在ondraw里再去应用randomTransRatioX[index] = (float) (0.9f + (0.2f * Math.random())); //[0.9,1.1)randomTransRatioY[index] = (float) (0.8f + (0.4f * Math.random())); //[0.8,1.2)}if (isRandomColor) {  //不要在ondraw里再去应用,会同时覆盖掉画笔的透明度通道,透明动画会失效randomBallColors[index] = getRandomColor();paint[index].setColor(randomBallColors[index]);} else {paint[index].setColor(ballColor);}if (isRandomRadius) {randomRadius[index] = (float) (radius * (0.7 + (0.6 * Math.random()))); //[0.7,1.3]} else {randomRadius[index] = radius;}}

之后是监听了动画过程中的实时回调,在该回调中获取的当前动画进度比例animation.getAnimatedFraction()并赋值给了translateFraction[index] ,然后还对小球做了透明度处理(效果图中可见:小球出现时由透明渐变到不透明,快结束时再渐变到透明)。最后就是调用了invalidate()去重绘小球。到这里,我们的控件已经成型了

仿物理效果插值器的实现

我们的目标是要根据小球的弹跳次数来实现一个多次“减速-加速”的插值器,怎么实现呢?难道要继承基础插值器BaseInterpolator然后自己设计算法来写一个插值器吗?也太难了吧。
我们这里要介绍一个神奇的插值器,叫PathInterpolator。它可以将一个path路径映射成对应的插值器。
举个例子,系统内置的加速插值器和减速插值器对应的图形如下:

那么我们想要一个先减速后加速的插值器就应该大致长这样:

然后将多个这样的路径拼接,就能实现多次先减速后加速的插值器了
但是这个路径怎么实现呢?从上图其实就可以看出来,当然是使用三次贝塞尔曲线啦,看到图中有红蓝两个点没有,他们就是两个控制点。若有不理解了,打开这个网站玩一玩就明白了:贝塞尔曲线可视化
path中三次贝塞尔曲线对应的方法是path.cubicTo (float x1, float y1, float x2, float y2, float x3, float y3) 其中x1/y1为第一个控制点坐标, x2/y2为第二个控制点坐标, x3/y3为终点坐标。

最后还有一个问题就是要确定每一次“减速-加速”路径的起点和终点,这主要是通过之前在initPath里记录好的segmentLength来确定的。
完整代码如下:

public class MultiDecelerateAccelerateInterpolator {private PointF originStart; //起点,用于构造PathInterpolator时须为[0,0]private PointF originEnd; //终点,用于构造PathInterpolator时须为[1,1]private float intervalX;private float intervalY;private float bezierControlRatioX;private float bezierControlRatioY;/*** ratiox = 0.2, ratioy = 0.55 为调试值* 单次路径效果图: http://cubic-bezier.com/#.2,.55,.8,.45* 可自行调整这两个值,配合动画的整体时长,调出比较接近自由落体的效果*/public MultiDecelerateAccelerateInterpolator() {this(new PointF(0,0),new PointF(1,1),0.2f ,0.55f );}/*** 用于构造PathInterpolator时,起点必须为[0,0]终点必须为[1,1]* 用于构造“先减速后加速”效果时,建议ratiox取值[0,0.5];ratioy取值范围[0,1]且ratiox < ratioy,* @param start 起点* @param end 终点* @param ratiox x比例值,用于控制贝塞尔控制点位置* @param ratioy y比例值,用于控制贝塞尔控制点位置*/public MultiDecelerateAccelerateInterpolator(PointF start,PointF end,float ratiox,float ratioy){originStart = start;originEnd = end;intervalX = Math.abs(originEnd.x - originStart.x);intervalY = Math.abs(originEnd.y - originStart.y);bezierControlRatioX = ratiox;bezierControlRatioY = ratioy;}/*** 利用三次贝塞尔构造减速加速函数* @param segmentLength 从起点到每一段终点的长度集合* @return*/public Path createPath(float[] segmentLength){Path path = new Path();float ratio;PointF start = new PointF();PointF con1 = new PointF();PointF con2 = new PointF();PointF end = new PointF();float totalLength = segmentLength[segmentLength.length - 1];for (int i = 0; i < segmentLength.length; i++) {ratio = segmentLength[i] / totalLength;if(i == 0){start.x = originStart.x;start.y = originStart.y;path.moveTo(originStart.x,originStart.y);}end.x = intervalX * ratio;end.y = intervalY * ratio;con1.x = start.x + (end.x - start.x) * bezierControlRatioX;con1.y =  start.y + (end.y - start.y) * bezierControlRatioY;con2.x = end.x - (end.x - start.x) * (bezierControlRatioX );con2.y = end.y - (end.y - start.y) * (bezierControlRatioY );path.cubicTo(con1.x,con1.y,con2.x,con2.y,end.x,end.y);start.x = end.x;start.y = end.y;}return path;}/*** 构造PathInterpolator* @param segmentLength* @return*/public Interpolator createInterpolator(float[] segmentLength){Path p = createPath(segmentLength);Interpolator inter =PathInterpolatorCompat.create(p);return inter;}

好了,到这里本控件主要的实现点都讲完了。想了解完整流程的可阅阅读源码:https://github.com/CCY0122/bounceballview
谢谢阅读。欢迎star

自定义View之小球自由落体弹跳加载控件相关推荐

  1. Asp.net动态加载控件的一些问题

    ASP.net 动态加载控件时一些问题的总结 1 .在使用LoadControl加载控件后,用户控件中的某些控件不再响应事件. 这个问题主要是由于将控件加载放在if (!Page.IsPostBack ...

  2. WinForm的延时加载控件概述

    这篇文章主要介绍了WinForm的延时加载控件,很实用的技巧,在C#程序设计中有着比较广泛的应用,需要的朋友可以参考下 本文主要针对WinForm的延迟加载在常用控件的实现做简单的描述.在进行C#项目 ...

  3. 2020.4.23工作记录————Regsvr32加载控件失败:“请确保该二进制存储在指定路径中。。。”

    工作记录----Regsvr32加载控件失败: 之前的OCX控件今天我因为工作需要又重新写了个新的,跟之前不同的是这次的控件链接了第三方的动态库,但是在OCX控件生成完成后我使用管理员身份运行cmd对 ...

  4. 64位 regsrv win10_Win10手动注册注册表提示regsvr32加载控件失败解决方案

    regsvr32加载控件失败怎么办?当我们在使用计算机时,有时候会发现有些注册表没有注册,需要手动进行注册,但是在cmd命令中,输入命令regsvr32 xx.dll,运行提示模块数据加载失败,这怎么 ...

  5. 省市县乡村 动态级联加载控件AreaRender(二)

    今天很激动啊,昨天下午赶时间写的省市县乡村 动态级联加载控件AreaRender(一)已经有博友评论了, 对于刚开始写博客的我是莫大的鼓励,我会继续写下去,逐步提高自己的文学素养,写出像 T2噬菌体博 ...

  6. Android所有View通用下拉刷新上拉加载控件

    转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38868463 前面写过一篇关于下拉刷新控件的博客下拉刷新控件终结者:Pull ...

  7. Android下拉刷新上拉加载控件,对所有View通用!

    前面写过一篇关于下拉刷新控件的博客下拉刷新控件终结者:PullToRefreshLayout,后来看到好多人还有上拉加载更多的需求,于是就在前面下拉刷新控件的基础上进行了改进,加了上拉加载的功能.不仅 ...

  8. 自定义view实战(6):滑动折叠Header的控件(滑动冲突)

    前言 上一篇文章直接通过安卓自定义view的知识手撕了一个侧滑栏,做的还不错,很有成就感.这篇文章的控件没有上一篇的复杂,比较简单,通过一个内容滚动造成header折叠的控件学习一下滑动事件冲突问题. ...

  9. Android自定义View,高仿QQ音乐歌词滚动控件!

    最近在以QQ音乐为样板做一个手机音乐播放器,源码下篇博文放出.今天我想聊的是这个QQ音乐播放器中歌词显示控件的问题,和小伙伴们一起来探讨怎么实现这个歌词滚动的效果.OK,废话不多说,先来看看效果图: ...

最新文章

  1. sql server 2008建域时提示admin密码不符合要求解决方法
  2. Discuz!NT负载均衡方案
  3. 百思不得其解,一个钻石玩家可以短时间上王者?因为猎游?
  4. Java反射机制:表单数据自动封装到JavaBean中【IT】
  5. 5分钟搞定Loki告警多渠道接入
  6. HashMap Hashtable区别
  7. UITableView自动计算图片的高度 SDWebImage
  8. Android 生成分享长图并且添加全图水印
  9. 分频测试软件,带有强大的分频控制软件_天逸 TY-D01_音频评测-中关村在线
  10. 将mdf文件转化为excel
  11. javafx 二维图形编程及可视化操作
  12. 文件被system进程锁定,不能删除
  13. cpu多开测试软件,游戏多开CPU优化工具
  14. 身为码农,为12306说两句公道话:
  15. [论文阅读笔记38]基于多标准主动学习EMR实体识别方法
  16. 大数据的python基础_大数据量化之路之python基础
  17. ImageJ自动批量多通道图片无损分离为单色荧光图
  18. Google Play 商品详情,考试内容
  19. 数据库查询结果去重常用方法整理
  20. ERTEC200P-2 PROFINET设备完全开发手册(8-1)

热门文章

  1. t4b代码生成_T4+VSIX 打造Visual Studio 2010 中的Entity代码生成自定义工具
  2. autohotkey 老板键,自定义程序老板键,自定义按键
  3. 如何用python控制OBS 狀態
  4. Deepin上编译wxWidgets
  5. 以太坊上的虚拟世界 Cryptovoxels - 30倍地皮背后的投资逻辑
  6. UDP 的报文结构及注意事项
  7. 蓝牙aoa精准定位产品行业头部公司进化历程深圳核芯物联前进发展方向思考
  8. 金仓KingbaseES中数据库逻辑备份总结
  9. 黑莓允许开发者将通讯软件BBM植入iOS和Android应用
  10. php设置rabbitmq重试,RabbitMQ实现重试次数方法一-SpringRetry