在Android开发中,我们的UI设计师可能会设计出一些比较比较炫酷或者个性的效果然后需要我们来实现,今天给大家分享一个使用Android自定义控件实现的水波纹效果。
废话不多说,我们直接来看一下效果图:
效果图已经看到了,这个控件主要是实现给定一个百分比的值,然后就可以自动绘制水位不断上涨直到达到给定的值。同时提供了几个方法(开始、暂停和重置)和一个接口(通过设置这个接口,在百分比的值发生变化时,可以实时获取到当前绘制的比例)。
在这里我只贴出一部分代码,所有的代码已经上传,优快云下载 GitHub下载
首先是构造方法:
public WaveView(Context context) {
this(context, null);
}
public WaveView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs); // 获取布局文件中定义的属性
init();
}
提供了三个构造方法,可以在布局文件中声明控件,也可以在代码中创建控件;
下面的方法是获取布局文件中的属性的方法:
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveView);
mWaveLenght1 = typedArray.getInteger(R.styleable.WaveView_wave1Length, WAVE_LENGTH1);
mWaveHeight1 = typedArray.getInteger(R.styleable.WaveView_wave1Height, WAVE_HEIGHT1);
mWaveColor1 = typedArray.getColor(R.styleable.WaveView_wave1Color, WAVE_COLOR1);
mOffset1 = typedArray.getInteger(R.styleable.WaveView_wave1Offset, WAVE_OFFSET1);
mWaveLenght2 = typedArray.getInteger(R.styleable.WaveView_wave2Length, WAVE_LENGTH2);
mWaveHeight2 = typedArray.getInteger(R.styleable.WaveView_wave2Height, WAVE_HEIGHT2);
mWaveColor2 = typedArray.getColor(R.styleable.WaveView_wave2Color, WAVE_COLOR2);
mOffset2 = typedArray.getInteger(R.styleable.WaveView_wave2Offset, WAVE_OFFSET2);
mBorderWidth = typedArray.getDimensionPixelSize(R.styleable.WaveView_borderWidth, BORDER_WIDTH);
mBorderColor = typedArray.getColor(R.styleable.WaveView_borderColor, BORDER_COLOR);
mTextSize = typedArray.getDimensionPixelSize(R.styleable.WaveView_textSize, DEFAULT_TEXT_SIZE);
mTextColor = typedArray.getColor(R.styleable.WaveView_textColor, DEFAULT_TEXT_COLOR);
mTime = typedArray.getInteger(R.styleable.WaveView_intervalTime, DEFAULT_TIME);
mPrecent = typedArray.getFloat(R.styleable.WaveView_precent, 0.5f);
int shapeValue = typedArray.getInteger(R.styleable.WaveView_showShape, 0);
if (shapeValue == 0) mShape = ShowShape.RECT;
else mShape = ShowShape.CIRCLE;
typedArray.recycle();
}
以上属性是可以直接在布局文件中定义的,然后通过上面这段代码读取到具体的值,
需要说明的是,如果直接把上传代码中的WaveView.java文件拷贝到自己的项目中,一定要记得将res\values目录下的attrs.xml文件中的一下属性声明拷贝到自己项目中的attrs.xml文件中,否则自定义属性会报错;同时在使用自定义属性时也要申明这些属性所在的名称空间,如果对自定义属性不是很了解的话,可以访问《Android自定义View的基本步骤和使用自定义属性》这篇博客的 “在自定义控件中使用自定义属性” 这个部分。
下面是attrs.xml文件中定义的属性:
<!--自定义水波纹效果属性-->
<declare-styleable name="WaveView">
<!--波浪1的长度、高度、颜色和每次重绘的偏移量-->
<attr name="wave1Length" format="integer" />
<attr name="wave1Height" format="integer" />
<attr name="wave1Color" format="color" />
<attr name="wave1Offset" format="integer" />
<!--波浪2的长度、高度、颜色和每次重绘的偏移量-->
<attr name="wave2Length" format="integer" />
<attr name="wave2Height" format="integer" />
<attr name="wave2Color" format="color" />
<attr name="wave2Offset" format="integer" />
<!--边框的宽度和颜色-->
<attr name="borderWidth" format="dimension" />
<attr name="borderColor" format="color" />
<!--文字的大小和颜色-->
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<!--水位高度的百分比-->
<attr name="precent" format="float" />
<!--两次重绘的间隔时间-->
<attr name="intervalTime" format="integer" />
<!--控件的显示形状,rect矩形、circle圆形-->
<attr name="showShape" format="enum">
<enum name="rect" value="0" />
<enum name="circle" value="1" />
</attr>
</declare-styleable>
在刚刚的构造函数中调用了两个方法,一个就是上面已经贴出来的获取xml文件中的属性的方法,还有一个就是下面的方法,主要是做一些初始化相关对象:
/**
* 初始化方法
*/
private void init() {
mWavePaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
mWavePaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mWavePath1 = new Path();
mWavePath2 = new Path();
mCirclePath = new Path();
mWavePaint1.setColor(mWaveColor1);
mWavePaint1.setStyle(Paint.Style.FILL);
mWavePaint2.setColor(mWaveColor2);
mWavePaint2.setStyle(Paint.Style.FILL);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderPaint.setStyle(Paint.Style.STROKE);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
// 定义数字显示个格式
mFormat = new DecimalFormat("###,###,##0.00");
}
需要实现圆形形状,我们需要计算出圆心和半径,需要实现水位的上升以及上升到指定的百分比高度就不在上升,我们需要通过指定的百分比和控件的大小来确定水位的高度,这个计算我们重写一个方法,onSizeChanged(int w, int h, int oldw, int oldh),这个方法是在控件的大小发生改变时系统回调的,所以我们在这个方法里面计算相关的值:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
mHeight = h;
mChangeY = mHeight;
// 计算波峰个数,为了实现移动效果,保证至少绘制两个波峰
mWaveCount = (int) Math.round(mWidth / Math.max(mWaveLenght1, mWaveLenght2) + 1.5);
mFinalY = (1 - mPrecent) * mHeight; // 计算水位最终高度
// 计算圆心和半径
mCenterX = mWidth / 2;
mCenterY = mHeight / 2;
mRadius = Math.min(mWidth, mHeight) / 2;
}
下面贴出来的代码就是怎个控件最核心的代码,也就是onDraw()绘制方法,注释已经写得很详细,我就不做过多的解释:
@Override
protected void onDraw(Canvas canvas) {
mWavePath1.reset();
mWavePath2.reset();
mCirclePath.reset();
// 如果想要显示更多的形状,在这里剪切不同形状即可
if (mShape == ShowShape.CIRCLE) {
// 关闭硬件加速,否则canvas的剪切无法生效
if(canvas.isHardwareAccelerated() || this.isHardwareAccelerated() && invalidateFlag){
this.setLayerType(LAYER_TYPE_SOFTWARE,null);
}
// 先判断是否为圆形,如果是,直接使用canvas的方法剪切一个圆形显示,其余地方不显示
mCirclePath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW);
canvas.clipPath(mCirclePath);
}
if (mBorderWidth > 0) {
// 边框大于0,表示需要绘制边框
if (mShape == ShowShape.CIRCLE) {
canvas.drawCircle(mCenterX, mCenterY, mRadius, mBorderPaint);
} else if (mShape == ShowShape.RECT) {
canvas.drawRect(0, 0, mWidth, mHeight, mBorderPaint);
}
}
mWavePath1.moveTo(-mWaveLenght1, mChangeY);
mWavePath2.moveTo(-mWaveLenght2, mChangeY);
if (!isReset) { // 判断重置标记
// 利用贝塞尔曲线绘制波浪
for (int i = 0; i < mWaveCount; i++) {
// 绘制波浪1的曲线
mWavePath1.quadTo((-mWaveLenght1 * 3 / 4) + (i * mWaveLenght1) + mMoveSum1, mChangeY + mWaveHeight1, (-mWaveLenght1 / 2) + (i * mWaveLenght1) + mMoveSum1, mChangeY);
mWavePath1.quadTo((-mWaveLenght1 * 1 / 4) + (i * mWaveLenght1) + mMoveSum1, mChangeY - mWaveHeight1, (i * mWaveLenght1) + mMoveSum1, mChangeY);
// 绘制波浪2的曲线
mWavePath2.quadTo((-mWaveLenght2 * 3 / 4) + (i * mWaveLenght2) + mMoveSum2, mChangeY - mWaveHeight2, (-mWaveLenght2 / 2) + (i * mWaveLenght2) + mMoveSum2, mChangeY);
mWavePath2.quadTo((-mWaveLenght2 * 1 / 4) + (i * mWaveLenght2) + mMoveSum2, mChangeY + mWaveHeight2, (i * mWaveLenght2) + mMoveSum2, mChangeY);
}
// 不断改变高度,实现逐渐水位逐渐上涨效果
mChangeY -= 1;
if (mChangeY < mFinalY) mChangeY = mFinalY;
// 波峰1往右移动,波峰2往左移动
mMoveSum1 += mOffset1;
mMoveSum2 -= mOffset2;
if (mMoveSum1 >= mWaveLenght1) mMoveSum1 = 0;
if (mMoveSum2 <= 0) mMoveSum2 = mWaveLenght2;
// 填充矩形,让上涨之后的水位下面填充颜色
mWavePath1.lineTo(mWidth, mHeight);
mWavePath1.lineTo(0, mHeight);
mWavePath1.close();
mWavePath2.lineTo(mWidth, mHeight);
mWavePath2.lineTo(0, mHeight);
mWavePath2.close();
canvas.drawPath(mWavePath1, mWavePaint1);
canvas.drawPath(mWavePath2, mWavePaint2);
} else {
// 是重置
canvas.drawColor(Color.TRANSPARENT);
}
// 计算当前的百分比
mCurrentPrecent = 1 - mChangeY / mHeight;
// 格式化数字格式
String format1 = mFormat.format(mCurrentPrecent);
// 绘制文字,将百分比绘制到界面。此处用的是 "50%" 的形式,可以根据需求改变格式
double parseDouble = Double.parseDouble(format1);
canvas.drawText((int) (parseDouble * 100) + " %", (mWidth - mTextPaint.measureText(format1)) / 2, mHeight / 5, mTextPaint);
// 监听对象不为null并且没有达到设置高度时,调用监听方法
if (mPrecentChangeListener != null && mChangeY != mFinalY) {
mPrecentChangeListener.precentChange(parseDouble);
}
// 判断绘制标记
if (invalidateFlag) postInvalidateDelayed(mTime);
}
在以上代码中,我是通过贝塞尔曲线来绘制波浪的;然后当设置显示的形状是圆形的时候,我是通过canvas的剪切区属性来设置的,只是在使用这个方法的时候需要关闭硬件加速,否则就达不到效果。当然,要定义圆形效果,我们还可以使用图像的混合模式(PorterDuffXfermode)来做。具体的做法可以参照《
Android自定义View之基本API(二)》这篇博客最后面的 “位图运算PorterDuffXfermode” 部分的一个实例,制作一个简单的圆形图片。
在这个自定义的水波纹的代码中,还定义的一个枚举来表示形状,一个监听来获取不断变化的百分比的值:
/**
* 形状枚举,暂时只支持矩形和圆形,可扩展
*/
public enum ShowShape {
RECT, CIRCLE
}
/**
* 百分比改变监听接口
*/
public interface PrecentChangeListener {
/**
* 百分比发生改变时调用的方法
*
* @param precent 当前的百分比,格式 0.00 范围 [0.00 , 1.00]
*/
void precentChange(double precent);
}
在最后提供了上面效果图中几个按钮的方法:
/**
* 开始
*/
public void start() {
invalidateFlag = true;
isReset = false;
postInvalidateDelayed(mTime);
}
/**
* 暂停
*/
public void stop() {
invalidateFlag = false;
isReset = false;
}
/**
* 重置
*/
public void reset() {
invalidateFlag = false;
isReset = true;
mChangeY = mHeight;
postInvalidate();
}
以上就是自定义水波纹效果控件的主要方法,在文章最后贴出在代码和布局文件中设置属性的代码:
// 代码设置相关属性
waveview1.setBorderWidth(2)
.setWaveColor1(Color.RED)
.setWaveColor2(Color.parseColor("#80ff0000"))
.setBorderColor(Color.GREEN)
.setTextColor(Color.BLUE)
.setShape(WaveView.ShowShape.RECT)
.setTextSize(36)
.setPrecent(0.65f)
.setTime(2);
在布局文件中设置属性:
<com.waveview.view.WaveView
android:id="@+id/waveview"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="10dp"
renj:borderColor="#00ff00"
renj:borderWidth="2dp"
renj:intervalTime="3"
renj:precent="0.6"
renj:showShape="circle"
renj:textColor="#0000ff"
renj:textSize="18sp"
renj:wave1Color="#ff0000"
renj:wave2Color="#80ff0000"/>
记得添加名称空间:
xmlns:renj="http://schemas.android.com/apk/res-auto"
在代码中设置监听器的方法:
waveview.setPrecentChangeListener(new WaveView.PrecentChangeListener() {
@Override
public void precentChange(double precent) {
tvPrecent.setText("当前进度:" + precent + "");
}
});
上面效果图的完整代码已上传, 优快云下载 GitHub下载,是一个完整的Android studio项目,不要导入,直接用studio打开一个新项目即可,打开之后直接运行就和上面的效果图一样。