自定义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>