有段时间对Android中自定义View非常痴迷,看到一些炫的总会手痒的自己尝试着实现一下。这个系列的就是整理一些之前实现的,跟大家一起看看Android中的魔法。
本篇文章讲解如何实现一个水晶球波浪进度条,实现后效果如下:
我们来观察其中一帧的画面,如下
(图1)
可以看到在一瞬间的波浪其实是两条不同的正弦函数曲线叠加在一起,而波浪的运动实际上这两条正弦函数在移动。由于两条曲线的振幅、周期和移动速率完全不同,所以产生了波浪的效果。
所以实现波浪的效果我们需要用到一个正弦函数:
a*sin(b*(x + c))+d
其中:
- a - 振幅,影响的是波浪的浪高
- b - 周期,影响的是两个浪头之间的距离
- c - 偏移,改变这个参数来实现曲线的移动
- d - 高度,即水平线的高度,曲线在这个高度上下波动(实际上是进度,后面会讲到)
实现这个函数:
/** * 波浪的函数,用于求y值 * 函数为a*sin(b*(x + c))+d * @param x x轴 * @param offset 偏移 * @param waveHeight 振幅 * @param waveCycle 周期 * @return */ private double getWaveY(int x, int offset, int waveHeight, float waveCycle) { return waveHeight * Math.sin(waveCycle * (x + offset)) + (1 - mProgress / 100.0) * getHeight(); }
下面就是最关键的部分,我们如何使用这个函数实现想要的效果?
在a、b、c、d确定的情况下,通过上面的函数我们只能得到一条线,如图
(图2)
但我们实际上想要一个填充的效果,解决办法是我们利用这个曲线上的点与基线(x轴)上对应的点连线,如下图
(图3)
当这些线足够多足够密集的时候,就实现了填充效果。如图
(图4)
有了这个效果还不够,因为我们需要的是一个圆形,这时候就需要使用遮罩来处理。为上面的图形加上一个圆形的遮罩,遮罩设置为DST_IN,就可以得到想要的效果,如图
(图5)
这样当我们有两条不同的曲线,经过(图5)处理后区域叠加在一起的时候,就形成了(图1)的波浪效果。
下面我们来看看具体代码怎么处理
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (getHeight() > 0 && getWidth() > 0) { //绘制边缘 Paint paint = new Paint(); paint.setColor(mWaveColor); paint.setStyle(Paint.Style.STROKE); RectF edge = new RectF(0, 0, getWidth(), getHeight()); canvas.drawArc(edge, 0, 360, false, paint); canvas.drawColor(Color.TRANSPARENT); int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG); if (isWaveMoving) { /** * 如果有波浪,则绘制两条波浪 * 波浪实际上是一条条一像素的直线组成的,线的顶端是根据正弦函数得到的 */ for (int i = 0; i < getWidth(); i++) { canvas.drawLine(i, (int) getWaveY(i, mOffsetA, mWaveHeightA, mWaveACycle), i, getHeight(), mWavePaint); canvas.drawLine(i, (int) getWaveY(i, mOffsetB, mWaveHeightB, mWaveBCycle), i, getHeight(), mWavePaint); } } else { /** * 如果没有波浪,则绘制两次矩形 * 之所以绘制两次,是因为波浪有两条,所以除了浪尖的部分,其他部分都是重合的,颜色较重 */ float height = (1 - mProgress / 100.0f) * getHeight(); canvas.drawRect(0, height, getWidth(), getHeight(), mWavePaint); canvas.drawRect(0, height, getWidth(), getHeight(), mWavePaint); } //设置遮罩效果,绘制遮罩 mWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); canvas.drawBitmap(mBallBitmap, 0, 0, mWavePaint); mWavePaint.setXfermode(null); canvas.restoreToCount(sc); } }
我们在onDraw中来处理这部分的绘制。
绘制分三个部分。
(1)第一部分绘制一个圆环,这是球的边缘。
(2)第二部分绘制(图4)区域。在这一部分中通过判断isWaveMoving做两种不同的处理。
当ture时表示现在波浪在运动,通过getWaveY生成两条参数完全不同的曲线上的点,以这些点为基础绘制直线达到填充效果。
当false时表示不在运动,这时没有波浪,即水平线是平的,直接绘制两个矩形即可。
(3)第三部分绘制遮罩,产生(图5)的效果。
遮罩是一个圆形的bitmap,遮罩模式我们使用DST_IN。
遮罩bitmap由于基本不会改变,所以不需要每次onDraw的时候去创建,它的创建放在onSizeChanged中,代码如下:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (w > 0 && h > 0) { /** * 根据宽高初始化波浪的一些参数 * 波浪的速度根据宽度的一定比例,这样不同宽度波浪移动的效果保持差不多 * 波浪的振幅根据高度和默认值,当高度太小就设为高度的一定比例,这样保证不同高度下波浪效果明显 * 波浪的周期固定即可 */ mWaveSpeedA = w / 10; mWaveSpeedB = w / 17; mWaveHeightA = DisplayUtils.dip2px(getContext(), 10); mWaveHeightB = DisplayUtils.dip2px(getContext(), 5); if (h / 10 < mWaveHeightA) { mWaveHeightA = h / 10; mWaveHeightB = h / 20; } initStopAnimator(mWaveHeightA, mWaveHeightB); mWaveACycle = (float) (3 * Math.PI / w); mWaveBCycle = (float) (4 * Math.PI / w); /** * 初始化圆形遮罩 * 圆形遮罩是一个与组件同大小的椭圆,并且四周为透明 * 注意: * 不在onDraw中直接绘制这个遮罩,因为那样绘制后遮罩只是一个椭圆,使用DST_IN的话在椭圆外的部分 * 就不会做任何处理,达不到效果;而先做成bitmap的话遮罩是一个方形,椭圆外部分就会去掉,达到效果 * */ mBallBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mBallBitmap); RectF ball = new RectF(0, 0, w, h); canvas.drawOval(ball, mWavePaint); } }
这样可以在Size改变的时候,重新创建一个符合的遮罩。
可以看到在onSizeChanged中,我们同样做了一些参数的初始化工作,根据组件的宽和高,为A和B两条曲线初始化不同的且合适的振幅、周期、移动速度。
到此我们绘制出了(图1)的效果,但是这只是一个静态图案,我们如果让波浪动起来呢?
波浪的运动包含两个方向的运动:上下运动和左右运动。
上下运动与参数d有关,在getWaveY函数中可以看到参数d是由mProgress这个参数决定的,所以改变这个参数就可以实现波浪的涨落。
左右运动本质上是曲线的偏移,由参数c控制,在onDraw代码中可以看到分别是mOffsetA和mOffsetB。
使用属性动画来动态改变这几个参数就可以实现波浪的运动效果,具体代码如下
/** * 设置进度,并且以动画的形式上涨到该进度 * @param progress 进度 * @param duration 持续时间 * @param delay 延时 */ public void startProgress(int progress, long duration, long delay){ if(mProgressAnimator != null && mProgressAnimator.isRunning()){ mProgressAnimator.cancel(); } if(mWaveStopAnimator != null && mWaveStopAnimator.isRunning()){ mWaveStopAnimator.cancel(); } isWaveMoving = true; mProgressAnimator = ObjectAnimator.ofInt(this, "Progress", progress); mProgressAnimator.setDuration(duration); mProgressAnimator.setStartDelay(delay); mProgressAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { mWaveStopAnimator.start(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { //改变曲线的偏移,达到波浪运动的效果 mOffsetA += mWaveSpeedA; mOffsetB += mWaveSpeedB; invalidate(); } }); mProgressAnimator.start(); }
先重点关注mProgressAnimator这个动画。
ObjectAnimator.ofInt(this, "Progress", progress)
第二个参数表示改变的属性,会调用setProgress和getProgress方法改变对应的属性,最终改变的是mProgress属性
第三个参数是最终值,表示这个动画会将该属性的从当前值逐渐改变成设定值(即最终值)
这样波浪的涨落就可以通过mProgressAnimator产生了。
然后可以看到为mProgressAnimator添加了AnimatorUpdateListener,所以在改变mProgress的同时,也在动态的改变mOffsetA和mOffsetB并重绘,这样同时波浪的左右也实现了。
注意在onSizeChanged函数中初始化mWaveSpeedA和mWaveSpeedB的值是不同的,这样两条曲线每次改变的位移会不同,就会产生两个波浪反复交错的效果。如果mWaveSpeedA和mWaveSpeedB一样,那么两条曲线就会相对静止,波浪效果很差。
到此,我们通过startProgress函数来改变WaveBallProgress的进度值,就会产生波浪涨落的效果。
离我们的最终效果只差一步了,因为当波浪涨到新的进度时,我们希望水面可以慢慢平静下来。
如果我们在mProgressAnimator动画结束时立刻让水面恢复平静,会显得很突兀。我们需要让波浪逐渐变小直至恢复平静,所以在mProgressAnimator动画结束(onAnimationEnd)时我们启动了另外一个动画mWaveStopAnimator。
这个动画的初始化是在onSizeChanged函数中进行的,初始化函数代码如下
private void initStopAnimator(final int waveHeightA, final int waveHeightB){ /** * 创建波浪停止动画 * 两条波浪振幅逐渐减小 */ PropertyValuesHolder holderA = PropertyValuesHolder.ofInt("WaveHeightA", 0); PropertyValuesHolder holderB = PropertyValuesHolder.ofInt("WaveHeightB", 0); mWaveStopAnimator = ObjectAnimator.ofPropertyValuesHolder(this, holderA, holderB); mWaveStopAnimator.setDuration(1000); mWaveStopAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { isWaveMoving = false; mWaveHeightA = waveHeightA; mWaveHeightB = waveHeightB; } @Override public void onAnimationCancel(Animator animation) { mWaveHeightA = waveHeightA; mWaveHeightB = waveHeightB; } @Override public void onAnimationRepeat(Animator animation) { } }); mWaveStopAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { //改变曲线的偏移,达到波浪运动的效果 mOffsetA += mWaveSpeedA; mOffsetB += mWaveSpeedB; invalidate(); } }); }
在这里使用了PropertyValueHolder,让一个动画同时实现多个效果。我们同时减小两条曲线的振幅直到为0,这样波浪就会逐渐变小直到变成一条直线。
同第一个动画一样,在动画过程中继续改变offset保证波浪运动。
在动画结束时或cancel时重置mWaveHeightA和mWaveHeightB,保证下一次startProgress使用正确的振幅。
再回头看startProgress函数一开始,判断两个动画是否在进行中,如果是cancle掉。保证在频繁改变进度的时候不会出现几个动画一起运行的情况。
到此所有功能都完成了,我们实现了一个水晶球波浪进度条。
总结一下,在本篇文章里我们主要使用了遮罩和属性动画这两个功能:
(1)遮罩对于实现一些特殊形状的绘制很有帮助,而且遮罩有很多种模式,不同的模式解决不同的问题。
(2)属性动画是Android几种动画之一,除了ObjectAnimator还有其他类,而且支持多种方式,比如同时执行、依次执行、反复执行等等。属性动画应用场景很多,可以解决很多问题,是Android开发者进阶必会的一个知识点。
大家有兴趣可以自己手动实现一下,对这两个功能有更深入的了解。
下一篇会继续讲解一个自定义效果,敬请期待!
作者:chzphoenix 发表于2017/9/1 10:26:17 原文链接
阅读:0 评论:0 查看评论