今天我們來利用Android自定義控件實現一個比較有趣的效果:滑動水波紋。先來看看最終效果圖:
圖一
效果還是很炫的;飯要一口口吃,路要一步步走,這裡我們將整個過程分成幾步來實現
一、實現單擊出現水波紋單圈效果:
圖二
照例來說,還是一個自定義控件,這裡我們直接讓這個控件撐滿整個屏幕(對自定義控件不熟悉的可以參看我之前的一篇文章:Android自定義控件系列二:自定義開關按鈕(一))。觀察這個效果,發現應該需要重寫onTouchEvent和onDraw方法,通過在onTouchEvent中獲取觸摸的坐標,然後以這個坐標值為圓心來繪制我們需要的圖形,這個繪制過程就是調用的onDraw方法。
1、新建一個工程,定義一個WaterWave的類,繼承自View,作為一個自定義控件;在清單文件中將這個自定義控件寫出來,直接填滿父窗體。
2、在WaterWave類中,實現它的兩參構造函數:
package com.example.waterwavedemo.ui; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; public class WaterWave extends View { ... /* * 1、兩參構造函數 */ public WaterWave(Context context, AttributeSet attrs) { super(context, attrs); alpha = 0; radius = 0; initPaint(); } ... }
3、要使用自定義控件,那麼一般都需要指定它的大小,這裡我們由於隻需要其填滿窗體,所以使用默認的onMeasure方法即可:
/** * onMeasure方法,確定控件大小,這裡使用默認的 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
4、將這個自定義圖形畫出來,重寫onDraw方法,在這裡由於我們需要畫一個圈,所以這樣寫:
@Override /** * 畫出需要的圖形的方法,這個方法比較關鍵 */ protected void onDraw(Canvas canvas) { canvas.drawCircle(xDown, yDown, radius, paint); }
其中的參數xDown和yDown是成員變量,代表按下時的x和y坐標,這個坐標所對應的點就是要繪制的圓環的圓心;radius參數也是成員變量,代表要繪制的圓環的半徑;
看到這裡還需要一個paint,是Paint類型的畫筆對象,這裡先將其定義成一個成員變量,由於onDraw方法在第一次自定義控件顯示的時候就會被調用,所以這個paint需要我們在兩參的構造函數中就進行初始化,否則會報出空指針異常;那麼我們這裡另外寫一個initPaint()方法來初始化我們的paint:
/** * 初始化paint */ private void initPaint() { /* * 新建一個畫筆 */ paint = new Paint(); paint.setAntiAlias(true); paint.setStrokeWidth(width); // 設置是環形方式繪制 paint.setStyle(Paint.Style.STROKE); System.out.println(alpha= + alpha); paint.setAlpha(alpha); System.out.println(得到的透明度: + paint.getAlpha()); paint.setColor(Color.RED); }
5、觸摸定時刷新
在onDraw方法之後,我們已經可以畫出這個圓環瞭,但是實際問題是,我們想要實現點擊的時候才在點擊的位置來畫一個圓環,那麼我們肯定需要獲得點擊的時候的坐標xDown和yDown,所以肯定需要重寫onTouchEvent方法,另外我們需要在按下的時候,讓透明度是最不透明(alpha=255),在繪制的過程中,讓圓環的半徑(radius)不斷擴大,同時讓透明度不斷減小,直至完全透明(alpha=0),這個不斷變化的過程又需要每隔一段時間重新刷新狀態和重新繪制圖形,所以我們這裡使用handler來處理:
@Override /** * 觸摸事件的方法 */ public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: radius = 0; alpha = MAX_ALPHA; width = radius / 4; xDown = (int) event.getX(); yDown = (int) event.getY(); handler.sendEmptyMessage(0); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; default: break; } return true; }
可以看到,我們這裡先隻實現瞭ACTION_DOWN裡面的邏輯,在每一個按下的時候將半徑radius設置為0,透明度alpha設置為完全不透明,而寬度也為0,並且獲取按下的x和y坐標,之後就使用handler發送瞭一個空消息,讓handler去實現定時刷新狀態和繪制圖形的工作,我們想讓圓環的透明度alpha撿到0的時候就不再繼續定時自動刷新瞭,否則在每一次handleMessage的時候都先刷新狀態值,然後繪制圖形:
private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case 0: flushState(); invalidate(); if (alpha != 0) { // 如果透明度沒有到0,則繼續刷新,否則停止刷新 handler.sendEmptyMessageDelayed(0, 50); } break; default: break; } } /** * 刷新狀態 */ private void flushState() { radius += 5; alpha -= 10; if (alpha < 0) { alpha = 0; } // System.out.println(alpha= + alpha); width = radius / 4; paint.setAlpha(alpha); paint.setStrokeWidth(width); } };
我們可以看到,在handler中,我們重寫瞭handleMessage方法,在msg.what=0的時候,我們調用flushState()方法來刷新狀態,和invalidate()方法來繪制圖形,,然後使用handler.sendEmptyMessageDelayed(0, 50);來每隔50毫秒重復一次上面的工作;其中invalidate()是Android提供的,而flushState()則需要我們自己來實現;
按照我們的需求,每一次狀態的刷新工作flushState(),我們需要做如下幾件事:
(1)讓半徑增加
(2)讓透明度減少,並設置給paint;
(3)環形的寬度增加,並設置給paint
(4)對於透明度而言,最大值是255,但是這裡如果讓透明度減少到0以下,比如說-1,那麼實際上alpha的值不會是-1,而是255+(-1)=254,所以我們還需要加一個判斷條件,防止alpha<0
/** * 刷新狀態 */ private void flushState() { radius += 5; alpha -= 10; if (alpha < 0) { alpha = 0; } // System.out.println(alpha= + alpha); width = radius / 4; paint.setAlpha(alpha); paint.setStrokeWidth(width); }
6、在兩參的構造函數中添加一些初始化工作:
public WaterWave(Context context, AttributeSet attrs) { super(context, attrs); alpha = 0; radius = 0; initPaint(); }
至此,我們的第一步就基本完成瞭
二、實現多次點擊圓環同時存在,同時刷新效果:
從面圖二中,我們不難發現,不論如何點擊,屏幕上都隻會同時存在一個圓圈的效果,這是因為我們每次點擊的時候,都重新設置瞭圓心,而且所有圓形的參數都是成員變量,都是共享的;不僅如此,如果在上一個圓圈沒有消失的時候,就再次點擊,會讓新出現的圓圈變大的速度大大增加,這是因為使用handler.sendEmptyMessageDelayed(0,50)方法的原因,第二次點擊時會重復觸發這個方法,使得前後兩次點擊的handler.sendEmptyMessageDelayed()重疊生效,讓實際間隔遠遠小於50毫秒,所以刷新速度快瞭很多
那麼我們現在就要解決上面兩個小問題,實現如下圖的效果:
解決這兩個小問題的思路:
1、針對所有水波紋圓圈共享參數的問題:
方法就是新建一個內部類Wave,用於存放每個圓圈的參數,每一個圓圈都對應一個Wave對象,然後在onDraw方法裡面,同時重繪所有的圓圈視圖;那麼這裡就還需要一個List集合waveList,用於存放所有的wave對象,方便遍歷。
2、針對handler.sendEmptyMessageDelayed方法在後續點擊的時候不斷被調用,導致刷新越來越快的問題。
這裡可以設置一個成員變量 boolean isStart;來標志是不是第一次按下;因為我們在第一次按下的時候,肯定是希望開始定時刷新,調用handler.sendEmptyMessageDelayed,讓圓環的狀態不斷變化。但是對於之後的點擊,我們其實隻希望它立刻被刷新一次,並被加入到waveList集合中,而並不需要發送一個handler的信息來調用handler.sendEmptyMessageDelayed。所以在一開始的時候我們將其設置為true,而在第一次點擊時候將其設置為false,那麼在什麼時候將其設置為false呢,這裡牽涉到第三個問題:
3、對於waveList集合而言,如果一直點擊往集合裡面添加Wave對象,那麼無疑會讓這個集合越來越大,這個是我們不希望看到的。
我們希望在圓環的透明度值alpha變為0,也就是完全透明的時候,讓其從waveList中remove掉,讓其能被垃圾回收回收掉,這樣如果點擊幾個點之後停止,點都會自動消失(alpha值減到0),那麼對應的Wave對象也會從waveList被移除,waveList的大小也會變成0,這個時候我們就可以停止handler.sendEmptyMessageDelayed方法繼續被調用,同時可以將isStart重新設為true。那麼isStart何時設為false呢?我們可以在flushState刷新狀態的時候將其設為false,因為刷新狀態的時候表明第一次點擊已經按下瞭。然後在onTouchEvent方法的ACTION_DWON條件下,如果isStart為true才發送handler的消息,這代表第一次點擊,之後再點擊也不會發送而隻是將wave對象添加到waveList中,因為第一次的時候調用flushState已經將isStart置為false瞭。
由於改動較大,代碼如下:
package com.example.waterwavedemo.ui; import java.util.ArrayList; import java.util.Collections; import java.util.List; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; public class WaterWave extends View { /** * 波形的List */ private List waveList; /** * 最大的不透明度,完全不透明 */ private static final int MAX_ALPHA = 255; protected static final int FLUSH_ALL = -1; private boolean isStart = true; // /** // * 按下的時候x坐標 // */ // private int xDown; // /** // * 按下的時候y的坐標 // */ // private int yDown; // /** // * 用來表示圓環的半徑 // */ // private float radius; // private int alpha; /* * 1、兩參構造函數 */ public WaterWave(Context context, AttributeSet attrs) { super(context, attrs); waveList = Collections.synchronizedList(new ArrayList()); } /** * onMeasure方法,確定控件大小,這裡使用默認的 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override /** * 畫出需要的圖形的方法,這個方法比較關鍵 */ protected void onDraw(Canvas canvas) { // 重繪所有圓環 for (int i = 0; i < waveList.size(); i++) { Wave wave = waveList.get(i); canvas.drawCircle(wave.xDown, wave.yDown, wave.radius, wave.paint); } } /** * 初始化paint */ private Paint initPaint(int alpha, float width) { /* * 新建一個畫筆 */ Paint paint = new Paint(); paint.setAntiAlias(true); paint.setStrokeWidth(width); // 設置是環形方式繪制 paint.setStyle(Paint.Style.STROKE); // System.out.println(alpha= + alpha); paint.setAlpha(alpha); // System.out.println(得到的透明度: + paint.getAlpha()); paint.setColor(Color.RED); return paint; } private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case 0: flushState(); invalidate(); if (waveList != null && waveList.size() > 0) { handler.sendEmptyMessageDelayed(0, 50); } break; default: break; } } }; /** * 刷新狀態 */ private void flushState() { for (int i = 0; i < waveList.size(); i++) { Wave wave = waveList.get(i); if (isStart == false && wave.alpha == 0) { waveList.remove(i); wave.paint = null; wave = null; continue; } else if (isStart == true) { isStart = false; } wave.radius += 5; wave.alpha -= 10; if (wave.alpha < 0) { wave.alpha = 0; } wave.width = wave.radius / 4; wave.paint.setAlpha(wave.alpha); wave.paint.setStrokeWidth(wave.width); } } // private Paint paint; // private float width; @Override /** * 觸摸事件的方法 */ public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Wave wave = new Wave(); wave.radius = 0; wave.alpha = MAX_ALPHA; wave.width = wave.radius / 4; wave.xDown = (int) event.getX(); wave.yDown = (int) event.getY(); wave.paint = initPaint(wave.alpha, wave.width); if (waveList.size() == 0) { isStart = true; } System.out.println(isStart= + isStart); waveList.add(wave); // 點擊之後刷洗一次圖形 invalidate(); if (isStart) { handler.sendEmptyMessage(0); } break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; default: break; } return true; } private class Wave { int waveX; int waveY; /** * 用來表示圓環的半徑 */ float radius; Paint paint; /** * 按下的時候x坐標 */ int xDown; /** * 按下的時候y的坐標 */ int yDown; float width; int alpha; } }
三、實現完全效果(點擊和移動,顏色隨機,圓圈大小變化速度)
效果圖就是跟圖一的一樣瞭,主要做幾個小地方:
1、讓onTouchEvent裡面的ACTION_DOWN和ACTION_MOVE響應同樣的事件,實際上就是去掉ACTION_DOWN的break;然後將處理代碼寫到隨後的ACTION_MOVE中去即可
2、新建一個成員變量數組colors,裡面放自己想要的顏色,然後在initPaint方法的設置color的時候,使用paint.setColor(colors[(int) (Math.random() * (colors.length – 1))]);
3、控制波形的變化趨勢,這個看個人愛好,我是這樣做的:在flushState中:
wave.radius += waveList.size() - i; wave.width = (wave.radius / 3); wave.paint.setStrokeWidth(wave.width); // wave.alpha -= 10; if (wave.alpha < 0) { wave.alpha = 0; } // wave.width = wave.radius / 4; wave.paint.setAlpha(wave.alpha);
至此,就完成瞭自定義的水波紋效果瞭。存在的問題就是,如果在模擬器上,快速滑動,會有卡頓,在我的手機Nexus5上,還算流暢,應該跟內存無關,後續可能還會做一些優化。