Android 水波紋效果


在Android開發中,我們的UI設計師可能會設計出一些比較比較炫酷或者個性的效果然后需要我們來實現,今天給大家分享一個使用Android自定義控件實現的水波紋效果。

廢話不多說,我們直接來看一下效果圖:

                               

效果圖已經看到了,這個控件主要是實現給定一個百分比的值,然后就可以自動繪制水位不斷上漲直到達到給定的值。同時提供了幾個方法(開始、暫停和重置)和一個接口(通過設置這個接口,在百分比的值發生變化時,可以實時獲取到當前繪制的比例)。

在這里我只貼出一部分代碼,所有的代碼已經上傳,CSDN下載   GitHub下載


首先是構造方法:

public WaveView(Context context) {
this(context, null);
}

public WaveView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs); // 獲取布局文件中定義的屬性
init();
}
提供了三個構造方法,可以在布局文件中聲明控件,也可以在代碼中創建控件;

下面的方法是獲取布局文件中的屬性的方法:

private void initAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveView);
mWaveLenght1 = typedArray.getInteger(R.styleable.WaveView_wave1Length, WAVE_LENGTH1);
mWaveHeight1 = typedArray.getInteger(R.styleable.WaveView_wave1Height, WAVE_HEIGHT1);
mWaveColor1 = typedArray.getColor(R.styleable.WaveView_wave1Color, WAVE_COLOR1);
mOffset1 = typedArray.getInteger(R.styleable.WaveView_wave1Offset, WAVE_OFFSET1);

mWaveLenght2 = typedArray.getInteger(R.styleable.WaveView_wave2Length, WAVE_LENGTH2);
mWaveHeight2 = typedArray.getInteger(R.styleable.WaveView_wave2Height, WAVE_HEIGHT2);
mWaveColor2 = typedArray.getColor(R.styleable.WaveView_wave2Color, WAVE_COLOR2);
mOffset2 = typedArray.getInteger(R.styleable.WaveView_wave2Offset, WAVE_OFFSET2);

mBorderWidth = typedArray.getDimensionPixelSize(R.styleable.WaveView_borderWidth, BORDER_WIDTH);
mBorderColor = typedArray.getColor(R.styleable.WaveView_borderColor, BORDER_COLOR);

mTextSize = typedArray.getDimensionPixelSize(R.styleable.WaveView_textSize, DEFAULT_TEXT_SIZE);
mTextColor = typedArray.getColor(R.styleable.WaveView_textColor, DEFAULT_TEXT_COLOR);

mTime = typedArray.getInteger(R.styleable.WaveView_intervalTime, DEFAULT_TIME);
mPrecent = typedArray.getFloat(R.styleable.WaveView_precent, 0.5f);
int shapeValue = typedArray.getInteger(R.styleable.WaveView_showShape, 0);
if (shapeValue == 0) mShape = ShowShape.RECT;
else mShape = ShowShape.CIRCLE;
typedArray.recycle();
}
以上屬性是可以直接在布局文件中定義的,然后通過上面這段代碼讀取到具體的值, 需要說明的是,如果直接把上傳代碼中的WaveView.java文件拷貝到自己的項目中,一定要記得將res\values目錄下的attrs.xml文件中的一下屬性聲明拷貝到自己項目中的attrs.xml文件中,否則自定義屬性會報錯;同時在使用自定義屬性時也要申明這些屬性所在的名稱空間,如果對自定義屬性不是很了解的話,可以訪問《Android自定義View的基本步驟和使用自定義屬性》這篇博客的 “在自定義控件中使用自定義屬性” 這個部分。

下面是attrs.xml文件中定義的屬性:

<!--自定義水波紋效果屬性-->
<declare-styleable name="WaveView">
<!--波浪1的長度、高度、顏色和每次重繪的偏移量-->
<attr name="wave1Length" format="integer" />
<attr name="wave1Height" format="integer" />
<attr name="wave1Color" format="color" />
<attr name="wave1Offset" format="integer" />
<!--波浪2的長度、高度、顏色和每次重繪的偏移量-->
<attr name="wave2Length" format="integer" />
<attr name="wave2Height" format="integer" />
<attr name="wave2Color" format="color" />
<attr name="wave2Offset" format="integer" />
<!--邊框的寬度和顏色-->
<attr name="borderWidth" format="dimension" />
<attr name="borderColor" format="color" />
<!--文字的大小和顏色-->
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<!--水位高度的百分比-->
<attr name="precent" format="float" />
<!--兩次重繪的間隔時間-->
<attr name="intervalTime" format="integer" />
<!--控件的顯示形狀,rect矩形、circle圓形-->
<attr name="showShape" format="enum">
<enum name="rect" value="0" />
<enum name="circle" value="1" />
</attr>
</declare-styleable>

在剛剛的構造函數中調用了兩個方法,一個就是上面已經貼出來的獲取xml文件中的屬性的方法,還有一個就是下面的方法,主要是做一些初始化相關對象:

/**
* 初始化方法
*/
private void init() {
mWavePaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
mWavePaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

mWavePath1 = new Path();
mWavePath2 = new Path();
mCirclePath = new Path();

mWavePaint1.setColor(mWaveColor1);
mWavePaint1.setStyle(Paint.Style.FILL);

mWavePaint2.setColor(mWaveColor2);
mWavePaint2.setStyle(Paint.Style.FILL);

mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderPaint.setStyle(Paint.Style.STROKE);

mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);

// 定義數字顯示個格式
mFormat = new DecimalFormat("###,###,##0.00");
}
需要實現圓形形狀,我們需要計算出圓心和半徑,需要實現水位的上升以及上升到指定的百分比高度就不在上升,我們需要通過指定的百分比和控件的大小來確定水位的高度,這個計算我們重寫一個方法,onSizeChanged(int w, int h, int oldw, int oldh),這個方法是在控件的大小發生改變時系統回調的,所以我們在這個方法里面計算相關的值:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
mHeight = h;
mChangeY = mHeight;
// 計算波峰個數,為了實現移動效果,保證至少繪制兩個波峰
mWaveCount = (int) Math.round(mWidth / Math.max(mWaveLenght1, mWaveLenght2) + 1.5);
mFinalY = (1 - mPrecent) * mHeight; // 計算水位最終高度

// 計算圓心和半徑
mCenterX = mWidth / 2;
mCenterY = mHeight / 2;
mRadius = Math.min(mWidth, mHeight) / 2;
}
下面貼出來的代碼就是怎個控件最核心的代碼,也就是onDraw()繪制方法,注釋已經寫得很詳細,我就不做過多的解釋:

@Override
protected void onDraw(Canvas canvas) {
mWavePath1.reset();
mWavePath2.reset();
mCirclePath.reset();

// 如果想要顯示更多的形狀,在這里剪切不同形狀即可
if (mShape == ShowShape.CIRCLE) {
// 關閉硬件加速,否則canvas的剪切無法生效
if(canvas.isHardwareAccelerated() || this.isHardwareAccelerated() && invalidateFlag){
this.setLayerType(LAYER_TYPE_SOFTWARE,null);
}
// 先判斷是否為圓形,如果是,直接使用canvas的方法剪切一個圓形顯示,其余地方不顯示
mCirclePath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW);
canvas.clipPath(mCirclePath);
}

if (mBorderWidth > 0) {
// 邊框大於0,表示需要繪制邊框
if (mShape == ShowShape.CIRCLE) {
canvas.drawCircle(mCenterX, mCenterY, mRadius, mBorderPaint);
} else if (mShape == ShowShape.RECT) {
canvas.drawRect(0, 0, mWidth, mHeight, mBorderPaint);
}
}

mWavePath1.moveTo(-mWaveLenght1, mChangeY);
mWavePath2.moveTo(-mWaveLenght2, mChangeY);
if (!isReset) { // 判斷重置標記
// 利用貝塞爾曲線繪制波浪
for (int i = 0; i < mWaveCount; i++) {
// 繪制波浪1的曲線
mWavePath1.quadTo((-mWaveLenght1 * 3 / 4) + (i * mWaveLenght1) + mMoveSum1, mChangeY + mWaveHeight1, (-mWaveLenght1 / 2) + (i * mWaveLenght1) + mMoveSum1, mChangeY);
mWavePath1.quadTo((-mWaveLenght1 * 1 / 4) + (i * mWaveLenght1) + mMoveSum1, mChangeY - mWaveHeight1, (i * mWaveLenght1) + mMoveSum1, mChangeY);

// 繪制波浪2的曲線
mWavePath2.quadTo((-mWaveLenght2 * 3 / 4) + (i * mWaveLenght2) + mMoveSum2, mChangeY - mWaveHeight2, (-mWaveLenght2 / 2) + (i * mWaveLenght2) + mMoveSum2, mChangeY);
mWavePath2.quadTo((-mWaveLenght2 * 1 / 4) + (i * mWaveLenght2) + mMoveSum2, mChangeY + mWaveHeight2, (i * mWaveLenght2) + mMoveSum2, mChangeY);
}

// 不斷改變高度,實現逐漸水位逐漸上漲效果
mChangeY -= 1;
if (mChangeY < mFinalY) mChangeY = mFinalY;

// 波峰1往右移動,波峰2往左移動
mMoveSum1 += mOffset1;
mMoveSum2 -= mOffset2;
if (mMoveSum1 >= mWaveLenght1) mMoveSum1 = 0;
if (mMoveSum2 <= 0) mMoveSum2 = mWaveLenght2;

// 填充矩形,讓上漲之后的水位下面填充顏色
mWavePath1.lineTo(mWidth, mHeight);
mWavePath1.lineTo(0, mHeight);
mWavePath1.close();
mWavePath2.lineTo(mWidth, mHeight);
mWavePath2.lineTo(0, mHeight);
mWavePath2.close();

canvas.drawPath(mWavePath1, mWavePaint1);
canvas.drawPath(mWavePath2, mWavePaint2);
} else {
// 是重置
canvas.drawColor(Color.TRANSPARENT);
}

// 計算當前的百分比
mCurrentPrecent = 1 - mChangeY / mHeight;
// 格式化數字格式
String format1 = mFormat.format(mCurrentPrecent);
// 繪制文字,將百分比繪制到界面。此處用的是 "50%" 的形式,可以根據需求改變格式
double parseDouble = Double.parseDouble(format1);
canvas.drawText((int) (parseDouble * 100) + " %", (mWidth - mTextPaint.measureText(format1)) / 2, mHeight / 5, mTextPaint);
// 監聽對象不為null並且沒有達到設置高度時,調用監聽方法
if (mPrecentChangeListener != null && mChangeY != mFinalY) {
mPrecentChangeListener.precentChange(parseDouble);
}

// 判斷繪制標記
if (invalidateFlag) postInvalidateDelayed(mTime);
}
在以上代碼中,我是通過貝塞爾曲線來繪制波浪的;然后當設置顯示的形狀是圓形的時候,我是通過canvas的剪切區屬性來設置的,只是在使用這個方法的時候需要關閉硬件加速,否則就達不到效果。當然,要定義圓形效果,我們還可以使用圖像的混合模式(PorterDuffXfermode)來做。具體的做法可以參照《 Android自定義View之基本API(二)》這篇博客最后面的 “位圖運算PorterDuffXfermode” 部分的一個實例,制作一個簡單的圓形圖片。

在這個自定義的水波紋的代碼中,還定義的一個枚舉來表示形狀,一個監聽來獲取不斷變化的百分比的值:

/**
* 形狀枚舉,暫時只支持矩形和圓形,可擴展
*/
public enum ShowShape {
RECT, CIRCLE
}

/**
* 百分比改變監聽接口
*/
public interface PrecentChangeListener {
/**
* 百分比發生改變時調用的方法
*
* @param precent 當前的百分比,格式 0.00 范圍 [0.00 , 1.00]
*/
void precentChange(double precent);
}
在最后提供了上面效果圖中幾個按鈕的方法:

/**
* 開始
*/
public void start() {
invalidateFlag = true;
isReset = false;
postInvalidateDelayed(mTime);
}

/**
* 暫停
*/
public void stop() {
invalidateFlag = false;
isReset = false;
}

/**
* 重置
*/
public void reset() {
invalidateFlag = false;
isReset = true;
mChangeY = mHeight;
postInvalidate();
}
以上就是自定義水波紋效果控件的主要方法,在文章最后貼出在代碼和布局文件中設置屬性的代碼:

// 代碼設置相關屬性
waveview1.setBorderWidth(2)
.setWaveColor1(Color.RED)
.setWaveColor2(Color.parseColor("#80ff0000"))
.setBorderColor(Color.GREEN)
.setTextColor(Color.BLUE)
.setShape(WaveView.ShowShape.RECT)
.setTextSize(36)
.setPrecent(0.65f)
.setTime(2);
在布局文件中設置屬性:

<com.waveview.view.WaveView
android:id="@+id/waveview"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="10dp"
renj:borderColor="#00ff00"
renj:borderWidth="2dp"
renj:intervalTime="3"
renj:precent="0.6"
renj:showShape="circle"
renj:textColor="#0000ff"
renj:textSize="18sp"
renj:wave1Color="#ff0000"
renj:wave2Color="#80ff0000"/>
記得添加名稱空間:

xmlns:renj="http://schemas.android.com/apk/res-auto"
在代碼中設置監聽器的方法:

waveview.setPrecentChangeListener(new WaveView.PrecentChangeListener() {
@Override
public void precentChange(double precent) {
tvPrecent.setText("當前進度:" + precent + "");
}
});

上面效果圖的完整代碼已上傳, CSDN下載    GitHub下載,是一個完整的Android studio項目,不要導入,直接用studio打開一個新項目即可,打開之后直接運行就和上面的效果圖一樣。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com