1.效果
换了大王之后,顺带就下拉联通手机营业厅App,刚开始用,出于好奇每天都会看看今天自己通过免流用了多少流量。
查看流量的入口长这样如下图
由于流量为0,不显示水波纹效果,下面是我实现的水波纹效果,这样的效果在很多App中都用到。
预告:
实现上面的效果将使用到以下知识点
- 1 xfermode(用的比较少)
- 2 贝塞尔曲线(挺常用的,Path中提供了相关Api)
- 3 属性动画(太常用了,这里不多说了)
2.分步实现
2.1分析
2.1.1 定义属性
如果从自定义的view的角度来时实现,那么首先考虑的是这个view可改变的属性是什么,比如边框的颜色、水波峰高度值、移动的快慢等。在布局文件中通过配置这些属性,让view呈现不同的效果,如下图。
<declare-styleable name="XFPView">
<!--水波纹颜色-->
<attr name="backgroundColorWave" format="color"/>
<!--圆心填充颜色-->
<attr name="backgroundColorRound" format="color"/>
<!--圆的边框颜色-->
<attr name="backgroundColorRoundBoder" format="color"/>
<!--波峰突出值-->
<attr name="waveDy" format="integer"/>
<!--最大完成值-->
<attr name="percentMax" format="float"/>
<!--动画移动时长(毫秒,建议大于1000,小于5000)-->
<attr name="durationAnim" format="integer"/>
</declare-styleable>
定义好了属性,这样用起来方便,不用修改代码,只通过改变xml中的属性值来就可以改变view的相关状态(让你同事去修改你的代码他会生气的),接下按自定义view的步骤应该要继承View,重写onMeasure方法、最后是onDraw方法(套路)。这个过程看似简单,其实考虑的细节很多(纸上得来终觉浅)。
那么就来吧!!!
2.1.2 自定义XfPathView
先写一个类继承view,直接上代码。
/**
* Created by Zhoudesen
* Created time 2018/1/29 16:20
* Description: Xfermode + Path之贝塞尔曲线 应用
* Version: V 1.0
*/
public class XfPathView extends View {
/**
* 圆形画笔
*/
private Paint mPaintRound;
/**
* 水波纹画笔
*/
private Paint mPaintWave;
/**
* 最小宽度
*/
private final static int MIN_WIDTH = 300;
/**
* 最小高度
*/
private final static int MIN_HEIGHT = 300;
/**
* 水波纹 背景颜色
*/
private int mColorBgWave;
/**
* 水波纹 默认颜色
*/
private final static int COLOR_BG_WAVE_DEFUALT = 0xaa00ff00;
/**
* 圆形boder颜色
*/
private int mColorBgRoundBoder;
/**
* 圆形背景颜色
*/
private int mColorBgRound;
/**
* 圆形 默认颜色 完全透明色
*/
private final static int COLOR_BG_ROUND_DEFUALT = 0x00000000;
/**
* y 轴百分比
*/
private float mPercentY = 0f;
/**
* 最大完成值
*/
private float mPercentMax;
/**
* 1/2 波峰x轴长度(每个波峰/波谷均为贝塞尔曲线的控制点)
*/
private float mRadius;
/**
* x 轴偏移量(让水波纹动起来)
*/
private float mDx = 0;
/**
* y 轴波峰突出值(波谷凹陷值)
*/
private int mDy = 0;
/**
* view高宽值(高=宽)
*/
private int mViewWidthHeight;
private Path mPath;
private Bitmap mBitmap;
private ValueAnimator mAnimator;
private PorterDuffXfermode mPorterDuffXfermode;
private RectF mRectf;
public XfPathView(Context context) {
this(context, null);
}
public XfPathView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public XfPathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* 初始化参数
*
* @param context
* @param attrs
*/
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XFPView);
mColorBgRound = typedArray.getColor(R.styleable.XFPView_backgroundColorRound, COLOR_BG_ROUND_DEFUALT);
mColorBgRoundBoder = typedArray.getColor(R.styleable.XFPView_backgroundColorRoundBoder, COLOR_BG_WAVE_DEFUALT);
mColorBgWave = typedArray.getColor(R.styleable.XFPView_backgroundColorWave, COLOR_BG_WAVE_DEFUALT);
mPercentMax = typedArray.getFloat(R.styleable.XFPView_percentMax, 100);
mDy = typedArray.getInteger(R.styleable.XFPView_waveDy, 20);
typedArray.recycle();
}
mPath = new Path();
//画笔初始化
mPaintRound = new Paint();
mPaintRound.setAntiAlias(true);
mPaintRound.setStyle(Paint.Style.STROKE);
mPaintRound.setStrokeWidth(3);
mPaintRound.setColor(mColorBgRoundBoder);
mPaintWave = new Paint();
mPaintWave.setAntiAlias(true);
mPaintWave.setColor(mColorBgWave);
mPaintWave.setStyle(Paint.Style.FILL);
mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
mRectf = new RectF();
}
以上代码初始化了画笔,颜色等相关基础工作,其中倒数第二行PorterDuffXfermode,才是主角,把主角放到最后登场(这里有坑)。
接下来开始对view进行测量,这个view高度和宽度是相等的,如果高度和宽度不相等,那么取小的值设置为它的高宽。视乎也很简单,就直接看下面代码吧。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = 0;
int width = 0;
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
width = MeasureSpec.getSize(widthMeasureSpec);
}
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
height = MeasureSpec.getSize(heightMeasureSpec);
}
//取最小值为view的高宽,让“高”=“宽”
width = Math.min(width, height);
if ( width > MIN_WIDTH) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(MIN_WIDTH, MIN_HEIGHT);
}
}
到这里,已经完成了自定义相关属性,获取了属性值,并且完成初始化准备,还重写了onMeasure测量了高宽。接下来就是onDraw,在onDraw之前要分析先画什么,后画什么,还要计算相关的坐标,所以先分析吧。
先从水波纹的实现说起(尽管大家都知道,但还是要啰嗦一下 >温故而知新嘛)
2.1.3水波纹的实现
画一条线或多边形(或闭合曲线)总得有一个起点吧。
【起点】
Path.moveTo(x,y)
有了点就可以画线了,就先看下面点曲线是怎么实现的【画曲线】(就是贝塞尔曲线)
Path.quadTo(x1,y1,x2,y2);
quadTo(x1,y1,x2,y2)这个函数传入两个坐标,第一个是控制点,第二个是这段曲线的终点。
这个实际上就是二阶贝塞尔曲线(还有三阶、四阶、用的不多) 。
quadTo是核心方法,各种曲线效果都是由第一个控制点的变化而变化的。当然它是有一个公式的,但是Path Api中封装好了,只管用就好(你所关注的就是调方法,传坐标参数)。
【链接到某一个点】(直线)
Path.lineTo(x,y) ;【首尾相连】构成一个闭合的路径(直线)
Path.close();
下图是一个带有水波纹的闭合图形,看看它是怎么实现的
解释一下上图吧
moveTo(点0) 起始点
quadTo(点1,点2);
quadTo(点3,点4);
quadTo(点5,点6);
quadTo(点7,点8);
没错,水波纹就是有一段段贝塞尔曲线拼接而来的。
lineTo(点9)
lineTo(点10)
close()将首尾相连(图中红色线部分)
通过Path的四个方法:moveTo、quadTo、lineTo和close。已经能满足画任意闭合的图形了,当然Path远不止这些,下回分解。
下图:实现了略带水波纹的闭合图形
2.1.4 onDraw
我们最终要实现的效果是这样的
可以拆分成两部分来画
1、在onDraw中可以先画一个外边圆的白色边框boder。
2、在画里面的动态水波纹。
水波纹的实现上述已经介绍过了,画一个圆边框视乎也毫不费劲,上代码。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//--画圆boder
drawBoder(canvas);
//--画水波纹
drawWave(canvas);
}
private void drawBoder(Canvas canvas){
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mViewWidthHeight / 2 - 1, mPaintRound);
}
drawWave(canvas)的代码也很简单,
private void drawWave(Canvas canvas){
mPath.reset();
//起点
mPath.moveTo(0 - mDx, mPercentY * mViewWidthHeight);
//四段曲线
for (int i = 0; i < 4; i++) {
mDy = -mDy;
mPath.quadTo((1 + 2 * i) * mRadius - mDx, mPercentY * mViewWidthHeight + mDy,
(1 + i) * 2 * mRadius - mDx, mPercentY * mViewWidthHeight);
}
//连接到view的右边底部
mPath.lineTo(2 * mViewWidthHeight - mDx, mViewWidthHeight);
//连接到view的左边底部
mPath.lineTo(0 - mDx, mViewWidthHeight);
//闭合
mPath.close();
canvas.drawPath(mPath, mPaintWave);
}
mDx是X轴上的偏移量,通过改变mDx,就可以让水波纹动起来。
mDy是施加给控制点的Y轴坐标的,其中mDy = - mDy (取反)是为了实现每段曲线上下波动效果的。
为了突出效果,画的是一个填充的圆
实际效果如下图
你发现了什么?
箭头所指的两块白色区域并不是我们想要的,随着水波纹的上升,圆的上部分也将会突出,这么解决这个问题,话不多说,让主角xfermode登场。
2.1.5 Xfermode
它是干嘛用的?
答:通过使用Xfermode将绘制的图形的像素和Canvas上对应位置的像素按照一定的规则进行混合,形成新的像素,再更新到Canvas中形成最终的图形它怎么用?
答:通过Paint.setXfermode代码片段
关于这段代码片段有几点记录一下:
- saveLayer 会产生一个新的图层,之后的画操作都是在这个图层上进行
- restoreToCount(int layer),将指定的图层绘制到cavas.
更进一步
实际上Xfermode有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,前两个类在API 16被遗弃了,PorterDuffXfermode才是我们需要关注的。在初始化的时候用的就是这个子类。
看看它是真么实例化的:
mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
PorterDuff.Mode,这个是什么?SRC_IN代表什么?
看源码:
// these value must match their native equivalents. See SkXfermode.h
public enum Mode {
/** [0, 0] */
CLEAR (0),
/** [Sa, Sc] */
SRC (1),
/** [Da, Dc] */
DST (2),
/** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
SRC_OVER (3),
/** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
DST_OVER (4),
/** [Sa * Da, Sc * Da] */
SRC_IN (5),
/** [Sa * Da, Sa * Dc] */
DST_IN (6),
/** [Sa * (1 - Da), Sc * (1 - Da)] */
SRC_OUT (7),
/** [Da * (1 - Sa), Dc * (1 - Sa)] */
DST_OUT (8),
/** [Da, Sc * Da + (1 - Sa) * Dc] */
SRC_ATOP (9),
/** [Sa, Sa * Dc + Sc * (1 - Da)] */
DST_ATOP (10),
/** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
XOR (11),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
DARKEN (16),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
LIGHTEN (17),
/** [Sa * Da, Sc * Dc] */
MULTIPLY (13),
/** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
SCREEN (14),
/** Saturate(S + D) */
ADD (12),
OVERLAY (15);
Mode(int nativeInt) {
this.nativeInt = nativeInt;
}
/**
* @hide
*/
public final int nativeInt;
}
PorterDuff.Mode它是一个枚举类型,而SRC_IN只是其中一个模式。
看一张图官方给的参考图
图中已经列出了两张图(Src和Dst)对应不同Mode所呈现的效果,参考这些模式对应的混合效果,实现的效果也太丰富了。是时候开始改写onDraw方法了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//--画圆boder
drawBoder(canvas);
//--saveLayer
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
//--画水波纹
drawWave(canvas);
canvas.restoreToCount(layerId);
}
根据之前的分析混合绘制要在一张新的图层进行所以执行saveLayer方法,绘制完成后restoreToCount方法将混合后的图层绘制到canvas上。
看看drawWave(canvas)中是如何绘制的
private void drawWave(Canvas canvas){
//--画圆心
mPaintWave.setColor(mColorBgRound);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, (mViewWidthHeight / 2) - 3, mPaintWave);
//--画水波纹
mPath.reset();
mPath.moveTo(0 - mDx, mPercentY * mViewWidthHeight);
//四个点
for (int i = 0; i < 4; i++) {
mDy = -mDy;
mPath.quadTo((1 + 2 * i) * mRadius - mDx, mPercentY * mViewWidthHeight + mDy,
(1 + i) * 2 * mRadius - mDx, mPercentY * mViewWidthHeight);
}
//连接到右边view底部
mPath.lineTo(2 * mViewWidthHeight - mDx, mViewWidthHeight);
//连接到左边底部
mPath.lineTo(0 - mDx, mViewWidthHeight);
//闭合
mPath.close();
//--圆与水波纹fix
mPaintWave.setColor(mColorBgWave);
mPaintWave.setXfermode(mPorterDuffXfermode);
canvas.drawPath(mPath, mPaintWave);
mPaintWave.setXfermode(null);
}
画一个圆与水波纹进行混合,去除超出圆的部分。
混合前
Srcin这个模式符合我们的需求
看效果图
哇,水波纹底部已经实现了我们的效果,but,上部分是什么鬼(这是混合圆的颜色)。需求可是要透明的啊,好那么就把这个圆改成透明色把,我们已经在xml中定义了颜色属性,很容易改,然后运行得到如下结果。
圆是透明了,水波纹也不见了。看来是想的太简单了
因为透明度会影响混合效果,完全透明那么混合够也透明了
那么圆不能完全透明,如果这个需求不要背景完全透明,那么也就完事了,下图Srctop模式的效果
好吧,接着又找了很几种相像的模式试了一遍,就是不行。
机智时刻:既然圆不能完全透明,和水波纹混合后上部的底色又得透明,那么可以通过一个完全透明矩形和上部分底色混合,就可以消除了。如下图
看最终效果
//--用完全透明的矩形消除圆的上部分
mPaintWave.setColor(COLOR_BG_ROUND_DEFUALT);
mRectf.bottom = mViewWidthHeight * mPercentY - mDy;
完整代码:
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;
import itsen.com.bduidemo.R;
import itsen.com.bduidemo.lib.tool.LogTool;
/**
* Created by Zhoudesen
* Created time 2018/1/29 16:20
* Description: Xfermode Path之贝塞尔曲线 应用
* Version: V 1.0
*/
public class XfPathView extends View {
/**
* 圆形画笔
*/
private Paint mPaintRound;
/**
* 水波纹画笔
*/
private Paint mPaintWave;
/**
* 最小宽度
*/
private final static int MIN_WIDTH = 300;
/**
* 最小高度
*/
private final static int MIN_HEIGHT = 300;
/**
* 水波纹 背景颜色
*/
private int mColorBgWave;
/**
* 水波纹 默认颜色
*/
private final static int COLOR_BG_WAVE_DEFUALT = 0xaa00ff00;
/**
* 圆形boder颜色
*/
private int mColorBgRoundBoder;
/**
* 圆形背景颜色
*/
private int mColorBgRound;
/**
* 圆形 默认颜色 完全透明色
*/
private final static int COLOR_BG_ROUND_DEFUALT = 0x00000000;
/**
* y 轴百分比
*/
private float mPercentY = 0f;
/**
* 最大完成值
*/
private float mPercentMax;
/**
* 1/2 波峰x轴长度(每个波峰/波谷均为贝塞尔曲线的控制点)
*/
private float mRadius;
/**
* x 轴偏移量(让水波纹动起来)
*/
private float mDx = 0;
/**
* y 轴波峰突出值(波谷凹陷值)
*/
private int mDy = 0;
/**
* view高宽值(高=宽)
*/
private int mViewWidthHeight;
private Path mPath;
private Bitmap mBitmap;
private ValueAnimator mAnimator;
private PorterDuffXfermode mPorterDuffXfermode;
private RectF mRectf;
public XfPathView(Context context) {
this(context, null);
}
public XfPathView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public XfPathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* 初始化参数
*
* @param context
* @param attrs
*/
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XFPView);
mColorBgRound = typedArray.getColor(R.styleable.XFPView_backgroundColorRound, COLOR_BG_ROUND_DEFUALT);
mColorBgRoundBoder = typedArray.getColor(R.styleable.XFPView_backgroundColorRoundBoder, COLOR_BG_WAVE_DEFUALT);
mColorBgWave = typedArray.getColor(R.styleable.XFPView_backgroundColorWave, COLOR_BG_WAVE_DEFUALT);
mPercentMax = typedArray.getFloat(R.styleable.XFPView_percentMax, 100);
mDy = typedArray.getInteger(R.styleable.XFPView_waveDy, 20);
typedArray.recycle();
}
mPath = new Path();
//画笔初始化
mPaintRound = new Paint();
mPaintRound.setAntiAlias(true);
mPaintRound.setStyle(Paint.Style.STROKE);
mPaintRound.setStrokeWidth(3);
mPaintRound.setColor(mColorBgRoundBoder);
mPaintWave = new Paint();
mPaintWave.setAntiAlias(true);
mPaintWave.setColor(mColorBgWave);
mPaintWave.setStyle(Paint.Style.FILL);
mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
mRectf = new RectF(0,0,mViewWidthHeight,0);
setmPercentY(10);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = 0;
int width = 0;
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
width = MeasureSpec.getSize(widthMeasureSpec);
}
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
height = MeasureSpec.getSize(heightMeasureSpec);
}
//取最小值为view的高宽,让“高”=“宽”
width = Math.min(width, height);
if (width > 0 && width > MIN_WIDTH) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(MIN_WIDTH, MIN_HEIGHT);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidthHeight = w;
//二分之一的波峰波谷
mRadius = mViewWidthHeight / 4;
if (mBitmap == null) {
mBitmap = Bitmap.createBitmap(mViewWidthHeight, mViewWidthHeight, Bitmap.Config.ARGB_8888);
}
startAinm();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//--画圆boder
drawBoder(canvas);
//--saveLayer
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
//--画水波纹
drawWave(canvas);
canvas.restoreToCount(layerId);
}
private void drawBoder(Canvas canvas){
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mViewWidthHeight / 2 - 1, mPaintRound);
}
private void drawWave(Canvas canvas){
//--画圆心
mPaintWave.setColor(mColorBgRound);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, (mViewWidthHeight / 2) - 3, mPaintWave);
//--画水波纹
mPath.reset();
mPath.moveTo(0 - mDx, mPercentY * mViewWidthHeight);
//四个点
for (int i = 0; i < 4; i++) {
mDy = -mDy;
mPath.quadTo((1 + 2 * i) * mRadius - mDx, mPercentY * mViewWidthHeight + mDy,
(1 + i) * 2 * mRadius - mDx, mPercentY * mViewWidthHeight);
}
//连接到右边view底部
mPath.lineTo(2 * mViewWidthHeight - mDx, mViewWidthHeight);
//连接到左边底部
mPath.lineTo(0 - mDx, mViewWidthHeight);
//闭合
mPath.close();
//--圆与水波纹fix
mPaintWave.setColor(mColorBgWave);
mPaintWave.setXfermode(mPorterDuffXfermode);
canvas.drawPath(mPath, mPaintWave);
//--用完全透明的矩形消除圆的上部分
mPaintWave.setColor(COLOR_BG_ROUND_DEFUALT);
mRectf.right = mViewWidthHeight;
mRectf.bottom = mViewWidthHeight * mPercentY - mDy;
canvas.drawRect(mRectf, mPaintWave);
mPaintWave.setXfermode(null);
}
/**
* 初始化并开始动画
*/
public void startAinm() {
mAnimator = ValueAnimator.ofFloat(0, mViewWidthHeight);
mAnimator.setDuration(2000);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mDx = (float) animation.getAnimatedValue();
postInvalidate();
}
});
mAnimator.start();
}
/**
* 暂停
*/
public void stopAnim() {
if (mAnimator.isStarted()) {
mAnimator.pause();
}
}
/**
* 重启
*/
public void reStartAinm() {
if (mAnimator != null && mAnimator.isPaused()) {
mAnimator.start();
}
}
/**
* 设置完成度
*
* @param value
*/
public void setmPercentY(float value) {
this.mPercentY = 1 - (value / mPercentMax < 1 ? value / mPercentMax : 1);
}
}
水平有限,疏漏之处请批评指正
写作不易、喜欢的给个星,谢谢!