自定義絢麗水波紋效果

今天我們來利用Android自定義控件實現一個比較有趣的效果:滑動水波紋。先來看看最終效果圖:

 

\

圖一

 

效果還是很炫的;飯要一口口吃,路要一步步走,這裏我們將整個過程分成幾步來實現

 

一、實現單擊出現水波紋單圈效果:

 

\

圖二

 

照例來說,還是一個自定義控件,這裏我們直接讓這個控件撐滿整個屏幕(對自定義控件不熟悉的可以參看我之前的一篇文章:Android自定義控件系列二:自定義開關按鈕(一))。觀察這個效果,發現應該需要重寫onTouchEvent和onDraw方法,通過在onTouchEvent中獲取觸摸的座標,然後以這個座標值爲圓心來繪製我們需要的圖形,這個繪製過程就是調用的onDraw方法。

 

1、新建一個工程,定義一個WaterWave的類,繼承自View,作爲一個自定義控件;在清單文件中將這個自定義控件寫出來,直接填滿父窗體。

 

2、在WaterWave類中,實現它的兩參構造函數:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
packagecom.example.waterwavedemo.ui;
 
 
importandroid.content.Context;
importandroid.graphics.Canvas;
importandroid.graphics.Color;
importandroid.graphics.Paint;
importandroid.os.Handler;
importandroid.os.Message;
importandroid.util.AttributeSet;
importandroid.view.MotionEvent;
importandroid.view.View;
 
 
publicclass WaterWave extendsView {
    ...
    /*
     * 1、兩參構造函數
     */
    publicWaterWave(Context context, AttributeSet attrs) {
        super(context, attrs);
        alpha = 0;
        radius = 0;
        initPaint();
    }
    ...
}



3、要使用自定義控件,那麼一般都需要指定它的大小,這裏我們由於只需要其填滿窗體,所以使用默認的onMeasure方法即可:

 

 

?
1
2
3
4
5
6
7
8
/**
     * onMeasure方法,確定控件大小,這裏使用默認的
     */
    @Override
    protectedvoid onMeasure(intwidthMeasureSpec, intheightMeasureSpec) {
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }


 

 

4、將這個自定義圖形畫出來,重寫onDraw方法,在這裏由於我們需要畫一個圈,所以這樣寫:

 

 

?
1
2
3
4
5
6
7
8
@Override
/**
 * 畫出需要的圖形的方法,這個方法比較關鍵
 */
protectedvoid onDraw(Canvas canvas) {
    canvas.drawCircle(xDown, yDown, radius, paint);
 
}


 

 

其中的參數xDown和yDown是成員變量,代表按下時的x和y座標,這個座標所對應的點就是要繪製的圓環的圓心;radius參數也是成員變量,代表要繪製的圓環的半徑;

 

看到這裏還需要一個paint,是Paint類型的畫筆對象,這裏先將其定義成一個成員變量,由於onDraw方法在第一次自定義控件顯示的時候就會被調用,所以這個paint需要我們在兩參的構造函數中就進行初始化,否則會報出空指針異常;那麼我們這裏另外寫一個initPaint()方法來初始化我們的paint:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 初始化paint
 */
privatevoid initPaint() {
    /*
     * 新建一個畫筆
     */
    paint = newPaint();
 
    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來處理:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
    /**
     * 觸摸事件的方法
     */
    publicboolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
 
        switch(event.getAction()) {
        caseMotionEvent.ACTION_DOWN:
            radius = 0;
            alpha = MAX_ALPHA;
            width = radius / 4;
            xDown = (int) event.getX();
            yDown = (int) event.getY();
 
            handler.sendEmptyMessage(0);
 
            break;
        caseMotionEvent.ACTION_MOVE:
 
            break;
        caseMotionEvent.ACTION_UP:
 
            break;
 
        default:
            break;
        }
 
        returntrue;
    }


 

 

可以看到,我們這裏先只實現了ACTION_DOWN裏面的邏輯,在每一個按下的時候將半徑radius設置爲0,透明度alpha設置爲完全不透明,而寬度也爲0,並且獲取按下的x和y座標,之後就使用handler發送了一個空消息,讓handler去實現定時刷新狀態和繪製圖形的工作,我們想讓圓環的透明度alpha撿到0的時候就不再繼續定時自動刷新了,否則在每一次handleMessage的時候都先刷新狀態值,然後繪製圖形:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
privateHandler handler = newHandler() {
 
    @Override
    publicvoid handleMessage(Message msg) {
        super.handleMessage(msg);
        switch(msg.what) {
        case0:
            flushState();
 
            invalidate();
 
            if(alpha != 0) {
                // 如果透明度沒有到0,則繼續刷新,否則停止刷新
                handler.sendEmptyMessageDelayed(0,50);
            }
 
            break;
 
        default:
            break;
        }
    }
 
    /**
     * 刷新狀態
     */
    privatevoid 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

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
         * 刷新狀態
         */
        privatevoid 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、在兩參的構造函數中添加一些初始化工作:

 

 

?
1
2
3
4
5
6
publicWaterWave(Context context, AttributeSet attrs) {
        super(context, attrs);
        alpha = 0;
        radius = 0;
        initPaint();
    }
demo

 

 

至此,我們的第一步就基本完成了

二、實現多次點擊圓環同時存在,同時刷新效果:

 

從面圖二中,我們不難發現,不論如何點擊,屏幕上都只會同時存在一個圓圈的效果,這是因爲我們每次點擊的時候,都重新設置了圓心,而且所有圓形的參數都是成員變量,都是共享的;不僅如此,如果在上一個圓圈沒有消失的時候,就再次點擊,會讓新出現的圓圈變大的速度大大增加,這是因爲使用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了。

由於改動較大,代碼如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
packagecom.example.waterwavedemo.ui;
 
importjava.util.ArrayList;
importjava.util.Collections;
importjava.util.List;
 
importandroid.content.Context;
importandroid.graphics.Canvas;
importandroid.graphics.Color;
importandroid.graphics.Paint;
importandroid.os.Handler;
importandroid.os.Message;
importandroid.util.AttributeSet;
importandroid.view.MotionEvent;
importandroid.view.View;
 
publicclass WaterWave extendsView {
 
    /**
     * 波形的List
     */
    privateList<wave> waveList;
 
    /**
     * 最大的不透明度,完全不透明
     */
    privatestatic final int MAX_ALPHA = 255;
 
    protectedstatic final int FLUSH_ALL = -1;
 
    privateboolean isStart = true;
 
    // /**
    // * 按下的時候x座標
    // */
    // private int xDown;
    // /**
    // * 按下的時候y的座標
    // */
    // private int yDown;
    // /**
    // * 用來表示圓環的半徑
    // */
    // private float radius;
    // private int alpha;
 
    /*
     * 1、兩參構造函數
     */
    publicWaterWave(Context context, AttributeSet attrs) {
        super(context, attrs);
        waveList = Collections.synchronizedList(newArrayList<wave>());
    }
 
    /**
     * onMeasure方法,確定控件大小,這裏使用默認的
     */
    @Override
    protectedvoid onMeasure(intwidthMeasureSpec, intheightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
 
    @Override
    /**
     * 畫出需要的圖形的方法,這個方法比較關鍵
     */
    protectedvoid onDraw(Canvas canvas) {
        // 重繪所有圓環
        for(inti = 0; i < waveList.size(); i++) {
            Wave wave = waveList.get(i);
            canvas.drawCircle(wave.xDown, wave.yDown, wave.radius, wave.paint);
        }
 
    }
 
    /**
     * 初始化paint
     */
    privatePaint initPaint(intalpha, floatwidth) {
        /*
         * 新建一個畫筆
         */
        Paint paint = newPaint();
 
        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);
        returnpaint;
    }
 
    privateHandler handler = newHandler() {
 
        @Override
        publicvoid handleMessage(Message msg) {
            super.handleMessage(msg);
            switch(msg.what) {
            case0:
                flushState();
 
                invalidate();
 
                if(waveList != null&& waveList.size() > 0) {
                    handler.sendEmptyMessageDelayed(0,50);
                }
 
                break;
 
            default:
                break;
            }
        }
 
    };
 
    /**
     * 刷新狀態
     */
    privatevoid flushState() {
        for(inti = 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;
            }elseif (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
    /**
     * 觸摸事件的方法
     */
    publicboolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
 
        switch(event.getAction()) {
        caseMotionEvent.ACTION_DOWN:
            Wave wave = newWave();
            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;
        caseMotionEvent.ACTION_MOVE:
 
            break;
        caseMotionEvent.ACTION_UP:
 
            break;
 
        default:
            break;
        }
 
        returntrue;
    }
 
    privateclass Wave {
        intwaveX;
        intwaveY;
        /**
         * 用來表示圓環的半徑
         */
        floatradius;
        Paint paint;
        /**
         * 按下的時候x座標
         */
        intxDown;
        /**
         * 按下的時候y的座標
         */
        intyDown;
        floatwidth;
        intalpha;
    }
 
}
</wave></wave>

附上例子:demo2

三、實現完全效果(點擊和移動,顏色隨機,圓圈大小變化速度)

效果圖就是跟圖一的一樣了,主要做幾個小地方:

1、讓onTouchEvent裏面的ACTION_DOWN和ACTION_MOVE響應同樣的事件,實際上就是去掉ACTION_DOWN的break;然後將處理代碼寫到隨後的ACTION_MOVE中去即可

 

2、新建一個成員變量數組colors,裏面放自己想要的顏色,然後在initPaint方法的設置color的時候,使用paint.setColor(colors[(int) (Math.random() * (colors.length - 1))]);

 例如:int[] colors=new int[]{Color.RED,Color.BLUE,Color.GREEN,Color.YELLOW};

3、控制波形的變化趨勢,這個看個人愛好,我是這樣做的:在flushState中:

 

 

?
1
2
3
4
5
6
7
8
9
10
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上,還算流暢,應該跟內存無關,後續可能還會做一些優化。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章