Android中水波紋使用之自定義檢視實現
在前面一篇部落格中介紹了Android中水波紋的使用,如果忘記了可以去複習一下Android中水波紋使用,但是那種實現方法要在API21+以上才能使用。如果我們要相容API21以下的系統,則需要通過第三方的類庫,即通過自定義檢視來實現水波紋的效果。
自定義檢視,是Android中擴充套件控制元件常用的方法,這裡不做過多的介紹,如果不太會,可以去搜尋學習一下。這裡主要介紹如何實現水波紋效果,其次是對自定義檢視中常用的問題總結一下。
自定義檢視會涉及到建構函式,現在的自定義檢視建構函式有4個,最後一個一般是API21+,所以平時不常用。對這個不瞭解的可以去看看Android View 四個建構函式詳解。
總結一下:
一般第一個建構函式是指在程式碼中建立例項化呼叫;第二個建構函式是通過XML方式建立控制元件檢視,提供AttributeSet屬性設定;第三個也是通過XML方式建立控制元件檢視,提供AttributeSet屬性設定,同時提供預設樣式值defStyleAttr;第四個一樣通過XML方式建立,除了提供跟第三個一樣的引數外,新增defStyleRes。
View類的後兩個建構函式都是與主題相關的。
它們的屬性賦值優先順序為:
XML直接定義 > XML中style引用 > defStyleAttr > defStyleRes > theme直接定義
瞭解了上面後,我們再來看看幾個自定義檢視中會常用到的函式:
onFinishInflate()
onAttachedToWindow()
onMeasure()
onSizeChanged ()
onLayout ()
onConfigurationChanged()
onDraw()
dispatchDraw ()
draw()
onTouchEvent()
onInterceptTouchEvent()
onFinishInflate()方法一般是在xml檔案載入完成後呼叫這個方法;
onAttachedToWindow()方法是將檢視依附到Window中;
onMeasure()方法是測量自定義檢視的大小 ;
onSizeChanged()方法是自定義檢視大小發生改變時呼叫;
onLayout()方法是將自定義檢視放置到父容器的具體某個位置中;
onConfigurationChanged()方法是在當手機螢幕從橫屏和豎屏相互轉化時呼叫;
onDraw() 、dispatchDraw ()、draw()這三個方法,則是根據具體的情況來呼叫的;
自定義一個view時,重寫onDraw()。
呼叫view.invalidate(),會導致draw流程重新執行。
view.postInvalidate(); //是在非UI執行緒上呼叫的自定義一個ViewGroup,重寫onDraw()。
onDraw可能不會被呼叫,原因是需要先設定一個背景(顏色或圖)。
表示這個group有東西需要繪製了,才會觸發draw,之後是onDraw。
因此,一般直接重寫dispatchDraw()來繪製viewGroupdispatchDraw會()對子檢視進行分發繪製操作。
總結一下:
View元件的繪製會呼叫draw(Canvas canvas)方法,draw過程中主要是先畫Drawable背景,對 drawable呼叫setBounds()然後是draw(Canvas c)方法。有點注意的是背景drawable的實際大小會影響view元件的大小,drawable的實際大小通過getIntrinsicWidth()和getIntrinsicHeight()獲取,當背景比較大時view元件大小等於背景drawable的大小。
畫完背景後,draw過程會呼叫onDraw(Canvas canvas)方法,然後就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分發給子元件進行繪製,我們通常定製元件的時候重寫的是onDraw()方法。值得注意的是ViewGroup容器元件的繪製,當它沒有背景時直接呼叫的是dispatchDraw()方法, 而繞過了draw()方法,當它有背景的時候就呼叫draw()方法,而draw()方法裡包含了dispatchDraw()方法的呼叫。因此要在ViewGroup上繪製東西的時候往往重寫的是dispatchDraw()方法而不是onDraw()方法,或者自定製一個Drawable,重寫它的draw(Canvas c)和 getIntrinsicWidth()。
該總結來自網路,感謝分享~~~
最後是onTouchEvent()和onInterceptTouchEvent()方法,這兩個方法也是我們經常會用到的。
onInterceptTouchEvent()方法定義在於ViewGroup中,預設返回值為false,表示不攔截TouchEvent()。onTouchEvent()方法定義在View中,當ViewGroup要呼叫onTouchEvent()時,呼叫super.onTouchEvent()方法。ViewGroup呼叫onTouchEvent()預設返回false,表示不消耗touch事件,View呼叫onTouchEvent()預設返回true,表示消耗了touch事件。
到這裡我們把自定義檢視會常遇到的方法都大致總結了一下。接下來再進行分析,因為有了這些方法,但我們還不知道它們呼叫的順序,只有清楚了這些後才能做出更好的自定義檢視。
首先建立自定義檢視如下,繼承View父類,然後列印出各種方法。
public class CustomView extends View {
public CustomView(Context context) {
super(context);
Log.i("anumbrella","constructor1");
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.i("anumbrella","constructor2");
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.i("anumbrella","constructor3");
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
Log.i("anumbrella","constructor3");
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.i("anumbrella","onAttachedToWindow()");
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.i("anumbrella","onConfigurationChanged()");
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
Log.i("anumbrella","dispatchDraw()");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i("anumbrella","onDraw()");
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Log.i("anumbrella","draw()");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.i("anumbrella","onLayout()");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.i("anumbrella","onMeasure()");
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.i("anumbrella", "onSizeChanged() " + " w = " + w + " h = " + h + " oldW = " + oldw + " oldH = " + oldw);
}
}
結果如下圖:
我們可以看到自定義view執行的方法順序為:constructor2->onAttachedToWindow()->onMeasure()->onSizeChanged()->onLayout()->onMeasure()->onLayout()->onDraw()->dispatchDraw()->draw()。
現在我們將上面的程式碼改為繼承VIewGroup(如:RelativeLayout)的檢視控制元件,再新增onTouchEvent()和onInterceptTouchEvent()方法,再執行。
如下:
為啥沒有呼叫draw()方法?因為ViewGroup沒有背景顏色,這就跟上面總結的一樣的。我們加上背景顏色android:background=”@color/colorPrimary”。重新執行,結果可以看到呼叫了draw()方法:
然後我們在自定義的ViewGroup下面新增子檢視,比如TextView重新執行,點選時就會呼叫onTouchEvent()方法。
這裡只是對自定義檢視會用到的方法進行了簡單的介紹,更深入的瞭解在此不做過多介紹。
好了,接下來我們才要開始進入主題——自定義的水波紋實現效果。有了上面的知識,我相信對下面的程式碼理解就會容易多了。
先來看看效果:
具體的程式碼如下,我們接下來一步一步介紹:
public class RippleView extends RelativeLayout {
/**
* 水波紋的顏色
*/
private int rippleColor;
/**
* 水波紋擴散型別
*/
private Integer rippleType;
/**
* 放大持續時間
*/
private int zoomDuration;
/**
* 放大比例
*/
private float zoomScale;
/**
* 放大動畫類
*/
private ScaleAnimation scaleAnimation;
/**
* 檢視是否放大
*/
private Boolean hasToZoom;
/**
* 是否從檢視中心開始動畫
*/
private Boolean isCentered;
/**
* 幀速率
*/
private int frameRate = 10;
/**
* 水波紋持續時間
*/
private int rippleDuration = 400;
/**
* 水波紋透明度
*/
private int rippleAlpha = 90;
/**
* canvas畫布執行Handler
*/
private Handler canvasHandler;
/**
* 水波紋畫筆
*/
private Paint paint;
/**
* 水波紋擴散內邊距
*/
private int ripplePadding;
/**
* 手勢監聽類
*/
private GestureDetector gestureDetector;
/**
* 水波紋動畫是否開始
*/
private boolean animationRunning = false;
/**
* 時間統計
*/
private int timer = 0;
/**
* 時間間隔
*/
private int timerEmpty = 0;
/**
* 水波紋持續時間間隔
*/
private int durationEmpty = -1;
/**
* 最大圓半徑
*/
private float radiusMax = 0;
/**
* 水波紋圓的座標點
*/
private float x = -1;
private float y = -1;
private Bitmap originBitmap;
private OnRippleCompleteListener onCompletionListener;
/**
* 檢視的寬和高
*/
private int WIDTH;
private int HEIGHT;
/**
* 定義水波紋型別
*/
public enum RippleType {
SIMPLE(0),
DOUBLE(1),
RECTANGLE(2);
int type;
RippleType(int type) {
this.type = type;
}
}
/**
* 水波紋更新波紋Runnable
*/
private final Runnable runnable = new Runnable() {
@Override
public void run() {
invalidate();
}
};
/**
* 定義回撥函式,當水波紋效果完成時呼叫
*/
public interface OnRippleCompleteListener {
void onComplete(RippleView rippleView);
}
public RippleView(Context context) {
super(context);
}
public RippleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RippleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* 初始化方法
*
* @param context
* @param attrs
*/
private void init(Context context, AttributeSet attrs) {
if (isInEditMode()) {
return;
}
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, getResources().getColor(R.color.rippelColor));
rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
canvasHandler = new Handler();
zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
typedArray.recycle();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);
//使onDraw方法可以呼叫,以便被我們重寫
this.setWillNotDraw(false);
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public void onLongPress(MotionEvent event) {
super.onLongPress(event);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
//開啟cache來繪製檢視
this.setDrawingCacheEnabled(true);
this.setClickable(true);
}
/**
* 繪製水波紋
*
* @param canvas
*/
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (animationRunning) {
canvas.save();
if (rippleDuration <= timer * frameRate) {
animationRunning = false;
timer = 0;
durationEmpty = -1;
timerEmpty = 0;
//android 23 會自動呼叫canvas.restore();
if (Build.VERSION.SDK_INT != 23) {
canvas.restore();
}
invalidate();
if (onCompletionListener != null) {
onCompletionListener.onComplete(this);
}
return;
} else {
canvasHandler.postDelayed(runnable, frameRate);
}
if (timer == 0) {
canvas.save();
}
canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
paint.setColor(Color.parseColor("#ffff4444"));
if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
if (durationEmpty == -1) {
durationEmpty = rippleDuration - timer * frameRate;
}
timerEmpty++;
final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
canvas.drawBitmap(tmpBitmap, 0, 0, paint);
tmpBitmap.recycle();
}
paint.setColor(rippleColor);
if (rippleType == 1) {
if ((((float) timer * frameRate) / rippleDuration) > 0.6f) {
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
} else {
paint.setAlpha(rippleAlpha);
}
} else {
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
}
timer++;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
WIDTH = w;
HEIGHT = h;
scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
scaleAnimation.setDuration(zoomDuration);
scaleAnimation.setRepeatMode(Animation.REVERSE);
scaleAnimation.setRepeatCount(1);
}
/**
* 啟動水波紋動畫,通過MotionEvent事件
*
* @param event
*/
public void animateRipple(MotionEvent event) {
createAnimation(event.getX(), event.getY());
}
/**
* 啟動水波紋動畫,通過x,y座標
*
* @param x
* @param y
*/
public void animateRipple(final float x, final float y) {
createAnimation(x, y);
}
private void createAnimation(final float x, final float y) {
if (this.isEnabled() && !animationRunning) {
if (hasToZoom) {
this.startAnimation(scaleAnimation);
}
radiusMax = Math.max(WIDTH, HEIGHT);
if (rippleType != 2) {
radiusMax /= 2;
}
radiusMax -= ripplePadding;
if (isCentered || rippleType == 1) {
this.x = getMeasuredWidth() / 2;
this.y = getMeasuredHeight() / 2;
} else {
this.x = x;
this.y = y;
}
animationRunning = true;
if (rippleType == 1 && originBitmap == null) {
originBitmap = getDrawingCache(true);
}
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
animateRipple(event);
sendClickEvent(false);
}
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
this.onTouchEvent(event);
return super.onInterceptTouchEvent(event);
}
/**
* 傳送一個點選事件,如果父檢視是ListView例項
*
* @param isLongClick
*/
private void sendClickEvent(final Boolean isLongClick) {
if (getParent() instanceof AdapterView) {
final AdapterView adapterView = (AdapterView) getParent();
final int position = adapterView.getPositionForView(this);
final long id = adapterView.getItemIdAtPosition(position);
if (isLongClick) {
if (adapterView.getOnItemLongClickListener() != null) {
adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
}
} else {
if (adapterView.getOnItemClickListener() != null) {
adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
}
}
}
}
/**
* 設定水波紋的顏色
*
* @param rippleColor
*/
public void setRippleColor(int rippleColor) {
this.rippleColor = getResources().getColor(rippleColor);
}
public int getRippleColor() {
return rippleColor;
}
public RippleType getRippleType() {
return RippleType.values()[rippleType];
}
/**
* 設定水波紋動畫型別,預設為RippleType.SIMPLE
*
* @param rippleType
*/
public void setRippleType(final RippleType rippleType) {
this.rippleType = rippleType.ordinal();
}
public Boolean isCentered() {
return isCentered;
}
/**
* 設定水波紋動畫是否開始從父檢視中心開始,預設為false
*
* @param isCentered
*/
public void setCentered(final Boolean isCentered) {
this.isCentered = isCentered;
}
public int getRipplePadding() {
return ripplePadding;
}
/**
* 設定水波紋內邊距,預設為0dip
*
* @param ripplePadding
*/
public void setRipplePadding(int ripplePadding) {
this.ripplePadding = ripplePadding;
}
public Boolean isZooming() {
return hasToZoom;
}
/**
* 在水波紋結束後,是否有放大動畫,預設為false
*
* @param hasToZoom
*/
public void setZooming(Boolean hasToZoom) {
this.hasToZoom = hasToZoom;
}
public float getZoomScale() {
return zoomScale;
}
/**
* 設定放大動畫比例
*
* @param zoomScale
*/
public void setZoomScale(float zoomScale) {
this.zoomScale = zoomScale;
}
public int getZoomDuration() {
return zoomDuration;
}
/**
* 設定放大動畫持續時間,預設為200ms
*
* @param zoomDuration
*/
public void setZoomDuration(int zoomDuration) {
this.zoomDuration = zoomDuration;
}
public int getRippleDuration() {
return rippleDuration;
}
/**
* 設定水波紋動畫持續時間,預設為400ms
*
* @param rippleDuration
*/
public void setRippleDuration(int rippleDuration) {
this.rippleDuration = rippleDuration;
}
public int getFrameRate() {
return frameRate;
}
/**
* 設定水波紋動畫的幀速率,預設為10
*
* @param frameRate
*/
public void setFrameRate(int frameRate) {
this.frameRate = frameRate;
}
public int getRippleAlpha() {
return rippleAlpha;
}
/**
* 設定水波紋動畫的透明度,預設為90,取值為0到255之間
*
* @param rippleAlpha
*/
public void setRippleAlpha(int rippleAlpha) {
this.rippleAlpha = rippleAlpha;
}
public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
this.onCompletionListener = listener;
}
/**
* 繪製擴散背景範圍檢視bitmap
*
* @param radius
* @return
*/
private Bitmap getCircleBitmap(final int radius) {
final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
final Rect rect = new Rect((int) (x - radius), (int) (y - radius), (int) (x + radius), (int) (y + radius));
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawCircle(x, y, radius, paint);
//出來兩圖交叉情況
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(originBitmap, rect, rect, paint);
return output;
}
}
定義RippleView類繼承RelativeLayout。為啥是RelativeLayout?覺得可能比LinearLayout耗效能,但是它的擴充套件性畢竟好很多。所以這點還是可以忽略的,其次通過建構函式,引入attr下的style屬性。
attr下的屬性為:
<!--自定義水波紋樣式屬性-->
<declare-styleable name="RippleView">
<!--定義透明度-->
<attr name="rv_alpha" format="integer" />
<!--畫面frame速率(幀速率)-->
<attr name="rv_framerate" format="integer" />
<!--水波紋持續時間-->
<attr name="rv_rippleDuration" format="integer" />
<!--檢視放大持續時間-->
<attr name="rv_zoomDuration" format="integer" />
<!--擴散水波紋顏色-->
<attr name="rv_color" format="color" />
<!--是否從中心擴散-->
<attr name="rv_centered" format="boolean" />
<!--水波紋樣式-->
<attr name="rv_type" format="enum">
<enum name="simpleRipple" value="0" />
<enum name="doubleRipple" value="1" />
<enum name="rectangle" value="2" />
</attr>
<!--水波紋擴散內邊距-->
<attr name="rv_ripplePadding" format="dimension" />
<!--水波紋是否放大-->
<attr name="rv_zoom" format="boolean" />
<!--放大比例-->
<attr name="rv_zoomScale" format="float" />
</declare-styleable>
有了這些後,我們就在init()函式中獲取定義的屬性,如果沒有獲取到
XML中定義的屬性就設定為預設的值。然後呼叫onMeasure()來獲取背
景檢視的大小,再呼叫draw()方法去繪製。
在draw()方法中,一開始animationRunning是false,所以不會執行任何操作。
當我們點選檢視時,這個onTouchEvent()方法就會呼叫,獲取具體的x,y座標,然後對radiusMax、animationRunning變數進行設定。
這個時候animationRunning為ture。最後呼叫invalidate(),重新draw()開始繪製。
在draw()中判斷時間是否結束了,沒有結束就通過canvasHandler來不停更新檢視,呼叫draw()重新繪製檢視。同時每次timer都會進行增加1,然後我們就通過timer的改變來實現半徑大小的變化。每次繪製圓就形成了水波紋。
當要實現不同效果的水波紋時,即rippleType的值不一樣時。就可以通過繪製不同的背景效果,改變透明度來實現。
好了,結束了。這就是水波紋自定義檢視的大致實現,水波紋演示程式碼。
相關文章
- Android水波紋效果實現Android
- Android中水波紋使用Android
- Android自定義View 水波氣泡AndroidView
- Android-貝塞爾曲線實現水波紋動畫Android動畫
- Android自定義View——從零開始實現水波浪進度框AndroidView
- 類似咻一咻,水波紋實現
- Android 水波紋效果的探究Android
- iOS實現自定義的彈出檢視(popView)iOSView
- Android Paint應用之自定義View實現進度條控制元件AndroidAIView控制元件
- 使用CSS實現逼真的水波紋點選效果CSS
- Flutter自定義實現神奇的卡片切換檢視Flutter
- 如何使用Android自定義複合檢視Android
- 水波紋特效—Ripple特效
- android中foreground水波實現過程分析Android
- 自定義檢視指令
- Android自定義拍照實現Android
- Android 實現自定義圓環Android
- Android 炫酷多重水波紋 MultiWaveHeaderAndroidHeader
- Laravel 自定義檢視元件Laravel元件
- 自定義例外 + 建立檢視
- 浪起來!使用 drawBitmapMesh 實現模擬水波紋效果
- 自定義波紋動畫動畫
- Flutter | 如何實現一個水波紋擴散效果的 WidgetFlutter
- HarmonyOS NEXT應用開發—自定義檢視實現Tab效果
- Item點選水波紋效果
- MVC自定義檢視規則MVC
- Android程式碼實現自定義ButtonAndroid
- Android 最簡單的自定義檢視管理之一Android
- Xamarin iOS教程之自定義檢視iOS
- video自定義實現視訊播放功能IDE
- SAPGUI裡實現自定義的語法檢查GUI
- MySQL使用之五_自定義函式和自定義過程MySql函式
- .Net Core中自定義認證實現
- UWP中實現自定義標題欄
- Android自定義控制元件之自定義ViewGroup實現標籤雲Android控制元件View
- 自定義View:自定義屬性(自定義按鈕實現)View
- 水波圖實現原理
- android 控制元件點選水波紋效果的幾種方案Android控制元件