自定義控制元件三部曲之繪圖篇(二十)——RadialGradient與水波紋按鈕效果
前言:每當感嘆自己的失敗時,那我就問你,如果讓你重新來一次,你會不會成功?如果會,那說明並沒有拼盡全力。
系列文章:
最近博主實在是太忙了,部落格更新實在是太慢了,真是有愧大家。
這篇將是Shader的最後一篇,下部分,我們將講述Canvas變換的知識。在講完Canvas變換以後,就正式進入第三部曲啦,是不是有點小激動呢……
今天給大家講的效果是使用RadialGradient來實現水波紋按鈕效果,水波紋效果是Android L平臺上自帶的效果,這裡我們就看看它是如何實現的,本篇的最終效果圖如下
一、RadialGradient詳解
RadialGradient的意思是放射漸變,即它會向一個放射源一樣,從一個點開始向外從一個顏色漸變成另一種顏色;
一、建構函式
RadialGradient有兩個建構函式
//兩色漸變
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
//多色漸變
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)
(1)、兩色漸變建構函式使用例項
下面我們來看一下兩色漸變建構函式的使用方法。
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
這個兩色漸變的建構函式的各項引數意義如下:
- centerX:漸變中心點X座標
- centerY:漸變中心點Y座標
- radius:漸變半徑
- centerColor:漸變的起始顏色,即漸變中心點的顏色,取值型別必須是八位的0xAARRGGBB色值!透明底Alpha值不能省略,不然不會顯示出顏色。
- edgeColor:漸變結束時的顏色,即漸變圓邊緣的顏色,同樣,取值型別必須是八位的0xAARRGGBB色值!
- TileMode:與我們前面講的各個Shader一樣,用於指定當控制元件區域大於指定的漸變區域時,空白區域的顏色填充方式。
下面我們舉個例子來看下用法:
public class DoubleColorRadialGradientView extends View {
private Paint mPaint;
private RadialGradient mRadialGradient;
private int mRadius = 100;
public DoubleColorRadialGradientView(Context context) {
super(context);
init();
}
public DoubleColorRadialGradientView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DoubleColorRadialGradientView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
setLayerType(LAYER_TYPE_SOFTWARE,null);
mPaint = new Paint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
mPaint.setShader(mRadialGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
}
}
程式碼量不大,這裡首先在onSizeChange中,建立RadialGradient例項。onSizeChange會在佈局發生改變時呼叫,onSizeChanged(int w, int h, int oldw, int oldh)
傳過來四個引數,前兩個引數就代表當前控制元件所應顯示的寬和高。有關onSizeChange的具體意義,我們會在第三部曲講解回撥函式流程中具體講到,這裡大家就先理解到這吧。
在onSizeChange中,我們建立了一個RadialGradient,以控制元件的中心點為圓點,建立一個半徑為mRadius的,從0xffff0000到0xff00ff00的放射漸變。我們這裡指定的空白填充方式為Shader.TileMode.REPEAT。
然後在繪圖的時候:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
}
在繪圖時,依然是以控制元件中心點為圓心,畫一個半徑為mRadius的圓;注意我們畫的圓的大小與所構造的放射漸變的大小是一樣的,所以不存在空白區域的填充問題。
效果圖如下:
(2)、多色漸變建構函式使用例項
多色漸變的建構函式如下:
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)
這裡與兩色漸變不同的是兩個函式:
- int[] colors:表示所需要的漸變顏色陣列
- float[] stops:表示每個漸變顏色所在的位置百分點,取值0-1,數量必須與colors陣列保持一致,不然直接crash,一般第一個數值取0,最後一個數值取1;如果第一個數值和最後一個數值並沒有取0和1,比如我們這裡取一個位置陣列:{0.2,0.5,0.8},起始點是0.2百分比位置,結束點是0.8百分比位置,而0-0.2百分比位置和0.8-1.0百分比的位置都是沒有指定顏色的。而這些位置的顏色就是根據我們指定的TileMode空白區域填充模式來自行填充!!!有時效果我們是不可控的。所以為了方便起見,建議大家stop陣列的起始和終止數值設為0和1.
下面我們舉個例子來看下用法:
public class MultiColorRadialGradientView extends View {
private Paint mPaint;
private RadialGradient mRadialGradient;
private int mRadius = 100;
public MultiColorRadialGradientView(Context context) {
super(context);
init();
}
public MultiColorRadialGradientView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MultiColorRadialGradientView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
setLayerType(LAYER_TYPE_SOFTWARE,null);
mPaint = new Paint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int[] colors = new int[]{0xffff0000,0xff00ff00,0xff0000ff,0xffffff00};
float[] stops = new float[]{0f,0.2f,0.5f,1f};
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,colors,stops, Shader.TileMode.REPEAT);
mPaint.setShader(mRadialGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
}
}
這裡主要看下多色漸變的構造方法:
int[] colors = new int[]{0xffff0000,0xff00ff00,0xff0000ff,0xffffff00};
float[] stops = new float[]{0f,0.2f,0.5f,1f};
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,colors,stops, Shader.TileMode.REPEAT);
這裡構造了一個四色顏色陣列,漸變位置對應{0f,0.2f,0.5f,1f},然後建立RadialGradient例項。沒什麼難度。
然後在繪畫的時候,同樣以控制元件中心為半徑,以放射漸變的半徑為半徑畫圓。由於畫的圓半徑與放射漸變的大小相同,所以不存在空白位置填充的問題,所以TileMode.REPEAT並沒有用到。
效果圖如下:
二、TileMode重複方式
TileMode的問題,已經重複講了幾篇文章了,其實雖然每種Shader所表現出來的效果不一樣,但是形成原理都是相同的。下面我們再來看一下RadialGradient在不同的TileMode下的具體表現。
(1)、X、Y軸共用填充引數
與LinearGradient一樣,從建構函式中,可以明顯看出RadialGradient只有一個填充模式:
//兩色漸變
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
//多色漸變
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)
這就說明了,當填充空白區域時,X軸和Y軸使用同一種填充模式。而不能像BitmapShader那樣分別指定X軸與Y軸的填充引數。
(2)、TileMode.CLAMP——邊緣填充
我們依然使用雙色漸變的示例來看下效果,為了顯示填充效果,我們這次畫一個螢幕大小的矩形:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.CLAMP);
mPaint.setShader(mRadialGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}
效果圖如下:
從效果圖中可以明顯看出,除了放漸漸變以外的空白區域都被邊緣填充成為了綠色;
(3)、TileMode.REPEAT——重複填充
我們仍使用上面的程式碼,只是將填充模式改為重複填充:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
mPaint.setShader(mRadialGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}
效果圖如下:
這個影象乍看去有點辣眼睛,花花綠綠的……從效果圖中可以看出,最內部的圓形是紅到綠的原始放射漸變。其外面的圓就是空白區域填充模式了,在它的外圍,從紅到綠漸變。
(4)、TileMode.MIRROR—映象填充
同樣是使用上面的程式碼,只是將填充模式改為映象填充:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.MIRROR);
mPaint.setShader(mRadialGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}
效果圖如下:
有些同學第一下看到這個圖可能有點懵,所謂映象,就是把原來的顏色的倒過來填充。即原始是紅到綠漸變,第二圈就變成了綠到紅漸變,第三圈就又是紅到綠漸變,如此往復。
如果我把每一圈漸變的界限標出來,大家可能就容易看懂了:
圖中白色線就是每一圈漸變的邊界線,一次完整的填充就是兩個白色圈中的部分。
(5)、填充方式:從控制元件左上角開始填充
在講BitmapShader和LinearShader時,我們就一再強調一個點:無論哪種Shader,都是從控制元件的左上角開始填充的,利用canvas.drawXXX系列函式只是用來指定顯示哪一塊;
我們在RadialGradient中也做下測試:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
mPaint.setShader(mRadialGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0,0,200,200,mPaint);
}
我們這裡使用TileMode.REPEAT來填充空白區域,在繪圖時,我們只畫出左上角的一部分;
效果圖如下:
從效果圖中明顯可以看出結論了:
無論哪種Shader,都是從控制元件的左上角開始填充的,利用canvas.drawXXX系列函式只是用來指定顯示哪一塊
二、水波紋按鈕效果
這部分就要利用RadialGradient來實現水波紋效果了,我們這裡直接繼承自Button類,做一個按鈕的水波紋效果,其實這裡繼承自任何一個類都是可以在這個類原本的顯示內容上顯示水波紋效果的,比如,大家可以試驗下在原始碼的基礎上,將其改為派生自ImageView,當然要記得給它新增上src屬性,是一樣會有水波紋效果的。
1、概述
根據上面的的對RadialGradient的講解,大家第一反應應該是,水波紋很好實現啊:不就是,畫一個帶有漸變效果的逐漸放大的圓不就得了。不錯,思想確實就是這麼簡單。
(1)、不過,第一個問題來了,從哪個顏色,漸變到哪個顏色呢?
最理想的狀態是,從按鈕的背景色漸變到天藍色(開篇效果圖中顏色)。但是,怎麼拿到按鈕的背景色呢?因為按鈕的android:background屬性填充不一定是顏色,有可能是一個drawable,而這個drawable可以是圖片,也可能是selector檔案等,所以這條路根本走不通。
而我們講過,RadialGradient中填充的漸變色彩必須是AARRGGBB形式的,所以我們只需要講初始顏色的透明度設為0,不就露出了按鈕的背景色了麼。即類似下面的程式碼:
mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
在這裡我們將初始的漸變色改為0x00FFFFFF,由於透明度部分全部設定為0,所以整個顏色就是透明的。所以整個漸變過程就變為從零透明度逐漸變為純天藍色(0xFF58FAAC)。
(2)、第二個問題,我們應該怎麼安排RadialGradient的填充模式
從效果圖中是可以明顯看出我們會逐漸放大繪製RadialGradient的圓的,那麼,我們是讓RadialGradient的漸變變徑隨著繪製的圓增大而增大,還是不改變RadialGradient的初始半徑,空餘部分使用Shader.TileMode.CLAMP填充來實現水波紋呢。
答案是讓RadialGradient的漸變變徑隨著繪製的圓增大而增大;下面我們分別舉個例子來看下效果就知道區別了:
我們將RadialGradient的初始半徑設定為20,而假設當前繪製圓的半徑是150,分別用模擬程式碼來展示在不同程式碼處理下的效果,以最終決定選用哪種方式來繪製RadialGradient漸變。
如果使用空餘部分使用Shader.TileMode.CLAMP填充:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mRadialGradient == null) {
int x = getWidth()/2;
int y = getHeight()/2;
mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
mPaint.setShader(mRadialGradient);
canvas.drawCircle(x, y, 150, mPaint);
}
}
這裡以控制元件中心為圓心,構造一個RadialGradient,這個RadialGradient的半徑是20,從透明色,漸變到天藍色
mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
而在canvas畫圓時,仍然以控制元件中心為圓心,但圓的半徑卻是150,明顯要超出RadialGradient的半徑,空白部分使用Shader.TileMode.CLAMP邊緣模式填充
canvas.drawCircle(x, y, 150, mPaint);
效果圖如下:
從效果圖中可以看出,在0-20的部分是從透明色到天藍色的漸變,但是超出半徑20的部分,都以邊緣模式填充為完全不透明的天藍色,感覺跟按鈕完全沒有融合在一起有沒有
如果讓RadialGradient的漸變變徑隨著繪製的圓增大而增大
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mRadialGradient == null) {
int x = getWidth()/2;
int y = getHeight()/2;
mRadialGradient = new RadialGradient(x, y,150 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
mPaint.setShader(mRadialGradient);
canvas.drawCircle(x, y, 150, mPaint);
}
}
這裡的程式碼跟上面的一樣,唯一不同的是,構造的RadialGradient的漸變半徑與canvas.drawCircle所畫的圓的半徑是一樣的,都是150;這就演示了讓RadialGradient的漸變變徑隨著繪製的圓增大而增大的效果
效果圖如下:
很明顯,這是我們想要的結果,漸變色與按鈕的背景完全融合。
2、程式碼實現
上面在講解了解決了核心問題,以後,下面我們就開始正式實戰了
我們再來看下效果圖:
從效果圖中,可以看到我們所需要完成的功能:
- 在手指按下時,繪製一個預設大小的圓
- 在手指移動時,所繪製的預設圓的位置需要跟隨手指移動
- 在手指放開時,圓逐漸變大
- 在動畫結束時,波紋效果消失
按下和移動
首先,我們來完成前兩個功能:當首先按下時,繪製一個預設大小的圓,而且當手指移動時,可以跟隨移動:
private int mX, mY;
private int DEFAULT_RADIUS = 50;
public boolean onTouchEvent(MotionEvent event) {
if (mX != event.getX() || mY != mY) {
mX = (int) event.getX();
mY = (int) event.getY();
setRadius(DEFAULT_RADIUS);
}
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return true;
}
return super.onTouchEvent(event);
}
首先,我們這裡並沒區分MotionEvent.ACTION_DOWN
和MotionEvent.ACTION_UP
的繪圖操作,只是統一在當前手指位置與上次的不一樣時,就呼叫setRadius(DEFAULT_RADIUS);
重繪RadialGradient;很明顯,mX、mY變量表示當前手指的位置,而DEFAULT_RADIUS變量表示預設的RadialGradient的漸變尺寸。但是必須在 MotionEvent.ACTION_DOWN時return true,因為如果不return true,就表示當前控制元件並不需要下按之後的訊息,所以ACTION_MOVE、ACTION_UP訊息都不會再傳到這個控制元件裡來了,有關這個問題,在前面的文章中已經不只一次提到,這裡就不再綴述了。
其中,setRadius(DEFAULT_RADIUS)函式的定義如下:
//表示當前漸變半徑
private int mCurRadius = 0;
public void setRadius(final int radius) {
mCurRadius = radius;
if (mCurRadius > 0) {
mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
mPaint.setShader(mRadialGradient);
}
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mX, mY, mCurRadius, mPaint);
}
在setRadius中主要負責在手指位置和漸變半徑改變時,重新建立RadialGradient,然後重繪。很明顯mCurRadius變量表示當前的漸變半徑。最後在OnDraw函式中重繪時,畫一個跟漸變半徑同樣大小的圓即可。
手指放開
在手指放開時,主要是開始逐漸放大放射半徑的動畫,然後在動畫結束的時候,清除RadialGradient。程式碼如下:
private ObjectAnimator mAnimator;
@Override
public boolean onTouchEvent(MotionEvent event) {
…………
f (event.getAction() == MotionEvent.ACTION_UP) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (mAnimator == null) {
mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
}
mAnimator.setInterpolator(new AccelerateInterpolator());
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
setRadius(0);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mAnimator.start();
}
return super.onTouchEvent(event);
}
在這段程式碼中,首先是在開始下一個動畫前,先判斷當前mAnimator是不是還在動畫中,如果是正在動畫就先取消:
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
這是為了避免當用戶連續點選多次的時候,下一次開始動畫時,上一次動畫還沒結束,這樣兩次動畫就會造成衝突,應該先把上次的動畫取消掉,然後再重新開始這次的動畫:
mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
}
mAnimator.setInterpolator(new AccelerateInterpolator());
然後建立一個ObjectAnimator物件,這裡動畫操作的函式是setRadius(final int radius)函式,動畫的區間是從預設半徑到整個控制元件的寬度,之所以用當前控制元件的寬度來做為最大動畫值,是因為,我們必須指定一個足夠大的值,足以讓波紋能夠覆蓋整個控制元件以後再結束。從效果圖中可以看出,在這裡控制元件的寬度是整個控制元件長度的最大值,所以,我們就以使用者點選控制元件最邊緣來算,當用戶點選最左或最右邊緣時,整個RadialGradient的半徑是最大的,此時的最大值是控制元件寬度,所以我們就用控制元件寬度來做為動畫的最大值即可。
其實這裡還是不夠嚴謹,因為在實際應用中,控制元件的寬度並不是整個控制元件的最大值,也有可能是控制元件的高度是最大的,所以最嚴謹的做法就是先判斷控制元件的高度和寬度哪個最大,然後將最大值做為動畫的半徑。這裡為了簡化程式碼可讀性,就不再對比了。
然後給mAnimator設定AccelerateInterpolator()插值器,因為我們需要讓波紋的速度逐漸加快,如果不設定插值器的話,預設是使用LinearInterpolator插值器的,這樣出來的效果是波紋的變大速度將是勻速的。
mAnimator.setInterpolator(new AccelerateInterpolator());
最後我們需要監聽mAnimator結束的動作,當動畫結束時,我們需要讓RadialGradient消失,最簡單的消失辦法就是將所畫圓的半徑設定為0。
mAnimator.addListener(new Animator.AnimatorListener() {
…………
@Override
public void onAnimationEnd(Animator animation) {
setRadius(0);
}
…………
});
到這裡所有的程式碼就講完了,完整的程式碼如下:
public class RippleView extends Button {
private int mX, mY;
private ObjectAnimator mAnimator;
private int DEFAULT_RADIUS = 50;
private int mCurRadius = 0;
private RadialGradient mRadialGradient;
private Paint mPaint;
public RippleView(Context context) {
super(context);
init();
}
public RippleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RippleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE,null);
mPaint = new Paint();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mX != event.getX() || mY != mY) {
mX = (int) event.getX();
mY = (int) event.getY();
setRadius(DEFAULT_RADIUS);
}
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (mAnimator == null) {
mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
}
mAnimator.setInterpolator(new AccelerateInterpolator());
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
setRadius(0);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mAnimator.start();
}
return super.onTouchEvent(event);
}
public void setRadius(final int radius) {
mCurRadius = radius;
if (mCurRadius > 0) {
mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
mPaint.setShader(mRadialGradient);
}
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mX, mY, mCurRadius, mPaint);
}
}
如果你喜歡我的文章,那麼你將會更喜歡我的微信公眾號,將定期推送博主最新文章與收集乾貨分享給大家(一週一次)