自定义View之仿慕课学院水波纹进度框
GertrudeAvi
8年前
<p><strong>场景</strong></p> <p>最近重新学了下自定义View打算仿造一下慕课学院的下拉刷新的水波纹进度框。先上效果图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/15a5906f030b4163bcbf2197c957d8ac.gif"></p> <p style="text-align:center">change.gif</p> <p><strong>实现思路</strong></p> <p>1.加入图片,并根据控件大小处理图片大小</p> <p>2.在临时画布上绘画图片图层和水波纹图层,并合并成图片。</p> <p>3.在画布上绘制合成的图片并调用invalidate();方法去重新计算绘制水波纹图层;</p> <p>首先让我们的控件去继承View,定义一些常量和自定义View的初始化:</p> <pre> <code class="language-java">/** * 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;</code></pre> <p>关于图片的大小,这里我希望在MeasureSpec.AT_MOST的时候让控件保持和图片大小一致,在MeasureSpec.EXACTLY模式下让图片大小跟随控件大小而改变,两种模式下都需考虑padding情况。</p> <p>先写一个处理图片缩放的方法:</p> <pre> <code class="language-java">/** * 按比例缩放图片 * * @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; }</code></pre> <p>在View的onMeasure()方法中,根据测量模式的不同分别处理图片,而处理图片的步骤只需要执行一次,为避免onMeasure()方法多次调用而造成资源浪费,引入一个flag变量mIsMeasured来规避这个问题。</p> <pre> <code class="language-java">@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); }</code></pre> <p>上述代码中在2个测量模式下都对padding参数进行了计算,而initDraw()方法主要是对绘画的参数做初始化。</p> <pre> <code class="language-java">/** * 初始化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); }</code></pre> <p>这个初始化的操作分为3部分分别对应initPaint()、initData()、initCanvas()三个函数。</p> <p>initData()主要是用于后续绘制水波纹图层时候的坐标点计算。</p> <p>initPaint()就是对画笔的初始化,这个比较容易理解。</p> <p>initCanvas()中根据处理后的图片大小创建一个等大的临时画布,并绘画集到mCombinedBitmap(合成的最终Bitmap)中。</p> <p>接下来需要绘画缩放后的原图和绘画水波纹图层。</p> <pre> <code class="language-java">/** * 合成bitmap */ private void combinedBitMap() { mCombinedBitmap.eraseColor(Color.parseColor("#00ffffff")); mTempCanvas.drawBitmap(mOriginalBitmap, 0, 0, mPaint); //取两层交集显示在上层 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //绘制水波纹图层 drawWaveBitmap(); }</code></pre> <p>上述的代码绘制了图片图层,在绘制水波纹的图层时设置了</p> <p>mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));</p> <p>该模式只取2个图层的交集,所以水波纹图层只会显示在图片图层的非空白处,就会做出水波纹在图片内部的视觉感觉。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/4453c430c5b5526457a4291dbd17d579.png"></p> <p style="text-align:center">src_in.png</p> <p>下一步就是绘制水波纹图层。把水波纹图层分为如下图所示的“水纹区域”和“静水区域”2部分。如下图;</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/97cd1faf238d1c4c76f631fb44a52796.png"></p> <p style="text-align:center">水面图层分布.png</p> <p>绘制水波纹图层主要在于绘制曲线。可以结合下面的图片便于理解。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/3c543d38b2e4642cdf155a6c97e7e81c.png"></p> <p style="text-align:center">曲线绘制过程.png</p> <pre> <code class="language-java">/** * 计算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); }</code></pre> <p>在曲线绘制过程.png中,取A、B、C、D四点作为曲线的绘制参考点。A、D两点坐标比较好确认。A点X坐标恒等于0,B点的X坐标值就为图片的宽度mOriginalBitmapWidth,两点的Y坐标的值都是静水区域的上边缘线的Y值mWaveY。所以A、B坐标分别(0, mWaveY)和(mOriginalBitmapWidth, mWaveY)。</p> <p>B、C两点的坐标没有固定的计算方法,这里介绍下我的计算方法:</p> <p>定义C点的Y值为mWaveLowestY,mWaveLowestY和mWaveY按照相同的增长数值变化,这样就让C点距离AD线段的距离就不变,为了计算方便也让B点到AD线段的距离等于这个数值。至于X坐标值这里假定让B、C两点分别在(10,1/2 AD),(10+1/2 AD,AD)区间内变化。</p> <pre> <code class="language-java">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; }</code></pre> <p>每次重新draw的时候,mWaveY的值会变化,这样曲线就可以随着mWaveY而上下浮动,而曲线上的B、C两点的X坐标发生变化,就能实现自身的水纹波动。画完曲线后在D点沿竖直方向画一条直线到最底部,再画一条横直线到最左部,设置path.close()便能形成一个闭环。填充效果就如上图曲线绘制过程.png中的填充图所示。这样水波纹图层就完成了。单独效果图如下:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/3c011de471cd64950c83fc5cdfdae056.gif"></p> <p style="text-align:center">wave.gif</p> <p>最后画在Canvas上并设置invalidate();就OK了。View之后会重新draw。</p> <pre> <code class="language-java">@Override protected void onDraw(Canvas canvas) { if (mCombinedBitmap == null) { return; } combinedBitMap(); //从左上角开始绘图(需要计算padding值) canvas.drawBitmap(mCombinedBitmap, getPaddingLeft(), getPaddingTop(), null); if (!mStopInvalidate) //重绘 invalidate(); }</code></pre> <p>mStopInvalidate是停止重绘的flag,后续设置自定义属性会用到。</p> <p><strong>设置自定义属性</strong></p> <p>自定义属性这里实现了设置来源图片,设置水波纹颜色以及停止水波纹的方法。</p> <pre> <code class="language-java">/** * 设置原始图片资源 * * @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(); }</code></pre> <p><strong>最终效果</strong></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d7b46e6f941315a7ffbca2c521abd99b.gif"></p> <p style="text-align:center">演示.gif</p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/0f88c34cce8f</p> <p> </p>