自定义View之仿慕课学院水波纹进度框

场景

最近重新学了下自定义View打算仿造一下慕课学院的下拉刷新的水波纹进度框。先上效果图:

change.gif

实现思路

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个图层的交集,所以水波纹图层只会显示在图片图层的非空白处,就会做出水波纹在图片内部的视觉感觉。

src_in.png

下一步就是绘制水波纹图层。把水波纹图层分为如下图所示的“水纹区域”和“静水区域”2部分。如下图;

水面图层分布.png

绘制水波纹图层主要在于绘制曲线。可以结合下面的图片便于理解。

曲线绘制过程.png
/**
 * 计算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中的填充图所示。这样水波纹图层就完成了。单独效果图如下:

wave.gif

最后画在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();
}

最终效果

演示.gif

源码下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容