场景
最近重新学了下自定义View打算仿造一下慕课学院的下拉刷新的水波纹进度框。先上效果图:
实现思路
1.加入图片,并根据控件大小处理图片大小
2.在临时画布上绘画图片图层和水波纹图层,并合并成图片。
3.在画布上绘制合成的图片并调用invalidate();方法去重新计算绘制水波纹图层;
首先让我们的控件去继承View,定义一些常量和自定义View的初始化:
/**
* Y方向上的每次增长值
*/
private int increateHeight;
/**
* X方向上的每次增长值
*/
private final int INCREATE_WIDTH = 0x00000005;
/**
* 画笔
*/
private Paint mPaint;
/**
* 临时画布
*/
private Canvas mTempCanvas;
/**
* 贝塞尔曲线路径
*/
private Path mBezierPath;
/**
* 当前波纹的y值
*/
private float mWaveY;
/**
* 贝塞尔曲线控制点距离原点x的增量
*/
private float mBezierDiffX;
/**
* 水波纹的X左边是否在增长
*/
private boolean mIsXDiffIncrease = true;
/**
* 水波纹最低控制点y
*/
private float mWaveLowestY;
/**
* 来源图片
*/
private Bitmap mOriginalBitmap;
/**
* 来源图片的宽度
*/
private int mOriginalBitmapWidth;
/**
* 来源图片的高度
*/
private int mOriginalBitmapHeight;
/**
* 临时图片
*/
private Bitmap mTempBitmap;
/**
* 组合图形
*/
private Bitmap mCombinedBitmap;
/**
* 是否测量过
*/
private boolean mIsMeasured = false;
/**
* 停止重绘
*/
private boolean mStopInvalidate = false;
关于图片的大小,这里我希望在MeasureSpec.AT_MOST的时候让控件保持和图片大小一致,在MeasureSpec.EXACTLY模式下让图片大小跟随控件大小而改变,两种模式下都需考虑padding情况。
先写一个处理图片缩放的方法:
/**
* 按比例缩放图片
*
* @param origin 原图
* @param widthRatio width缩放比例
* @param heightRatio heigt缩放比例
* @return 新的bitmap
*/
private Bitmap scaleBitmap(Bitmap origin, float widthRatio, float heightRatio) {
int width = origin.getWidth();
int height = origin.getHeight();
Matrix matrix = new Matrix();
matrix.preScale(widthRatio, heightRatio);
Bitmap newBitmap = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
if (newBitmap.equals(origin)) {
return newBitmap;
}
origin.recycle();
origin = null;
return newBitmap;
}
在View的onMeasure()方法中,根据测量模式的不同分别处理图片,而处理图片的步骤只需要执行一次,为避免onMeasure()方法多次调用而造成资源浪费,引入一个flag变量mIsMeasured来规避这个问题。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (null == mTempBitmap) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
float widthRatio = 1f, heightRatio = 1f;
if (MeasureSpec.AT_MOST == widthMode) {
widthSize = mTempBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
}
if (MeasureSpec.AT_MOST == heightMode) {
heightSize = mTempBitmap.getHeight() + getPaddingLeft() + getPaddingRight();
}
//只在首次绘制的时候进行onDraw()操作前的初始化
if (!mIsMeasured) {
if (MeasureSpec.EXACTLY == widthMode) {
widthRatio = (float) (widthSize - getPaddingLeft() - getPaddingRight()) / mTempBitmap.getWidth();
}
if (MeasureSpec.EXACTLY == widthMode) {
heightRatio = (float) (heightSize - getPaddingTop() - getPaddingBottom()) / mTempBitmap.getHeight();
}
//初始化onDrawa()需要的参数,后续会介绍
initDraw(mTempBitmap, widthRatio, heightRatio);
}
setMeasuredDimension(widthSize, heightSize);
}
上述代码中在2个测量模式下都对padding参数进行了计算,而initDraw()方法主要是对绘画的参数做初始化。
/**
* 初始化Draw所需数据
*
* @param tempBitmap
* @param widthRatio
* @param heightRatio
*/
private void initDraw(Bitmap tempBitmap, float widthRatio, float heightRatio) {
mOriginalBitmap = scaleBitmap(tempBitmap, widthRatio, heightRatio);
initData();
if (null == mPaint)
initPaint();
initCanvas();
mIsMeasured = true;
}
/**
* 初始化绘画曲线和左边所需的一些变量值
*/
private void initData() {
mOriginalBitmapWidth = mOriginalBitmap.getWidth();
mOriginalBitmapHeight = mOriginalBitmap.getHeight();
mWaveY = mOriginalBitmapHeight;
mBezierDiffX = INCREATE_WIDTH;
mWaveLowestY = 1.4f * mOriginalBitmapHeight;
increateHeight = mOriginalBitmapHeight / 100;
}
/**
* 初始化画笔
*/
private void initPaint() {
mPaint = new Paint();
mBezierPath = new Path();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
}
/**
* 初始化画布讲2个图层绘画至mCombinedBitmap
*/
private void initCanvas() {
mTempCanvas = new Canvas();
//根据原图缩放处理结果创建一个等大的临时画布
mCombinedBitmap = Bitmap.createBitmap(mOriginalBitmapWidth + getPaddingLeft() + getPaddingRight(),
mOriginalBitmapHeight + getPaddingTop() + getPaddingBottom(), Bitmap.Config.ARGB_8888);
//将临时画布上的绘画画在mCombinedBitmap上
mTempCanvas.setBitmap(mCombinedBitmap);
}
这个初始化的操作分为3部分分别对应initPaint()、initData()、initCanvas()三个函数。
initData()主要是用于后续绘制水波纹图层时候的坐标点计算。
initPaint()就是对画笔的初始化,这个比较容易理解。
initCanvas()中根据处理后的图片大小创建一个等大的临时画布,并绘画集到mCombinedBitmap(合成的最终Bitmap)中。
接下来需要绘画缩放后的原图和绘画水波纹图层。
/**
* 合成bitmap
*/
private void combinedBitMap() {
mCombinedBitmap.eraseColor(Color.parseColor("#00ffffff"));
mTempCanvas.drawBitmap(mOriginalBitmap, 0, 0, mPaint);
//取两层交集显示在上层
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//绘制水波纹图层
drawWaveBitmap();
}
上述的代码绘制了图片图层,在绘制水波纹的图层时设置了
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
该模式只取2个图层的交集,所以水波纹图层只会显示在图片图层的非空白处,就会做出水波纹在图片内部的视觉感觉。
下一步就是绘制水波纹图层。把水波纹图层分为如下图所示的“水纹区域”和“静水区域”2部分。如下图;
绘制水波纹图层主要在于绘制曲线。可以结合下面的图片便于理解。
/**
* 计算path,绘画水波纹图层
*/
private void drawWaveBitmap() {
mBezierPath.reset();
if (mIsXDiffIncrease) {
mBezierDiffX += INCREATE_WIDTH;
} else {
mBezierDiffX -= INCREATE_WIDTH;
}
checkIncrease(mBezierDiffX);
if (mWaveY >= 0) {
mWaveY -= increateHeight;
mWaveLowestY -= increateHeight;
} else {
//还原坐标
mWaveY = mOriginalBitmapHeight;
mWaveLowestY = 1.2f * mOriginalBitmapHeight;
}
//曲线路径
mBezierPath.moveTo(0, mWaveY);
mBezierPath.cubicTo(
mBezierDiffX, mWaveY - (mWaveLowestY - mWaveY),
mBezierDiffX + mOriginalBitmapWidth / 2, mWaveLowestY,
mOriginalBitmapWidth, mWaveY);
//竖直线
mBezierPath.lineTo(mOriginalBitmapWidth, mOriginalBitmapHeight);
//横直线
mBezierPath.lineTo(0, mOriginalBitmapHeight);
mBezierPath.close();
mTempCanvas.drawPath(mBezierPath, mPaint);
mPaint.setXfermode(null);
}
在曲线绘制过程.png中,取A、B、C、D四点作为曲线的绘制参考点。A、D两点坐标比较好确认。A点X坐标恒等于0,B点的X坐标值就为图片的宽度mOriginalBitmapWidth,两点的Y坐标的值都是静水区域的上边缘线的Y值mWaveY。所以A、B坐标分别(0, mWaveY)和(mOriginalBitmapWidth, mWaveY)。
B、C两点的坐标没有固定的计算方法,这里介绍下我的计算方法:
定义C点的Y值为mWaveLowestY,mWaveLowestY和mWaveY按照相同的增长数值变化,这样就让C点距离AD线段的距离就不变,为了计算方便也让B点到AD线段的距离等于这个数值。至于X坐标值这里假定让B、C两点分别在(10,1/2 AD),(10+1/2 AD,AD)区间内变化。
private void checkIncrease(float mBezierDiffX) {
if (mIsXDiffIncrease) {
mIsXDiffIncrease = mBezierDiffX > 0.5 * mOriginalBitmapWidth ? !mIsXDiffIncrease : mIsXDiffIncrease;
} else {
mIsXDiffIncrease = mBezierDiffX < 10 ? !mIsXDiffIncrease : mIsXDiffIncrease;
}
}
if (mIsXDiffIncrease) {
//INCREATE_WIDTH是每次增涨的固定值
mBezierDiffX += INCREATE_WIDTH;
} else {
mBezierDiffX -= INCREATE_WIDTH;
}
每次重新draw的时候,mWaveY的值会变化,这样曲线就可以随着mWaveY而上下浮动,而曲线上的B、C两点的X坐标发生变化,就能实现自身的水纹波动。画完曲线后在D点沿竖直方向画一条直线到最底部,再画一条横直线到最左部,设置path.close()便能形成一个闭环。填充效果就如上图曲线绘制过程.png中的填充图所示。这样水波纹图层就完成了。单独效果图如下:
最后画在Canvas上并设置invalidate();就OK了。View之后会重新draw。
@Override
protected void onDraw(Canvas canvas) {
if (mCombinedBitmap == null) {
return;
}
combinedBitMap();
//从左上角开始绘图(需要计算padding值)
canvas.drawBitmap(mCombinedBitmap, getPaddingLeft(), getPaddingTop(), null);
if (!mStopInvalidate)
//重绘
invalidate();
}
mStopInvalidate是停止重绘的flag,后续设置自定义属性会用到。
设置自定义属性
自定义属性这里实现了设置来源图片,设置水波纹颜色以及停止水波纹的方法。
/**
* 设置原始图片资源
*
* @param resId
*/
public void setOriginalImage(@DrawableRes int resId) {
mTempBitmap = BitmapFactory.decodeResource(getResources(), resId);
mIsMeasured = false;
requestLayout();
}
/**
* 设置最终生成图片的填充颜色资源
*
* @param color
*/
public void setWaveColor(@ColorInt int color) {
if (null == mPaint)
initPaint();
mPaint.setColor(color);
}
/**
* 停止/开启 重绘
*
* @param mStopInvalidate
*/
public void setmStopInvalidate(boolean mStopInvalidate) {
this.mStopInvalidate = mStopInvalidate;
if (!mStopInvalidate)
invalidate();
}