Android自定义View实现钟摆效果进度条PendulumView
在网上看到了一个ios组件pendulumview,实现了钟摆的动画效果。由于原生的进度条确实是不好看,所以想可以自定义view实现这样的效果,以后也可以用于加载页面的进度条。
废话不多说,先上效果图
底部黑边是录制时不小心录上的,可以忽略。
既然是自定义view我们就按标准的流程来,第一步,自定义属性
自定义属性
建立属性文件
在android项目的res->values目录下新建一个attrs.xml文件,文件内容如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="pendulumview"> <attr name="globenum" format="integer"/> <attr name="globecolor" format="color"/> <attr name="globeradius" format="dimension"/> <attr name="swingradius" format="dimension"/> </declare-styleable> </resources>
其中declare-styleable的name属性用于在代码中引用该属性文件。name属性,一般情况下写的都是我们自定义view的类名,较为直观。
使用styleale,系统可以为我们完成很多常量(int[]数组,下标常量)等的编写,简化我们的开发工作,例如下面代码中用到的r.styleable.pendulumview_golbenum等就是系统为我们自动生成的。
globenum属性表示小球数量,globecolor表示小球颜色,globeradius表示小球半径,swingradius表示摆动半径
读取属性值
在自定view的构造方法中通过typedarray读取属性值
通过attributeset同样可以获取属性值,但是如果属性值是引用类型,则得到的只是id,仍需继续通过解析id获取真正的属性值,而typedarray直接帮助我们完成了上述工作。
public pendulumview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); //使用typedarray读取自定义的属性值 typedarray ta = context.getresources().obtainattributes(attrs, r.styleable.pendulumview); int count = ta.getindexcount(); for (int i = 0; i < count; i++) { int attr = ta.getindex(i); switch (attr) { case r.styleable.pendulumview_globenum: mglobenum = ta.getint(attr, 5); break; case r.styleable.pendulumview_globeradius: mgloberadius = ta.getdimensionpixelsize(attr, (int) typedvalue.applydimension(typedvalue.complex_unit_px, 16, getresources().getdisplaymetrics())); break; case r.styleable.pendulumview_globecolor: mglobecolor = ta.getcolor(attr, color.blue); break; case r.styleable.pendulumview_swingradius: mswingradius = ta.getdimensionpixelsize(attr, (int) typedvalue.applydimension(typedvalue.complex_unit_px, 16, getresources().getdisplaymetrics())); break; } } ta.recycle(); //避免下次读取时出现问题 mpaint = new paint(); mpaint.setcolor(mglobecolor); }
重写onmeasure()方法
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); int widthmode = measurespec.getmode(widthmeasurespec); int widthsize = measurespec.getsize(widthmeasurespec); int heightmode = measurespec.getmode(heightmeasurespec); int heightsize = measurespec.getsize(heightmeasurespec); //高度为小球半径+摆动半径 int height = mgloberadius + mswingradius; //宽度为2*摆动半径+(小球数量-1)*小球直径 int width = mswingradius + mgloberadius * 2 * (mglobenum - 1) + mswingradius; //如果测量模式为exactly,则直接使用推荐值,如不为exactly(一般处理wrap_content情况),使用自己计算的宽高 setmeasureddimension((widthmode == measurespec.exactly) ? widthsize : width, (heightmode == measurespec.exactly) ? heightsize : height); }
其中
int height = mgloberadius + mswingradius;
<pre name="code" class="java">int width = mswingradius + mgloberadius * 2 * (mglobenum - 1) + mswingradius;
用于处理测量模式为at_most的情况,一般是自定义view的宽高设置为了wrap_content,此时通过小球的数量,半径,摆动的半径等计算view的宽高,如下图:
以小球个数5为例,view的大小为下图红色矩形区域
重写ondraw()方法
@override protected void ondraw(canvas canvas) { super.ondraw(canvas); //绘制除左右两个小球外的其他小球 for (int i = 0; i < mglobenum - 2; i++) { canvas.drawcircle(mswingradius + (i + 1) * 2 * mgloberadius, mswingradius, mgloberadius, mpaint); } if (mleftpoint == null || mrightpoint == null) { //初始化最左右两小球坐标 mleftpoint = new point(mswingradius, mswingradius); mrightpoint = new point(mswingradius + mgloberadius * 2 * (mglobenum - 1), mswingradius); //开启摆动动画 startpendulumanimation(); } //绘制左右两小球 canvas.drawcircle(mleftpoint.x, mleftpoint.y, mgloberadius, mpaint); canvas.drawcircle(mrightpoint.x, mrightpoint.y, mgloberadius, mpaint); }
ondraw()方法是自定义view的关键所在,在该方法体内绘制view的显示效果。代码首先绘制了除去最左边最右边小球以外的其他小球,然后对左右两小球的坐标值进行判断,如果是第一次绘制,坐标值均为空,则初始化两小球坐标,并且开启动画。最后通过mleftpoint,mrightpoint的x,y值,绘制左右两个小球。
其中mleftpoint,mrightpoint均是android.graphics.point对象,仅是使用它们来存放左右两小球的x,y坐标信息。
使用属性动画
public void startpendulumanimation() { //使用属性动画 final valueanimator anim = valueanimator.ofobject(new typeevaluator() { @override public object evaluate(float fraction, object startvalue, object endvalue) { //参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值 double angle = math.toradians(90 * fraction); int x = (int) ((mswingradius - mgloberadius) * math.sin(angle)); int y = (int) ((mswingradius - mgloberadius) * math.cos(angle)); point point = new point(x, y); return point; } }, new point(), new point()); anim.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator animation) { point point = (point) animation.getanimatedvalue(); //获得当前的fraction值 float fraction = anim.getanimatedfraction(); //判断是否是fraction先减小后增大,即是否处于即将向上摆动状态 //在每次即将向上摆动时切换小球 if (lastslope && fraction > mlastfraction) { isnext = !isnext; } //通过不断改动左右小球的x,y坐标值实现动画效果 //利用isnext来判断应该是左边小球动,还是右边小球动 if (isnext) { //当左边小球摆动时,右边小球置于初始位置 mrightpoint.x = mswingradius + mgloberadius * 2 * (mglobenum - 1); mrightpoint.y = mswingradius; mleftpoint.x = mswingradius - point.x; mleftpoint.y = mgloberadius + point.y; } else { //当右边小球摆动时,左边小球置于初始位置 mleftpoint.x = mswingradius; mrightpoint.y = mswingradius; mrightpoint.x = mswingradius + (mglobenum - 1) * mgloberadius * 2 + point.x; mrightpoint.y = mgloberadius + point.y; } invalidate(); lastslope = fraction < mlastfraction; mlastfraction = fraction; } }); //设置永久循环播放 anim.setrepeatcount(valueanimator.infinite); //设置循环模式为倒序播放 anim.setrepeatmode(valueanimator.reverse); anim.setduration(200); //设置补间器,控制动画的变化速率 anim.setinterpolator(new decelerateinterpolator()); anim.start(); }
其中使用valueanimator.ofobject方法是为了可以对point对象进行操作,更为形象具体。还有就是通过ofobject方法使用了自定义的typeevaluator对象,由此得到了fraction值,该值是一个从0-1变化的小数。所以该方法的后两个参数startvalue(new point()),endvalue(new point())并没有实际意义,也可以直接不写,此处写上主要是为了便于理解。同样道理也可以直接使用valueanimator.offloat(0f, 1f)方法获取到一个从0-1变化的小数。
final valueanimator anim = valueanimator.ofobject(new typeevaluator() { @override public object evaluate(float fraction, object startvalue, object endvalue) { //参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值 double angle = math.toradians(90 * fraction); int x = (int) ((mswingradius - mgloberadius) * math.sin(angle)); int y = (int) ((mswingradius - mgloberadius) * math.cos(angle)); point point = new point(x, y); return point; } }, new point(), new point());
通过fraction,我们计算得到小球摆动时的角度变化值,0-90度
mswingradius-mgloberadius表示的值是图中绿色直线的长度,摆动的路线,小球圆心的路线是一个以(mswingradius-mgloberadius)为半径的弧线,变化的x值为(mswingradius-mgloberadius)*sin(angle),变化的y值为(mswingradius-mgloberadius)*cos(angle)
对应的小球实际的圆心坐标为(mswingradius-x,mgloberadius+y)
右边小球运动路线与左边类似,仅仅是方向不同。右边小球实际的圆心坐标(mswingradius + (mglobenum - 1) * mgloberadius * 2 + x,mgloberadius+y)
可见左右两边小球的纵坐标是相同的,仅横坐标不同。
float fraction = anim.getanimatedfraction(); //判断是否是fraction先减小后增大,即是否处于即将向上摆动状态 //在每次即将向上摆动时切换小球 if (lastslope && fraction > mlastfraction) { isnext = !isnext; } //记录上一次fraction是否不断减小 lastslope = fraction < mlastfraction; //记录上一次的fraction mlastfraction = fraction;
这两段代码用于计算何时切换运动的小球,本动画设置了循环播放,且循环模式为倒序播放,所以动画的一个周期即为小球抛起加上小球落下的过程。在该过程中fraction的值先有0变为1,再由1变为0。那么何时是动画新一轮周期的开始呢?就是在小球即将抛起的时候,在这个时候切换运动的小球,即可实现左边小球落下后右边小球抛起,右边小球落下后左边小球抛起的动画效果。
那么如何捕捉到这个时间点呢?
小球抛起时fraction值不断增大,小球落下时fraction值不断减小。小球即将抛起的时刻,就是fraction从不断减小转变为不断增大的时刻。代码中记录上一次fraction是否在不断减小,然后比较这一次fraction是否在不断增大,若两个条件均成立则切换运动的小球。
anim.setduration(200); //设置补间器,控制动画的变化速率 anim.setinterpolator(new decelerateinterpolator()); anim.start();
设置动画的持续时间为200毫秒,读者可以通过更改该值而达到修改小球摆动速度的目的。
设置动画的补间器,由于小球抛起是一个逐渐减速的过程,落下是一个逐渐加速的过程,所以使用decelerateinterpolator实现减速效果,在倒序播放时为加速效果。
启动动画,钟摆效果的自定义view进度条就实现了!赶快运行,看看效果吧!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 谈谈java的concurrent用法