Android 自定義View一個控制元件搞定多種水波紋漣漪擴散效果
效果圖
實現思路
這個效果實現起來並不難,重要的是思路
此View滿足了多種水波紋漣漪擴散效果,這要求它能滿足很多的變化
根據上面的樣式,可以看出此View需要滿足以下變化
圓圈從中心可迴圈向外擴散
圓圈之間的擴散間距可以改變
可控制擴散圓的漸變度
圓圈可以是線條樣式或者實心樣式
圓圈擴散的速度可以控制
適配圓圈不同大小下的擴散效果
具體實現
建立自定義屬性
首先為View建立自定義的xml屬性
在工程的values目錄下新建attrs.xml檔案
<declare-styleable name="mRippleView">
<attr name="cColor" format="color"/>
<attr name="cSpeed" format="integer"/>
<attr name="cDensity" format="integer"/>
<attr name="cIsFill" format="boolean"/>
<attr name="cIsAlpha" format="boolean"/>
</declare-styleable>
各個屬性的作用如下
- cColor:View控制元件的顏色
- cSpeed:向外擴散的速度
- cDensity:圓形波紋擴散的間距
- cIsFill:是否開啟填充模式,true為實心圓
- cIsAlpha:是否開啟漸變效果,true為開啟
建立自定義View控制元件
新建RippleView類繼承View類,重寫它的三個構造方法,獲取使用者設定的屬性,同時指定預設值
public RippleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 獲取使用者配置屬性
TypedArray tya = context.obtainStyledAttributes(attrs, R.styleable.mRippleView);
mColor = tya.getColor(R.styleable.mRippleView_cColor, Color.BLUE);
mSpeed = tya.getInt(R.styleable.mRippleView_cSpeed, 1);
mDensity = tya.getInt(R.styleable.mRippleView_cDensity, 10);
mIsFill = tya.getBoolean(R.styleable.mRippleView_cIsFill, false);
mIsAlpha = tya.getBoolean(R.styleable.mRippleView_cIsAlpha, false);
tya.recycle();
init();
}
使用TypedArray讀取完自定義的屬性後一定要記得呼叫recycle
方法釋放掉
重寫onMeasure
測量onMeasure,首先需要測量出View的寬和高,並指定View在wrap_content時的最小範圍,對於View繪製流程還不熟悉的同學,可以先去了解下具體的繪製流程
重寫onMeasure方法,其中我們要考慮當View的寬高被指定為wrap_content時的情況,如果我們不對wrap_content的情況進行處理,那麼當使用者指定View的寬高為wrap_content時將無法正常顯示出View
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int myWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int myWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int myHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int myHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 獲取寬
if (myWidthSpecMode == MeasureSpec.EXACTLY) {
// match_parent/精確值
mWidth = myWidthSpecSize;
} else {
// wrap_content
mWidth = DensityUtil.dip2px(mContext, 120);
}
// 獲取高
if (myHeightSpecMode == MeasureSpec.EXACTLY) {
// match_parent/精確值
mHeight = myHeightSpecSize;
} else {
// wrap_content
mHeight = DensityUtil.dip2px(mContext, 120);
}
// 設定該view的寬高
setMeasuredDimension(mWidth, mHeight);
}
MeasureSpec的狀態分為三種EXACTLY、AT_MOST、UNSPECIFIED,這裡只要單獨指定非精確值EXACTLY之外的情況就好了
本文中使用到的DensityUtil類,是為了將dp轉換為px來使用,以便適配不同的螢幕顯示效果
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
重寫onDraw
設計的整體思路如下圖所示
先要實現圓形向外擴散的效果
- 初始化第一個圓
這裡的動畫效果本來是想使用ValueAnimator
屬性動畫的數值發生器來實現,但是我們這裡有很多的計算需求,所以最後還是選擇使用演算法來實現,方便控制圓的一些引數
想要實現擴散的效果,這裡思路是在每次更新View時動態改變圓的半徑,同時還需要給圓設定漸變度數,所以決定用一個類來儲存圓的狀態,所有圓都存在一個List
裡
// 新增第一個圓圈
mRipples = new ArrayList<>();
Circle c = new Circle(0, 255);
mRipples.add(c);
傳入Circle類裡的兩個引數,第一個0表示圓的初始寬度,第二個255表示初始透明度
- 新增新圓
要想實現不斷有圓向外擴散,就需要在第一個圓擴散到一定範圍時在圓心處再新增一個圓,這個的範圍可以由圓的半徑來控制,當List
集合中最後一個圓的半徑增加到某個值mDensity
時,新的圓就從圓心處創建出來
// 新增圓
if (mRipples.size() > 0) {
// 控制第二個圓出來的間距
if (mRipples.get(mRipples.size() - 1).width > DensityUtil.dip2px(mContext, mDensity)) {
mRipples.add(new Circle(0, 255));
}
}
- 刪除List中多餘的圓
List
中的圓儲存的數量不宜過多,多了記憶體消耗大,需要在當圓的半徑超過View的寬度時就刪掉這個圓
// 當圓超出View的寬度後刪除
if (c.width > mWidth / 2) {
mRipples.remove(i);
}
我們也可以在外切正方形的頂點處刪除這個圓,需要用到勾股定律來計算擴散圓到外切正方形頂點的位置
如上圖所示,得出計算公式為
// 使用勾股定律求得一個外切正方形中心點離頂點的距離
sqrtNumber = (int) (Math.sqrt(mWidth * mWidth + mHeight * mHeight) / 2);
這樣就需要修改刪除圓的位置了
if (c.width > sprtNumber) {
mRipples.remove(i);
}
- 控制擴散圓的漸變度
當圓在向View的邊緣擴散時,漸變度數的改變需要動態來計算,漸變的計算演算法要適配不同的圓寬度大小,我們知道透明度是0~255之間的,0表示完全透明,255表示百分百不透明,計算的時候就是需要將這個數值等份分配到圓的寬度裡
這裡要區分一點,對於圓來說,寬度是由圓心從0開始向外遞增,而漸變度數則是由圓心從255開始向外遞減,當圓與最外圍的正方形內切時漸變度必須變為0,由此分析得知,公式如下
透明度 = 255 - 圓的寬度 * (255 / View寬度)
double alpha = 255 - c.width * (255 / ((double) mWidth / 2));
c.alpha = (int) alpha;
GitHub地址
總結
關於自定義View的總結部分在我的其它部落格中已經寫過蠻多了,有興趣的可以去看看
- 做自定義View,思路很重要,當想到一種方法可以實現時,先不要著急的做出來,試著換一個角度再多思考一下還有沒有更好的實現方式
- 本例子還缺少一些控制邏輯程式碼,將在之後新增上去並更新在GitHub中