时间:2021-10-18 10:05:56 | 栏目:Android代码 | 点击:次
效果图
实现思路
这个效果实现起来并不难,重要的是思路
此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>
各个属性的作用如下
创建自定义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.jb51.net/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中的圆存储的数量不宜过多,多了内存消耗大,需要在当圆的半径超过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的总结部分在我的其它博客中已经写过蛮多了,有兴趣的可以去看看