前言

我们都知道,平时表现进度的方式有千千万万种(没有UI想不到的,只有你做不到的= =.),其中有一种就是水波纹进度球的形式,网上很多种实现都是直接采用纯色填充的方式,即水波纹都是纯颜色填充,效果看起来都挺不错,例如下面的效果:

纯色水波纹球

突发奇想,如果不止满足于纯色的水波纹呢?能不能通过设置一个图标,来做出同样的效果呢?原理差不多,只是多了些细节处理,先上效果图:

图标水波纹进度球.gif

实现

思路

将水波纹的内容替换成了Logo,原理上主要也是使用图像合成混合模式以及贝塞尔曲线,结合属性动画而成。从效果图可以看出,绘制的部分主要有三部分:水波纹、图标、球体。主要步骤和实现方式如下:

1.通过贝塞尔曲线绘制出水波纹路径

2.绘制一个圆形Bitmap,用于后面裁剪

3.绘制出目标Icon图

4.通过PorterDuffXfermode,将水波纹路径+圆形Bitmap围起来的图形作为一个遮罩,与Icon图混合显示。

1.通过贝塞尔曲线绘制出水波纹路径

关于贝塞尔实现水波纹的效果,详见我另一篇文章 Android 路径绘制艺术——贝塞尔曲线,这里再简单重述一下:

private Path mWavePath1 = new Path();

//每一小节波浪的宽度

private int mItemWidth = 120;

//波浪与View最左边缘之间偏移的距离

private int mOffsetX1;

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

mWavePath1.reset();

int halfItem = mItemWidth / 2;

//为了闭合整个View, 否则波浪遮罩顶部显示不正常

mWavePath1.moveTo(0, 0);

//必须先减去一个浪的宽度,以便第一遍动画能够刚好位移出一个波浪,形成无限波浪的效果

mWavePath1.lineTo(-mItemWidth + mOffsetX1, mWaterTop);

for (int i = mLeft - mItemWidth; i < mLeft + mItemWidth + mWidth; i += mItemWidth) {

mWavePath1.rQuadTo(halfItem / 2, -mWaveHeight, halfItem, 0);

mWavePath1.rQuadTo(halfItem / 2, mWaveHeight, halfItem, 0);

}

//闭合路径波浪以下区域

mWavePath1.lineTo(mWidth, mHeight);

mWavePath1.lineTo(0, mHeight);

mWavePath1.lineTo(-mItemWidth + mOffsetX, mWaterTop);

mWavePath1.close();

}

这里一开始将路径起点通过moveTo(0,0)移到左上顶角,是为了让整个波浪曲线的高度能达到整个View的高度,否则最终做动画时水波纹遮罩会无法遮盖水波纹以上的空白区域。

然后主要是通过rQuadTo来绘制出每一小节波浪,然后通过for循环,从View左边缘到View右边缘,连接多节小波浪,形成一个完整的水波纹曲线路径。

然后通过属性动画,不断改变波浪路径的起始点,让其整体视觉上形成动态循环波浪的效果:

ValueAnimator offsetAnimator1 = ValueAnimator.ofInt(0, mItemWidth);

offsetAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mOffsetX1 = (int) animation.getAnimatedValue();

invalidate();

}

});

offsetAnimator1.setInterpolator(new LinearInterpolator());

offsetAnimator1.setDuration(500);

offsetAnimator1.setRepeatCount(-1);

offsetAnimator1.start();

以上就完成了一段水波纹路径的绘制,但是对于我们要的效果还不够,因为单纯一段水波纹看起来有点僵硬,需要再多加另一段水波纹来衬托波浪的随性感:

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

mWavePath2.reset();

int wave2ItemWidth = mItemWidth / 2;

int halfItem2 = wave2ItemWidth / 2;

//为了闭合整个View, 否则波浪遮罩顶部显示不正常

mWavePath2.moveTo(0, 0);

//必须先减去一个浪的宽度,以便第一遍动画能够刚好位移出一个波浪,形成无限波浪的效果

mWavePath2.lineTo(-wave2ItemWidth + mOffsetX2, mWaterTop);

for (int i = mLeft - wave2ItemWidth; i < mLeft + wave2ItemWidth + mWidth; i += wave2ItemWidth) {

mWavePath2.rQuadTo(halfItem2 / 2, -mWaveHeight / 2, halfItem2, 0);

mWavePath2.rQuadTo(halfItem2 / 2, mWaveHeight / 2, halfItem2, 0);

}

//闭合路径波浪以下区域

mWavePath2.lineTo(mWidth, mHeight);

mWavePath2.lineTo(0, mHeight);

mWavePath2.lineTo(-wave2ItemWidth + mOffsetX2, mWaterTop);

mWavePath2.close();

}

WavePath2跟WavePath1的不同,在于它的波浪宽改为了一半:int wave2ItemWidth = mItemWidth / 2;,这样这条曲线路径的效果就会稍微比刚才那条紧凑一点(因为每节波浪宽度都变短了),然后我们再在这条波浪的速度上做点手脚:

ValueAnimator offsetAnimator2 = ValueAnimator.ofInt(0, mItemWidth / 2);

mOffsetAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mOffsetX2 = (int) animation.getAnimatedValue();

invalidate();

}

});

offsetAnimator2.setInterpolator(new LinearInterpolator());

offsetAnimator2.setDuration(800);

offsetAnimator2.setRepeatCount(-1);

offsetAnimator2.start();

将偏移的时间周期设置成了800,而刚才第一条水波纹曲线的动画周期是500,从而让它们错开。

但是这还不够,我们让它的波浪激烈程度从一开始缓慢到中间加强,最后再变慢,也就是上面代码中的mWaveHeight这个值也通过属性动画动态调整:

mWaveHeightAnim = ValueAnimator.ofInt(0, getHeight() / 2, 0);

mWaveHeightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mWaveHeight = (int) animation.getAnimatedValue() / 6;

invalidate();

}

});

mWaveHeightAnim.setInterpolator(new AccelerateDecelerateInterpolator());

mWaveHeightAnim.setDuration(5000);

波浪波动的幅度变化过程由0到View的高度/12,再慢慢变回0,并且使用AccelerateDecelerateInterpolator插值器,让它一开始先加速之后慢慢变缓。

最后还有一个,通过属性动画不断调整波浪的起点的纵坐标,从View.getHeight变到0,让它实现水位增高的效果:

ValueAnimator mProgressAnim = ValueAnimator.ofInt(getHeight(), 0);

mProgressAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

mWaterTop = (int) animation.getAnimatedValue();

invalidate();

}

});

mProgressAnim.setInterpolator(new AccelerateDecelerateInterpolator());

mProgressAnim.setDuration(5000);

mProgressAnim.start();

两段水波纹绘制完毕,效果如下:

双重水波纹曲线.gif

2.创建一个圆形Bitmap,用于后面裁剪

这个圆形最终是要用来做遮罩效果所用的,因此这个Bitmap的宽高肯定要填充我们的自定义View,然后我们在该Bitmap的画布上再以中心为圆点,绘制出一个圆。这里创建一个圆形Bitmap,只是为了下一步裁剪显示圆形部分所用,最终这个Bitmap本身是没啥视觉效果的。

private Bitmap mBallBitmap;

@Override

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

super.onLayout(changed, left, top, right, bottom);

mLeft = left;

mTop = top;

mRight = right;

mBottom = bottom;

mWidth = mRight - mLeft;

mHeight = mBottom - mTop;

mBallBitmap = Bitmap.createBitmap((int) mWidth, (int) mHeight, Bitmap.Config.ARGB_8888);

Canvas canvas = new Canvas(mBallBitmap);

canvas.drawCircle(mWidth / 2f, mHeight / 2f, mWidth / 2f - mBallStrokeWidth * 3f / 2f, mIconPaint);

}

3.绘制目标Icon

通过上面两步我们绘制好了波浪线,创建了圆形遮罩,也就是相当于遮罩层的东西我们已经绘制好了,接下来就先把我们的目标Icon绘制出来:

private Drawable mDrawable;

mDrawable = getResources().getDrawable(R.drawable.ic_keep);

@Override

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

super.onLayout(changed, left, top, right, bottom);

mDrawable.setBounds(0, 0, (int) mWidth, (int) mHeight);

}

//绘制Icon

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

mDrawable.draw(canvas);

}

这里采用Drawable的形式,同样是将其设置为View的宽高大小。

4.通过混合模式裁剪

准备好了遮罩和源图,那接下来就可以通过PorterDuffXfermode来混合它们了,PorterDuffXfermode类主要用于图形合成时的图像过渡模式计算,它初始化的时候会传入一个混合模式类型,类似如下:

PorterDuffXfermode duffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

这个PorterDuff.Mode中还有很多模式可供选择,比如说DST_OUT、SRC_IN等等,不同的模式有不同的效果,通俗点讲就是当两个图层叠在一起并且它们之间有交集的时候,通过设置不同的混合模式能够结合成不同的效果,比如说只显示相交部分的源图像,或者只显示相交部分的目标图像,各种模式的效果例图如下:

PorterDuffXfermode模式图解

而我们这个自定义View所需要的场景就是:只显示波浪曲线(步骤1)和圆形遮罩(步骤2)围起来的那部分区域的Icon图像(步骤3),那么我们就在刚才的onDraw中,将圆形遮罩和波浪曲线在混合模式下进行绘制:

PorterDuffXfermode mDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

int layerId = canvas.saveLayer(0, 0, mWidth, mHeight, null, Canvas.ALL_SAVE_FLAG);

//绘制Icon

mDrawable.draw(canvas);

mIconPaint.setXfermode(mDuffXfermode);

//绘制水波纹1

canvas.drawPath(mWavePath1, mIconPaint);

//绘制水波纹2

canvas.drawPath(mWavePath2, mIconPaint);

//绘制圆形位图

canvas.drawBitmap(mBallBitmap, 0, 0, mIconPaint);

mIconPaint.setXfermode(null);

canvas.restoreToCount(layerId);

}

采用DST_IN模式,这种模式下,会在两者相交的地方绘制目标图像,并且绘制的是这块区域里的目标图像。由于图像合成是很昂贵的操作,将用到硬件加速,这里将图像合成的处理通过canvas.save和canvas.restore放到离屏缓存中进行。

可以看到,我们这里先绘制的是Icon,所以我们的Icon是目标图像(SRC),水波纹曲线和圆形遮罩部分则是源图像(DST),从上面图解中也可以看出,DST_IN模式下,相交区域为显示DST的图像,也就是会在我们的圆形和波浪图中显示我们的Icon,这里拿了Keep的logo做素材:

波浪进度球.gif

结语

上面已经实现了主体的效果,支持对图标做进度球效果,同时也添加了对纯色调的波浪球效果的定制,可以设置动画时长球体颜色等等,对一些细节做了优化处理,完整代码已上传到 自定义组件库,欢迎提出优化意见。

欢迎关注 Android小Y 的简书,更多Android精选自定义View

GitHub:GitHub-ZJYWidget

CSDN博客:IT_ZJYANG

简 书:Android小Y

在 GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

关注Android 技术小栈,更多精彩原创

android 图片处理过程中添加进度条,『Android自定义View实战』给我一个图标,还你一个水波纹进度球...相关推荐

  1. android 图片处理过程中添加进度条,[Android] 随时拍图像处理部分总结及源码分......

    ? ? ? ? 随着寒假到来,终于有时间总结下Android这个随手拍的课程作业了,同时分享了我完成的这部分对图像处理的心得吧!你可以结合demo来阅读这篇文章,希望对大家有所帮助. ? ? ? ? ...

  2. android 行布局选择器,『自定义View实战』—— 银行种类选择器

    在工作中难免遇到自定义 View 的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累.自定义View实战 前言 年前的最后一个开发需求,将之前H5开卡界面转变成native.意思就 ...

  3. android 选择银行类型,『自定义View实战』—— 银行种类选择器

    在工作中难免遇到自定义 View 的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累.自定义View实战 前言 年前的最后一个开发需求,将之前H5开卡界面转变成native.意思就 ...

  4. android 画圆弧动画,『Android自定义View实战』自定义带入场动画的弧形百分比进度条...

    写在前面 这是在简书发表的处女座,这个想法也停留在脑海中很久了,一直拖到现在(懒癌发作2333),先自我介绍一番,一枚刚毕业不久的Android程序猿,初出茅庐的Android小生,之前一直在CSDN ...

  5. Android studio 实验过程中遇到的问题之android.support.v7.app.AppCompatActivity不能使用的解决办法

    android.support.v7.app.AppCompatActivity不能使用的解决办法 在构建项目时使用 android.support.v7.XX android.support.v4. ...

  6. android 弹出菜单环形,『Android自定义View实战』实现一个小清新的弹出式圆环菜单...

    前言 Android表现快捷菜单的形式有很多种,比如使用PopupWindow弹出来的小弹窗,类似QQ的侧拉功能菜单,以及之前讲过的弧形菜单( Android 自定义弧形旋转菜单栏--卫星菜单),这次 ...

  7. Java渐变进度条_Android ProgressBar自定义图片进度,自定义渐变色进度条

    java.lang.Object ↳android.view.View ↳android.widget.ProgressBar 直接子类 AbsSeekBar 间接子类 RatingBar, Seek ...

  8. android 动态改变进度条,Android条纹进度条的实现(调整view宽度仿进度条)

    Android条纹进度条的实现(调整view宽度仿进度条) 发布时间:2020-10-03 16:14:24 来源:脚本之家 阅读:89 作者:RustFisher 前言 本文主要给大家介绍了关于An ...

  9. Android进度条函数,Android实用笔记——使用ProgressBar实现进度条

    1.ProgressBar的分类 可以精确显示进度(可以显示刻度或者精确百分比) 不可以精确显示精度(一直转,类似于一个过场动画) 2.关键属性和方法 指定ProgressBar显示风格 style= ...

最新文章

  1. 没听说过这些,就不要说你懂并发了,three。
  2. 分区表--SQLServer创建分区表
  3. 只会python好找工作吗-转行去培训班学Python好找工作吗?老男孩教育
  4. SQL 难点解决:循环计算
  5. mysql+%3c%3e+cdata,bbed 使用实现 drop index 操作
  6. Maven学习笔记(待完善)
  7. 褚时健:现在的年轻人太急了,我快90了还在摸爬滚打
  8. [数分提高]2014-2015-2第7教学周第1次课 (2015-04-14)
  9. jQuery框架学习第七天:jQuery动画–jQuery让页面动起来!
  10. sublime----------快捷键的记录
  11. cmake--生成--动态库
  12. 高等数学复习笔记(三)- 中值定理
  13. just show up失败让我们成长
  14. 无人驾驶 ai算法_质疑AI是否具有意图以及这对无人驾驶汽车意味着什么
  15. JavaScript快速入门-基础
  16. 使用Cubic备份或制作自定义通用型Ubuntu Live ISO镜像适用于 16.04 18.04 20.04 等
  17. HTML5 进阶系列:文件上传下载
  18. 联想小新 Pro 14、联想小新 Pro 16 2023 酷睿版 评测 怎么样值得买吗
  19. 软件工程 sdlcmodel
  20. Linux内核中的IPSEC实现(1)

热门文章

  1. Java中Set集合是如何实现添加元素保证不重复的?
  2. 如何告别那些没卵用的线上告警!
  3. 推荐一款让你纵横Github的读码神器
  4. T1187 强制 NTLM 认证
  5. 5模型数据起别名_DMBOK 06|数据建模与设计
  6. 多项式拟合缺点_机器学习——机器学习算法优缺点综述
  7. C语言比较法排大小,c语言 比较法排序区别
  8. CSS样式----文字样式
  9. Legacy autograd function with non-static forward method is deprecated. Please use new-style autograd
  10. OutOfRangeError closed and has insufficient elements (requested 512, current size 362)