天天看點

自定義Drawable實作點選水波紋漣漪效果系統水波紋問題思考開始寫代碼總結

自定義Drawable實作點選水波紋漣漪效果

  • 系統水波紋
  • 問題思考
  • 開始寫代碼
    • 初始化工作
    • 偵測點選事件
    • 捕獲按下時手指的坐标
    • 執行動畫并繪制
    • 退出的動畫
  • 總結
Google 在Android 5.0時代推出了Material Design設計風格, 其中包含水波紋漣漪效果, 雖然已經過去号幾年了, 但我覺得還是有必要聊聊這個, 本文偏向于無界水波紋的實作, 主要起到抛磚引玉的作用, 不會寫得太複雜

系統水波紋

自定義Drawable實作點選水波紋漣漪效果系統水波紋問題思考開始寫代碼總結

咱們先來看一個示範,如上圖, 是系統的水波紋點選效果,通過以下代碼為TextView添加的點選效果:

<TextView
    android:id="@+id/tv_system"
    android:background="?android:attr/selectableItemBackground"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:gravity="center"
    android:text="系統水波紋效果" />
           

代碼很簡單,就是給TextView加了一個背景,而這個背景其實就是一個 ripple 标簽的Drawable, 如下代碼也能建立一個系統的水波紋效果,

ripple

标簽對應

RippleDrawable

,系統的水波紋都是在這個類裡實作的。

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@color/item_hover">
    <item
        android:id="@android:id/mask"
        android:drawable="@android:color/white"/>
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/transparent"/>
        </shape>
    </item>
</ripple>
           

問題思考

既然是自定義

Drawable

實作,那麼有如下問題需要解答:

  • Drawable

    裡怎麼監聽使用者點選事件? 難道要外部調用嗎? 系統可沒有這麼幹
  • 怎麼監聽點選的位置? (明顯系統的效果是從點選位置開始擴散的)
  • 水波紋效果分析, 包含哪些動畫?

通過閱讀系統的

RippleDrawable

源碼,我獲得了以上問題的答案,大家請不要急,一步一步的來。

開始寫代碼

既然要自定義 Drawable 實作, 我建立了類

CustomRippleDrawable

,繼承 Drawable, 重寫4個必須重寫的方法,再

MainActivity

中添加一個TextView, 設定點選事件和背景:

CustomRippleDrawable

, 準備工作就完成了。

<TextView
    android:id="@+id/tv_custom"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:gravity="center"
    android:text="自定義水波紋效果" />
           
mTvCustom.setOnClickListener(v -> {/**/});
mTvCustom.setBackground(new CustomRippleDrawable());

······N行代碼

public class CustomRippleDrawable extends Drawable {
	@Override
	public void draw(@NonNull Canvas canvas) {}

	@Override
	public void setAlpha(int alpha) {}

	@Override
	public void setColorFilter(@Nullable ColorFilter colorFilter) {}

	@Override
	public int getOpacity() {
		return 0;
	}
}
           

初始化工作

首先我們需要建立畫筆,設定顔色,抗鋸齒等

mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.parseColor("#33000000"));
mRealAlpha = mPaint.getAlpha();
           

偵測點選事件

當使用者點選按鈕時需要出發波紋動畫,是以我們需要偵測點選事件,用于出發動畫效果。

檢視系統

RippleDrawable

源碼後, 發現可以重寫

onStateChange

方法:

/**
 * Override this in your subclass to change appearance if you recognize the
 * specified state.
 *
 * @return Returns true if the state change has caused the appearance of
 * the Drawable to change (that is, it needs to be drawn), else false
 * if it looks the same and there is no need to redraw it since its
 * last state.
 */
protected boolean onStateChange(int[] state) {
    return false;
}
           

參數

int[] state

中是目前控件的所有狀态, 當state包含

android.R.attr.state_pressed

時,代表使用者按下了按鈕,當不包含

android.R.attr.state_pressed

并且上個狀态是按下狀态時,即使用者松開了按鈕;傳回值為

true

系統會認為

Drawable

需要重新繪制,才會走

onDraw

方法。

注意: 必須重寫

isStateful()

方法并傳回

true

,不然即使使用者觸發點選事件也不會走

onStateChange

方法。

/**
 * Indicates whether this drawable will change its appearance based on
 * state. Clients can use this to determine whether it is necessary to
 * calculate their state and call setState.
 *
 * @return True if this drawable changes its appearance based on state,
 *         false otherwise.
 * @see #setState(int[])
 */
public boolean isStateful() {
    return true;
}
           

如下圖所示代碼,我通過

onStateChange

偵測到了按下和擡起的事件。

自定義Drawable實作點選水波紋漣漪效果系統水波紋問題思考開始寫代碼總結

捕獲按下時手指的坐标

我們需要得到按下的坐标,以此為中心點,繪制一個逐漸擴散的圓。

擷取手指坐标可以重寫方法

setHotspot()

,此方法回報了手指實時移動的軌迹,我們需要記錄下最新的位置:

private PointF mCurrentPointF = new PointF();

/**
 * Specifies the hotspot's location within the drawable.
 *
 * @param x The X coordinate of the center of the hotspot
 * @param y The Y coordinate of the center of the hotspot
 */
public void setHotspot(float x, float y) {	
	mCurrentPointF.set(x, y);
}
           

當按下時儲存該位置,計算半徑,開啟擴散動畫:

private void enter() {
	mState = STATE_ENTER;
	progress = 0;
	mPressedPointF.set(mCurrentPointF);
	Rect bounds = getBounds();
	mMaxRadius = Math.max(bounds.width(), bounds.height());
	startEnterAnimation();
}
           

執行動畫并繪制

動畫的執行我使用了

ValueAnimator

, 動畫控制進度,

draw

方法根據進度繪制動畫

private void startEnterAnimation() {
	mRunningAnimator = ValueAnimator.ofInt(progress, maxProgress);
	mRunningAnimator.setInterpolator(new LinearInterpolator());
	mRunningAnimator.setDuration(animationTime);//300ms
	mRunningAnimator.addUpdateListener(animation -> {
		progress = (int) animation.getAnimatedValue();
		invalidateSelf();
	});
	mRunningAnimator.addListener(new AnimatorListenerAdapter() {
		@Override
		public void onAnimationEnd(Animator animation) {
			if(pendingExit){
				pendingExit = false;
				mState = STATE_EXIT;
				startExitAnimation();
			}
		}
	});
	mRunningAnimator.start();
}

@Override
public void draw(@NonNull Canvas canvas) {

	if(mState == STATE_ENTER){
		canvas.drawCircle(mPressedPointF.x, mPressedPointF.y, mMaxRadius * progress / maxProgress, mPaint);
	}
}
           

運作後效果如下:

自定義Drawable實作點選水波紋漣漪效果系統水波紋問題思考開始寫代碼總結

退出的動畫

細心的同學可能發現了,咱們自定義的明顯比系統的生硬,仔細觀察系統波紋效果,其實擡起的時候是有一個漸變的動畫的,接下來就來實作這個漸變動畫。

private void exit() {
	//如果正在執行擴散動畫, 需要等待動畫執行完畢再執行退出動畫
	if(progress != maxProgress && mState == STATE_ENTER){
		pendingExit = true;
	}else {
		mState = STATE_EXIT;
		startExitAnimation();
	}
}

private void startExitAnimation() {
	if(mRunningAnimator != null && mRunningAnimator.isRunning()){
		mRunningAnimator.cancel();
	}
	mRunningAnimator = ValueAnimator.ofInt(maxProgress, 0);
	mRunningAnimator.setInterpolator(new LinearInterpolator());
	mRunningAnimator.setDuration(animationTime);//300ms
	mRunningAnimator.addUpdateListener(animation -> {
		progress = (int) animation.getAnimatedValue();
		invalidateSelf();
	});
	mRunningAnimator.start();
}

@Override
public void draw(@NonNull Canvas canvas) {
	if(mState == STATE_ENTER){
		if(mPaint.getAlpha() != mRealAlpha){
			mPaint.setAlpha(mRealAlpha);
		}
		canvas.drawCircle(mPressedPointF.x, mPressedPointF.y, mMaxRadius * progress / maxProgress, mPaint);
	}else if(mState == STATE_EXIT){
		mPaint.setAlpha(mRealAlpha * progress / maxProgress);
		canvas.drawRect(getBounds(), mPaint);
	}
}
           

最終實作的效果如下:

自定義Drawable實作點選水波紋漣漪效果系統水波紋問題思考開始寫代碼總結

總結

其實動畫不難實作,關鍵點在于執行動畫的時機,以及捕獲手指位置的API需要閱讀源碼才能了解到,将這些結合起來其實基本效果很容易實作。當然自己定義的沒有系統的靈活、全面,本文主要起到一個抛磚引玉的作用,掌握了基本原理之後大家都可以對動畫效果深入定制。

另外,如果有人興趣可以前往

github

下載下傳 demo 源碼:

https://github.com/EshelGuo/RippleDrawableDemo