专栏首页恩蓝脚本Android自定义View控件实现多种水波纹涟漪扩散效果

Android自定义View控件实现多种水波纹涟漪扩散效果

效果图

实现思路

这个效果实现起来并不难,重要的是思路

此View满足了多种水波纹涟漪扩散效果,这要求它能满足很多的变化

根据上面的样式,可以看出此View需要满足以下变化

  • 圆圈从中心可循环向外扩散
  • 圆圈之间的扩散间距可以改变
  • 可控制扩散圆的渐变度
  • 圆圈可以是线条样式或者实心样式
  • 圆圈扩散的速度可以控制
  • 适配圆圈不同大小下的扩散效果

具体实现

创建自定义属性

首先为View创建自定义的xml属性

在工程的values目录下新建attrs.xml文件

 <declare-styleable name="mRippleView" 
 <attr name="cColor" format="color"/ 
 <attr name="cSpeed" format="integer"/ 
 <attr name="cDensity" format="integer"/ 
 <attr name="cIsFill" format="boolean"/ 
 <attr name="cIsAlpha" format="boolean"/ 
 </declare-styleable 

各个属性的作用如下

  • cColor:View控件的颜色
  • cSpeed:向外扩散的速度
  • cDensity:圆形波纹扩散的间距
  • cIsFill:是否开启填充模式,true为实心圆
  • cIsAlpha:是否开启渐变效果,true为开启

创建自定义View控件

新建RippleView类继承View类,重写它的三个构造方法,获取用户设置的属性,同时指定默认值

 public RippleView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 // 获取用户配置属性
 TypedArray tya = context.obtainStyledAttributes(attrs, R.styleable.mRippleView);
 mColor = tya.getColor(R.styleable.mRippleView_cColor, Color.BLUE);
 mSpeed = tya.getInt(R.styleable.mRippleView_cSpeed, 1);
 mDensity = tya.getInt(R.styleable.mRippleView_cDensity, 10);
 mIsFill = tya.getBoolean(R.styleable.mRippleView_cIsFill, false);
 mIsAlpha = tya.getBoolean(R.styleable.mRippleView_cIsAlpha, false);
 tya.recycle();
 init();
 }

使用TypedArray读取完自定义的属性后一定要记得调用recycle方法释放掉

重写onMeasure

测量onMeasure,首先需要测量出View的宽和高,并指定View在wrap_content时的最小范围,对于View绘制流程还不熟悉的同学,可以先去了解下具体的绘制流程

https://www.zalou.cn/article/118775.htm

重写onMeasure方法,其中我们要考虑当View的宽高被指定为wrap_content时的情况,如果我们不对wrap_content的情况进行处理,那么当使用者指定View的宽高为wrap_content时将无法正常显示出View

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 int myWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
 int myWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
 int myHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
 int myHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
 // 获取宽
 if (myWidthSpecMode == MeasureSpec.EXACTLY) {
  // match_parent/精确值
  mWidth = myWidthSpecSize;
 } else {
  // wrap_content
  mWidth = DensityUtil.dip2px(mContext, 120);
 }

 // 获取高
 if (myHeightSpecMode == MeasureSpec.EXACTLY) {
  // match_parent/精确值
  mHeight = myHeightSpecSize;
 } else {
  // wrap_content
  mHeight = DensityUtil.dip2px(mContext, 120);
 }
 // 设置该view的宽高
 setMeasuredDimension(mWidth, mHeight);
 }

MeasureSpec的状态分为三种EXACTLY、AT_MOST、UNSPECIFIED,这里只要单独指定非精确值EXACTLY之外的情况就好了

本文中使用到的DensityUtil类,是为了将dp转换为px来使用,以便适配不同的屏幕显示效果

 public static int dip2px(Context context, float dpValue) {
 final float scale = context.getResources().getDisplayMetrics().density;
 return (int) (dpValue * scale + 0.5f);
 }

重写onDraw

设计的整体思路如下图所示

先要实现圆形向外扩散的效果

  • 初始化第一个圆

这里的动画效果本来是想使用ValueAnimator属性动画的数值发生器来实现,但是我们这里有很多的计算需求,所以最后还是选择使用算法来实现,方便控制圆的一些参数

想要实现扩散的效果,这里思路是在每次更新View时动态改变圆的半径,同时还需要给圆设置渐变度数,所以决定用一个类来保存圆的状态,所有圆都存在一个List里

// 添加第一个圆圈
mRipples = new ArrayList< ();
Circle c = new Circle(0, 255);
mRipples.add(c);

传入Circle类里的两个参数,第一个0表示圆的初始宽度,第二个255表示初始透明度

  • 添加新圆

要想实现不断有圆向外扩散,就需要在第一个圆扩散到一定范围时在圆心处再添加一个圆,这个的范围可以由圆的半径来控制,当List集合中最后一个圆的半径增加到某个值mDensity时,新的圆就从圆心处创建出来

// 添加圆
if (mRipples.size()   0) {
 // 控制第二个圆出来的间距
 if (mRipples.get(mRipples.size() - 1).width   DensityUtil.dip2px(mContext, mDensity)) {
 mRipples.add(new Circle(0, 255));
 }
}
  • 删除List中多余的圆

List中的圆存储的数量不宜过多,多了内存消耗大,需要在当圆的半径超过View的宽度时就删掉这个圆

// 当圆超出View的宽度后删除
if (c.width   mWidth / 2) {
 mRipples.remove(i);
}

我们也可以在外切正方形的顶点处删除这个圆,需要用到勾股定律来计算扩散圆到外切正方形顶点的位置

如上图所示,得出计算公式为

// 使用勾股定律求得一个外切正方形中心点离顶点的距离
sqrtNumber = (int) (Math.sqrt(mWidth * mWidth + mHeight * mHeight) / 2);

这样就需要修改删除圆的位置了

if (c.width   sprtNumber) {
 mRipples.remove(i);
}
  • 控制扩散圆的渐变度

当圆在向View的边缘扩散时,渐变度数的改变需要动态来计算,渐变的计算算法要适配不同的圆宽度大小,我们知道透明度是0~255之间的,0表示完全透明,255表示百分百不透明,计算的时候就是需要将这个数值等份分配到圆的宽度里

这里要区分一点,对于圆来说,宽度是由圆心从0开始向外递增,而渐变度数则是由圆心从255开始向外递减,当圆与最外围的正方形内切时渐变度必须变为0,由此分析得知,公式如下

透明度 = 255 – 圆的宽度 * (255 / View宽度)

double alpha = 255 - c.width * (255 / ((double) mWidth / 2));
c.alpha = (int) alpha;

GitHub地址

https://github.com/zhuwentao2150/RippleView

总结

关于自定义View的总结部分在我的其它博客中已经写过蛮多了,有兴趣的可以去看看

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对ZaLou.Cn的支持。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 5分钟快速实现Android爆炸破碎酷炫动画特效的示例

    先来看下是怎样的动效,要是感觉不是理想的学习目标,就跳过,避免浪费大家的时间。��

    砸漏
  • Android开发中软键盘的显示和隐藏

    本篇内容通过操作软键盘的函数着手详细分析了隐藏或者显示软键盘的实现方法,并且对其中重要的代码做了详细分析。

    砸漏
  • Android仿QQ长按弹出删除复制框

    本文实例为大家分享了Android仿QQ长按删除弹出框的具体代码,供大家参考,具体内容如下

    砸漏
  • ViewRootImpl的独白,我不是一个View(布局篇)

    前一段时间写过两篇关于View的文章 Activity中的Window的setContentView 和 遇见LayoutInflater&Factory 。分...

    静默加载
  • Android自定义View系列 (从小白做起) 二: 相知

    上一章节中,主要介绍了三个主要成分 1.LayoutInflater.inflate()的参数及其用法 2.四种构造函数的说明,以及使用的地方 3.工具Pain...

    Anymarvel
  • Android : 控制图片如何resized/moved来匹对ImageView的size

    android:scaleType是控制图片如何resized/moved来匹对ImageView的size。

    一个会写诗的程序员
  • 笔记50 | Android自定义View(一)

    项勇
  • 自定义View Layout过程 - 最易懂的自定义View原理系列(3)

    a. 步骤2 类似于 单一View的layout过程 b. 自上而下、一层层地传递下去,直到完成整个View树的layout()过程

    Carson.Ho
  • 5分钟快速实现Android爆炸破碎酷炫动画特效的示例

    先来看下是怎样的动效,要是感觉不是理想的学习目标,就跳过,避免浪费大家的时间。��

    砸漏
  • 关于招聘的那点事:哪些职位薪水更高,哪些公司待遇更好

    之前爬取了一批拉勾的数据,包括将近10w个公司以及100w个招聘职位的数据,放在服务器上好几个月了,最近拿来做一些分析,得出以下结论。

    华章科技

扫码关注云+社区

领取腾讯云代金券