Android自定义View实现水面上涨效果

作者:copor 时间:2023-03-02 22:13:06 

实现效果如下:

Android自定义View实现水面上涨效果

实现思路:

1、如何实现圆中水面上涨效果:利用Paint的setXfermode属性为PorterDuff.Mode.SRC_IN画出进度所在的矩形与圆的交集实现

2、如何水波纹效果:利用贝塞尔曲线,动态改变波峰值,实现“随着进度的增加,水波纹逐渐变小的效果”

话不多说,看代码。

首先是自定义属性值,有哪些可自定义属性值呢?

圆的背景颜色:circle_color,进度的颜色:progress_color,进度显示文字的颜色:text_color,进度文字的大小:text_size,还有最后一个:波纹最大高度:ripple_topheight


<declare-styleable name="WaterProgressView">
<attr name="circle_color" format="color"/><!--圆的颜色-->
<attr name="progress_color" format="color"/><!--进度的颜色-->
<attr name="text_color" format="color"/><!--文字的颜色-->
<attr name="text_size" format="dimension"/><!--文字大小-->
<attr name="ripple_topheight" format="dimension"/><!--水页涟漪最大高度-->
</declare-styleable>

下面是自定义View:WaterProgressView的部份代码:

成员变量


public class WaterProgressView extends ProgressBar {
//默认圆的背景色
public static final int DEFAULT_CIRCLE_COLOR = 0xff00cccc;
//默认进度的颜色
public static final int DEFAULT_PROGRESS_COLOR = 0xff00CC66;
//默认文字的颜色
public static final int DEFAULT_TEXT_COLOR = 0xffffffff;
//默认文字的大小
public static final int DEFAULT_TEXT_SIZE = 18;
//默认的波峰最高点
public static final int DEFAULT_RIPPLE_TOPHEIGHT = 10;

private Context mContext;
private Canvas mPaintCanvas;
private Bitmap mBitmap;

//画圆的画笔
private Paint mCirclePaint;
//画圆的画笔的颜色
private int mCircleColor;

//画进度的画笔
private Paint mProgressPaint;
//画进度的画笔的颜色
private int mProgressColor ;
//画进度的path
private Path mProgressPath;
//贝塞尔曲线波峰最大值
private int mRippleTop = 10;

//进度文字的画笔
private Paint mTextPaint;
//进度文字的颜色
private int mTextColor;
private int mTextSize = 18;
//目标进度,也就是双击时处理任务的进度,会影响曲线的振幅
private int mTargetProgress = 50;

//监听双击和单击事件
private GestureDetector mGestureDetector;
}

获取自定义属性值:


private void getAttrValue(AttributeSet attrs) {

TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView);
mCircleColor = ta.getColor(R.styleable.WaterProgressView_circle_color,DEFAULT_CIRCLE_COLOR);    
mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color,DEFAULT_PROGRESS_COLOR);
mTextColor = ta.getColor(R.styleable.WaterProgressView_text_color,DEFAULT_TEXT_COLOR);  
mTextSize = (int) ta.getDimension(R.styleable.WaterProgressView_text_size, DesityUtils.sp2px(mContext,DEFAULT_TEXT_SIZE));
mRippleTop = (int)ta.getDimension(R.styleable.WaterProgressView_ripple_topheight,DesityUtils.dp2px(mContext,DEFAULT_RIPPLE_TOPHEIGHT));
ta.recycle();

}

定义构造函数,注意 mProgressPaint.setXfermode


//当new该类时调用此构造函数
public WaterProgressView(Context context) {
this(context,null);
}

//当xml文件中定义该自定义View时调用此构造函数
public WaterProgressView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}

public WaterProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
getAttrValue(attrs);
//初始化画笔的相关属性
initPaint();
mProgressPath = new Path();
}

private void initPaint() {
//初始化画圆的paint
mCirclePaint = new Paint();
mCirclePaint.setColor(mCircleColor);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true);
mCirclePaint.setDither(true);

//初始化画进度的paint
mProgressPaint = new Paint();
mProgressPaint.setColor(mProgressColor);
mProgressPaint.setAntiAlias(true);
mProgressPaint.setDither(true);
mProgressPaint.setStyle(Paint.Style.FILL);
//其实mProgressPaint画的也是矩形,当设置xfermode为PorterDuff.Mode.SRC_IN后则显示的为圆与进度矩形的交集,则为半圆
mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

//初始化画进度文字的画笔
mTextPaint = new Paint();
mTextPaint.setColor(mTextColor);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setAntiAlias(true);
mTextPaint.setDither(true);
mTextPaint.setTextSize(mTextSize);

}

onMeasure()方法代码:


@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//使用时,需要明确定义该View的尺寸,即用测量模式为MeasureSpec.EXACTLY
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width,height);

//初始化Bitmap,让所有的drawCircle,drawPath,drawText都draw在该bitmap所在的canvas上,然后再将该bitmap 画在onDraw方法的canvas上,
//所以此bitmap的width,height需要减去left,top,right,bottom的padding
mBitmap = Bitmap.createBitmap(width-getPaddingLeft()-getPaddingRight(),height- getPaddingTop()-getPaddingBottom(), Bitmap.Config.ARGB_8888);
mPaintCanvas = new Canvas(mBitmap);
}

接下来是核心部份,onDraw中的代码。我们先将Circle,进度条,进度文字draw到自定义canvas的bitmap上,再将此bitmap draw到onDraw方法中的canvas上。drawCircle与drawText应该没什么难度,关键点就在于画进度条,怎么画呢?既然有水波纹效果,有曲线,就用drawPath了。

drawPath的流程如下:

Android自定义View实现水面上涨效果

其中ratio的代码如下,即ratio为当前进度占总进度的百分比


float ratio = getProgress()*1.0f/getMax();

因为坐标是从B点向下和向右正向延伸的,则A点的坐标为(width,(1-ratio)*height),其中width为bitmap的宽,height为bitmap的高。我们先将mProgressPath.moveTo到A点,然后从A点顺时针方向确定path的各个关键点,如图,则代码如下:


int rightTop = (int) ((1-ratio)*height);
mProgressPath.moveTo(width,rightTop);
mProgressPath.lineTo(width,height);
mProgressPath.lineTo(0,height);
mProgressPath.lineTo(0,rightTop);

如此mProgressPath已经lineTo到了C点,需要在A点与C点之间形成水波纹效果,则需要在A点与C点间画贝塞尔曲线。

Android自定义View实现水面上涨效果

我们设定波峰最高点为10,则一段波长为40,需要画width*1.0f/40段这样的曲线,则画曲线的代码如下:


int count = (int) Math.ceil(width*1.0f/(10 *4));
for(int i=0; i<count; i++) {
mProgressPath.rQuadTo(10,10,2* 10,0);
mProgressPath.rQuadTo(10,-10,2* 10,0);  
}

mProgressPath.close();
mPaintCanvas.drawPath(mProgressPath,mProgressPaint);


这样就能画出水面上涨且有波纹效果的进度条了。但我们还要实现随着水面上涨,越接近目标进度,水面波纹应该越来越小,则应该把10抽出为变量定义为mRippleTop等初始时波峰最大值,然后定义top为随着进度不断接近目标进度时曲线的实时波峰值 ,其中mTargetProgress为目标progress,因为有一个目标进度才能实现当前进度不断接近目标进度的过程中,水面渐趋于平面的效果:


float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;

所以drawPath的代码更新如下:


float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;

for(int i=0; i<count; i++) {
mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);
mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);
}

如此就能真正实现水面上涨的进度条了。

但如何实现图中双击时水面从0%上涨到目标进度,单击时水面在目标进度不断涌动的效果呢?
先说说双击效果的实现:这个简单,定义一个Handler,,当双击时,handler.postDelayed(runnable,time) ,每隔一段时间progress+1,在runnable中invalidate()不断更新进度,直到当前progress到达mTargetProgress。

代码如下


/**
* 实现双击动画
*/
private void startDoubleTapAnimation() {
setProgress(0);
doubleTapHandler.postDelayed(doubleTapRunnable,60);
}

private Handler doubleTapHandler = new Handler(){
@Override
public void handleMessage(Message msg) {  
super.handleMessage(msg);
}
};

//双击处理线程,隔60ms发送一次数据
private Runnable doubleTapRunnable = new Runnable() {
@Override
public void run() {  
if(getProgress() < mTargetProgress) {  
 invalidate();  
 setProgress(getProgress()+1);  
 doubleTapHandler.postDelayed(doubleTapRunnable,60);  
} else {  
 doubleTapHandler.removeCallbacks(doubleTapRunnable);  
}
}
};

双击效果实现了,那如何实现单击效果呢?单击时要求水面不断涌动一段时间,水面波纹逐渐变小,然后水面变平。我们可以定义一个mSingleTapAnimationCount变量为水面涌动的次数,然后像双击时的处理一样,定义一个Handler隔一段时间发送一次更新界面的message,mSingleTapAnimationCount-- ,然后我们交替地让初始时的波峰一次为正一次为负,则能实现水面涌动的效果。

核心代码如下:


private void startSingleTapAnimation() {
isSingleTapAnimation = true;
singleTapHandler.postDelayed(singleTapRunnable,200);
}

private Handler singleTapHandler = new Handler(){
@Override
public void handleMessage(Message msg) {  
super.handleMessage(msg);
}
};

//单击处理线程,隔200ms发送一次数据
private Runnable singleTapRunnable = new Runnable() {
@Override
public void run() {  
if(mSingleTapAnimationCount > 0) {  
 invalidate();  
 mSingleTapAnimationCount--;  
 singleTapHandler.postDelayed(singleTapRunnable,200);  
} else {  
 singleTapHandler.removeCallbacks(singleTapRunnable);  
//是否正在进行单击动画  
 isSingleTapAnimation = false;  
//重置单击动画运行次数为50次
 mSingleTapAnimationCount = 50;  
}
}
};

onDraw中的代码作相应的更改,因单击与双击时drawPath中曲线部分的绘制逻辑不一样,则我们定义一个变量isSingleTapAnimation 区别是正在进行单击动画还是在进行双击动画。

更改后的代码如下:


//画进度
mProgressPath.reset();
//从右上边开始draw path
int rightTop = (int) ((1-ratio)*height);
mProgressPath.moveTo(width,rightTop);
mProgressPath.lineTo(width,height);
mProgressPath.lineTo(0,height);
mProgressPath.lineTo(0,rightTop);

//画贝塞尔曲线,形成波浪线
int count = (int) Math.ceil(width*1.0f/(mRippleTop *4));
//不是单击animation状态
if(!isSingleTapAnimation&&getProgress()>0) {
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
for(int i=0; i<count; i++) {  
mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);  
mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);
}
} else {
//单击animation状态,为了将效果放大,将mRippleTop放大2倍
//同时偶数时曲线走向如图所示,奇数时则曲线刚好相反
float top = (mSingleTapAnimationCount*1.0f/50)*10;
//奇偶数时曲线切换
if(mSingleTapAnimationCount%2==0) {  
 for(int i=0; i<count; i++) {  
 mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);    
 mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);  
}
} else {  
for(int i=0; i<count; i++) {  
 mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);    
 mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);
}
}
}
mProgressPath.close();
mPaintCanvas.drawPath(mProgressPath,mProgressPaint);

基本上重要的代码与核心逻辑与代码就在上面了。

注意点:

1、当drawCircle时要考虑到padding,则circle的宽和高为getWidth与getHeight减去padding值,代码如下:


//自定义bitmap的宽和高
int width = getWidth()-getPaddingLeft()-getPaddingRight();
int height = getHeight()-getPaddingTop()-getPaddingBottom();

//画圆
mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);

2、当drawText时,不是从text的height的中间开始draw的,而是从baseline开始draw的

Android自定义View实现水面上涨效果

那如何获取baseline的height坐标呢


Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
//因为ascent在baseline之上,所以ascent为负数。descent+ascent为负数,所以是减而不是加
float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;

drawText的全部代码如下:


//画进度文字
String text = ((int)(ratio*100))+"%";

//获得文字的宽度
float textWidth = mTextPaint.measureText(text);

Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
//descent+ascent为负数,所以是减而不是加
float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;
mPaintCanvas.drawText(text,width/2-textWidth/2,baseLine,mTextPaint);

3、因为要顾及到padding,记得将onDraw中的canvas translate到(getPaddingLeft(),getPaddingTop())处。


canvas.translate(getPaddingLeft(),getPaddingTop());
canvas.drawBitmap(mBitmap,0,0,null);

最后记得将自定义的bitmap draw到onDraw中的canvas上。到这儿自定义水面上涨效果的进度条于写完了。

总结

标签:android,view
0
投稿

猜你喜欢

  • mybatis查询返回Map<String,Object>类型的讲解

    2022-12-25 02:07:38
  • Java虚拟机JVM性能优化(三):垃圾收集详解

    2021-10-16 10:38:49
  • c#设计模式之单例模式的实现方式

    2021-12-30 19:46:33
  • 解析Java中的static关键字

    2022-03-02 17:15:35
  • JavaWeb详细讲述Cookie和Session的概念

    2022-03-23 08:39:44
  • Android Recyclerview实现多选,单选,全选,反选,批量删除的功能

    2021-12-10 21:57:24
  • 超详细的Java 问题排查工具单

    2023-02-10 06:02:17
  • Flutter快速制作一个水印组件实例详解

    2023-05-16 23:32:20
  • Android横竖屏切换及其对应布局加载问题详解

    2023-12-17 20:45:13
  • Android自定义EditText实现淘宝登录功能

    2023-04-20 01:33:04
  • Yml转properties文件工具类YmlUtils的详细过程(不用引任何插件和依赖)

    2021-08-18 03:55:00
  • Java 多线程并发ReentrantLock

    2022-02-23 11:53:23
  • Android广播接收机制详细介绍(附短信接收实现)

    2023-04-16 10:09:57
  • mybatisplus逻辑删除基本实现和坑点解决

    2021-05-24 11:35:50
  • Android实现简单旋转动画

    2023-11-07 09:50:58
  • java实现图片角度旋转并获得图片信息

    2022-03-25 10:45:00
  • Java获取时间打印到控制台代码实例

    2022-07-20 22:43:48
  • C# 使用鼠标点击对Chart控件实现数据提示效果

    2023-03-05 14:20:06
  • Android DrawerLayout带有侧滑功能的布局类(1)

    2023-04-09 20:32:07
  • Android实现简易秒表功能

    2021-08-29 11:06:58
  • asp之家 软件编程 m.aspxhome.com