Android 水波紋顯示進度效果
關於水波紋的效果想必大家見的已經很多了,我就在這裡再囉嗦一次,為了加深自己的印象。先來看看效果圖
關於這個效果的實現不必想的太過複雜了,要想實現這個效果,我們還需要了解一下PorterDuff及Xfermode
關於上面的這張圖想必大家也見過很多次了。這其實就是PorterDuff的16種模式。效果想比大家已經見到了,下面我們就來一一瞭解如何使用。
PorterDuff.Mode.CLEAR (所繪製不會提交到畫布上)
PorterDuff.Mode.SRC(顯示上層繪製圖片)
PorterDuff.Mode.DST(顯示下層繪製圖片)
PorterDuff.Mode.SRC_OVER(正常繪製顯示,上下層繪製疊蓋)
PorterDuff.Mode.DST_OVER(上下層都顯示。下層居上顯示)
PorterDuff.Mode.SRC_IN(取兩層繪製交集。顯示上層)
PorterDuff.Mode.DST_IN(取兩層繪製交集。顯示下層)
PorterDuff.Mode.SRC_OUT( 取上層繪製非交集部分)
PorterDuff.Mode.DST_OUT(取下層繪製非交集部分)
PorterDuff.Mode.SRC_ATOP(取下層非交集部分與上層交集部分)
PorterDuff.Mode.DST_ATOP(取上層非交集部分與下層交集部分)
PorterDuff.Mode.XOR( 異或:去除兩圖層交集部分)
PorterDuff.Mode.DARKEN( 取兩圖層全部區域,交集部分顏色加深)
PorterDuff.Mode.LIGHTEN(取兩圖層全部,點亮交集部分顏色)
PorterDuff.Mode.MULTIPLY(取兩圖層交集部分疊加後顏色)
PorterDuff.Mode.SCREEN( 取兩圖層全部區域,交集部分變為透明色)
Xfermode有三個子類 :
AvoidXfermode 指定了一個顏色和容差,強制Paint避免在它上面繪圖(或者只在它上面繪圖)。
PixelXorXfermode 當覆蓋已有的顏色時,應用一個簡單的畫素異或操作。
PorterDuffXfermode 這是一個非常強大的轉換模式,使用它可以使用影象合成的上圖中的任意一種來控制Paint如何與已有的Canvas影象進行互動。要應用轉換模式,可以使用setXferMode方法
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));
這些只能夠顯示一些合成的效果也就是上面的16種的任意一種效果而已,要想實現水波紋的效果還是不夠的,我們還需要藉助於貝塞爾曲線來實現水波效果。我們使用到的是Path類的quadTo(x1, y1, x2, y2)方法,屬於二階貝塞爾曲線,使用一張圖來展示二階貝塞爾曲線,這裡的(x1,y1)是控制點,(x2,y2)是終止點,起始點預設是Path的起始點(0,0)。關於使用貝塞爾曲線來實現水波效果的原理就是:通過for迴圈畫出兩個波紋,我們以WL代表水波紋的長度。需要波紋的-WL點、-3/4*WL點、-1/2*WL、-1/4*WL四個點,通過path的quadTo畫出,並無限重複。實現的效果其實也就是我們平時的正弦效果。那麼我們也需要了解一下path的使用。先來看一下效果圖
實現程式碼為
public class WaveView extends View {
private Paint mPaint;
private Path mPath;
private Point assistPoint;
public WaveView(Context context) {
super(context);
}
public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
super (context, attrs, defStyleAttr);
}
public WaveView(Context context, AttributeSet attrs) {
super(context, attrs);
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
assistPoint = new Point(250, 350);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.moveTo(50, 300);
mPath.quadTo(assistPoint.x, assistPoint.y, 450, 300);
canvas.drawPath(mPath, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
assistPoint.x = (int) event.getX();
assistPoint.y = (int) event.getY();
invalidate();
break;
}
return true;
}
}
先來說一下mPath.moveTo(50, 300);這個方法的作用是將起點移動到螢幕座標為(50, 300)的位置。mPath.quadTo(assistPoint.x, assistPoint.y, 450, 300);這個方法就是重點了,對應的原始碼為
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
native_quadTo(mNativePath, x1, y1, x2, y2);
}
第一個座標是對應的控制點的座標(assistPoint.x, assistPoint.y),第二個座標是終點座標也就是我們看到的水平線的終點位置座標。上圖之所以會出現移動的效果就是因為控制點的位置隨著滑鼠的位置在移動而產生的。
下面我們再來看一個效果圖
這個圖形的實現程式碼為
public class WaveViewTest extends View {
private int width;
private int height;
private Path mPath;
private Paint mPathPaint;
private int mWaveHight = 150;//水波紋的高度
private int mWaveWidth = 100;//水波紋的寬度
private String mWaveColor = "#FFFFFF";
private int maxProgress = 100;
private int currentProgress = 0;
private float currentY;
public WaveViewTest(Context context) {
this(context,null,0);
}
public WaveViewTest(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveViewTest(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public void setCurrent(int currentProgress,String currentText) {
this.currentProgress = currentProgress;
}
public void setWaveColor(String mWaveColor){
this.mWaveColor = mWaveColor;
}
private void init() {
mPath = new Path();
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
currentY = height = MeasureSpec.getSize(heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
mPathPaint.setColor(Color.parseColor(mWaveColor));
Paint paint = new Paint();
paint.setAntiAlias(true);
float currentMidY = height*(maxProgress-currentProgress)/maxProgress;
if(currentY>currentMidY){
currentY = currentY - (currentY-currentMidY)/10;
}
mPath.reset();
mPath.moveTo(0,300);
//顯示的區域內的水波紋的數量
int waveNum = width/mWaveWidth;
int num = 0;
for(int i =0;i<waveNum;i++){
mPath.quadTo(mWaveWidth*(num+1),300-mWaveHight,mWaveWidth*(num+2),300);
mPath.quadTo(mWaveWidth*(num+3),300+mWaveHight,mWaveWidth*(num+4),300);
num+=4;
}
canvas.drawPath(mPath, mPathPaint);
}
}
再來一個效果圖以及實現程式碼
public class WaveViewTest extends View {
private int width;
private int height;
private Path mPath;
private Paint mPathPaint;
private String mWaveColor = "#FF49C12E";
public WaveViewTest(Context context) {
this(context,null,0);
}
public WaveViewTest(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveViewTest(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mPath = new Path();
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
mPathPaint.setColor(Color.parseColor(mWaveColor));
mPath.reset();
mPath.moveTo(0,0);
mPath.lineTo(0,height);
mPath.lineTo(width,height);
mPath.lineTo(width,0);
mPath.close();
canvas.drawPath(mPath, mPathPaint);
}
}
上圖是我們看到的一個靜態的圖,但是距離我們實現的效果已經很接近了,實現程式碼為
public class WaveViewTest extends View {
private int width;
private int height;
private Path mPath;
private Paint mPathPaint;
private float mWaveWidth = 100f;//水波紋的寬度
private String mWaveColor = "#FF49C12E";
public WaveViewTest(Context context) {
this(context,null,0);
}
public WaveViewTest(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveViewTest(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mPath = new Path();
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(createImage(), 0, 0, null);
}
private Bitmap createImage()
{
mPathPaint.setColor(Color.parseColor(mWaveColor));
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap bmp = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
mPath.reset();
mPath.moveTo(0,300);
//顯示的區域內的水波紋的數量
int waveNum = width/((int)mWaveWidth);
int num = 0;
for(int i =0;i<waveNum;i++){
mPath.quadTo(mWaveWidth*(num+1),300-150,mWaveWidth*(num+2),300);
mPath.quadTo(mWaveWidth*(num+3),300+150,mWaveWidth*(num+4),300);
num+=4;
}
mPath.lineTo(width,height);
mPath.lineTo(0,height);
mPath.close();
canvas.drawPath(mPath, mPathPaint);
return bmp;
}
}
這個效果的產生其實就是上面的圖形通過for迴圈產生移動距離產生的,程式碼如下
public class WaveView extends View {
private int width;
private int height;
private Path mPath;
private Paint mPathPaint;
private float mWaveHight = 30f;
private float mWaveWidth = 100f;//水波紋的寬度
private String mWaveColor = "#FFFFFF";
private int mWaveSpeed = 30;
private int maxProgress = 100;
private int currentProgress = 0;
private float currentY;
private float distance = 0;
private int RefreshGap = 10;
private static final int INVALIDATE = 0X777;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case INVALIDATE:
invalidate();
sendEmptyMessageDelayed(INVALIDATE,RefreshGap);
break;
}
}
};
public WaveView(Context context) {
this(context,null,0);
}
public WaveView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public void setCurrent(int currentProgress,String currentText) {
this.currentProgress = currentProgress;
}
public void setWaveColor(String mWaveColor){
this.mWaveColor = mWaveColor;
}
private void init() {
mPath = new Path();
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setStyle(Paint.Style.FILL);
handler.sendEmptyMessageDelayed(INVALIDATE,100);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
currentY = height = MeasureSpec.getSize(heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(createImage(), 0, 0, null);
}
private Bitmap createImage()
{
mPathPaint.setColor(Color.parseColor(mWaveColor));
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap bmp = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
float currentMidY = height*(maxProgress-currentProgress)/maxProgress;
if(currentY>currentMidY){
currentY = currentY - (currentY-currentMidY)/10;
}
mPath.reset();
//之所以0-distance是因為有原點向上增加的
mPath.moveTo(0-distance,currentY);
//顯示的區域內的水波紋的數量
int waveNum = width/((int)mWaveWidth);
int num = 0;
for(int i =0;i<waveNum;i++){
mPath.quadTo(mWaveWidth*(num+1)-distance,currentY-mWaveHight,mWaveWidth*(num+2)-distance,currentY);
mPath.quadTo(mWaveWidth*(num+3)-distance,currentY+mWaveHight,mWaveWidth*(num+4)-distance,currentY);
num+=4;
}
distance +=mWaveWidth/mWaveSpeed;
distance = distance%(mWaveWidth*4);
mPath.lineTo(width,height);
mPath.lineTo(0,height);
mPath.close();
canvas.drawPath(mPath, mPathPaint);
return bmp;
}
}
通過對比程式碼你會發現,其實就是通過移動定時重新整理不停的呼叫onDraw方法,通過distance 的不斷變化而產生的動畫效果。如果想要實現我們最上面的動畫效果還需要藉助於PorterDuff及Xfermode,關於PorterDuff及Xfermode上面已經說過了。剩下的就是它們之間的配合使用了
完整的實現的程式碼如下:
自定義view
public class WaveProgressView extends View {
private int width;
private int height;
private Bitmap backgroundBitmap;
private Path mPath;
private Paint mPathPaint;
private float mWaveHight = 30f;
private float mWaveWidth = 100f;
private String mWaveColor = "#FFFFFF";
private int mWaveSpeed = 30;
private Paint mTextPaint;
private String currentText = "";
private String mTextColor = "#FFFFFF";
private int mTextSize = 35;
private int maxProgress = 100;
private int currentProgress = 0;
private float currentY;
private float distance = 0;
private int RefreshGap = 10;
private static final int INVALIDATE = 0X777;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case INVALIDATE:
invalidate();
sendEmptyMessageDelayed(INVALIDATE,RefreshGap);
break;
}
}
};
public WaveProgressView(Context context) {
this(context,null,0);
}
public WaveProgressView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaveProgressView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public void setCurrent(int currentProgress,String currentText) {
this.currentProgress = currentProgress;
this.currentText = currentText;
}
public void setWaveColor(String mWaveColor){
this.mWaveColor = mWaveColor;
}
private void init() {
if(null==getBackground()){
throw new IllegalArgumentException(String.format("background is null."));
}else{
backgroundBitmap = getBitmapFromDrawable(getBackground());
}
mPath = new Path();
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setStyle(Paint.Style.FILL);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
handler.sendEmptyMessageDelayed(INVALIDATE,100);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
currentY = height = MeasureSpec.getSize(heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
if(backgroundBitmap!=null){
canvas.drawBitmap(createImage(), 0, 0, null);
}
}
private Bitmap createImage()
{
mPathPaint.setColor(Color.parseColor(mWaveColor));
mTextPaint.setColor(Color.parseColor(mTextColor));
mTextPaint.setTextSize(mTextSize);
mPathPaint.setColor(Color.parseColor(mWaveColor));
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap bmp = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
float currentMidY = height*(maxProgress-currentProgress)/maxProgress;
if(currentY>currentMidY){
currentY = currentY - (currentY-currentMidY)/10;
}
mPath.reset();
//之所以0-distance是因為有原點向上增加的
mPath.moveTo(0-distance,currentY);
//顯示的區域內的水波紋的數量
int waveNum = width/((int)mWaveWidth);
int num = 0;
for(int i =0;i<waveNum;i++){
mPath.quadTo(mWaveWidth*(num+1)-distance,currentY-mWaveHight,mWaveWidth*(num+2)-distance,currentY);
mPath.quadTo(mWaveWidth*(num+3)-distance,currentY+mWaveHight,mWaveWidth*(num+4)-distance,currentY);
num+=4;
}
distance +=mWaveWidth/mWaveSpeed;
distance = distance%(mWaveWidth*4);
mPath.lineTo(width,height);
mPath.lineTo(0,height);
mPath.close();
canvas.drawPath(mPath, mPathPaint);
int min = Math.min(width,height);
backgroundBitmap = Bitmap.createScaledBitmap(backgroundBitmap,min,min,false);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));
canvas.drawBitmap(backgroundBitmap,0,0,paint);
canvas.drawText(currentText, width/2, height/2, mTextPaint);
return bmp;
}
private Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
try {
Bitmap bitmap;
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (OutOfMemoryError e) {
return null;
}
}
}
MainActivity.class
public class MainActivity extends Activity {
private WaveProgressView wpv;
private static final int FLAG_ONE = 0X0001;
private int max_progress = 100;
private int progress;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
progress++;
switch (msg.what) {
case FLAG_ONE:
if (progress <= max_progress){
wpv.setCurrent(progress, progress + "%");
sendEmptyMessageDelayed(FLAG_ONE, 100);
}else {
return;
}
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
wpv = (WaveProgressView) findViewById(R.id.wpv);
wpv.setWaveColor("#FF49C12E");
handler.sendEmptyMessageDelayed(FLAG_ONE, 1000);
}
}
佈局檔案
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
<com.lyxrobert.waveprogressview.WaveProgressView
android:id="@+id/wpv"
android:background="@drawable/bg"
android:layout_centerInParent="true"
android:layout_width="230dp"
android:layout_height="230dp" />
</RelativeLayout>