本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
最近學(xué)習(xí)了一些自定義控件的知識(shí),想著趁熱多做些練習(xí)來鞏固,上周自定義了一個(gè)等級(jí)進(jìn)度條,是一個(gè)自定義View,這周就換一個(gè)類型,做一個(gè)自定義的ViewGroup。這周自定義ViewGroup的是一個(gè)鎖屏控件,效果如下:

效果分析:
仔細(xì)分析效果圖發(fā)現(xiàn),鎖屏控件需要繪制的有三個(gè)部分,分別是:
1.圖案點(diǎn),圖案點(diǎn)有四種狀態(tài),分別是默認(rèn)、選中、正確和錯(cuò)誤




2.圖案點(diǎn)之間的連線
連線會(huì)根據(jù)1中點(diǎn)的狀態(tài)改變發(fā)生顏色上的變化

3.懸空線段
就是圖案點(diǎn)和懸空點(diǎn)之間的線段

整體思路:
1.自定義一個(gè)LockScreenView來表示圖案點(diǎn),LockScreenView有四種狀態(tài)
2.自定義一個(gè)LockScreenViewGroup,在onMeasure中獲取到寬度以后(根據(jù)寬度算圖案點(diǎn)之間的間距),動(dòng)態(tài)地將LockScreenView添加進(jìn)來
3.在LockScreenViewGroup的onTouchEvent中消耗觸摸事件,根據(jù)觸摸點(diǎn)的軌跡來更新LockScreenView、圖案點(diǎn)連線和懸空線段
實(shí)現(xiàn):
1.自定義LockScreenView
由于沒有和這個(gè)自定義View比較類似的原生控件,因此自定義的時(shí)候直接繼承自View
首先,需要的屬性通過構(gòu)造函數(shù)傳入:


View的狀態(tài)用一個(gè)枚舉類型來表示:

View的狀態(tài)通過暴露一個(gè)方法給LockScreenViewGroup來進(jìn)行設(shè)置。
在onDraw方法中判斷類型,進(jìn)行繪制:

這里在選中時(shí)用屬性動(dòng)畫做了一個(gè)放大效果,在下次恢復(fù)正常的時(shí)候要將大小恢復(fù)回去:

在LockScreenViewGroup中,我將LockScreenView的寬高設(shè)置為wrap_content,因此需要在onMeasure方法做一些特殊的處理,至于為什么要做特殊處理,在上一篇博文《等級(jí)進(jìn)度條》中已經(jīng)提到過了。

2.自定義LockScreenViewGroup
為了方便確定子View的位置,LockScreenViewGroup繼承自RelativeLayout
在xml中賦予了如下屬性:

其中itemCount表示一行有幾個(gè)LockScreenView,其它屬性都已經(jīng)提到過了
在構(gòu)造函數(shù)中解析xml中的自定義屬性:

在onMeasure方法中,獲取到LockScreenViewGroup的寬以后,算出LockScreenView之間的間隙,并動(dòng)態(tài)地將LockScreenView添加進(jìn)來(每個(gè)LockScreenView添加進(jìn)來的時(shí)候,設(shè)置id作為唯一標(biāo)識(shí),后面在判斷圖案是否正確時(shí)會(huì)用到):


這里有兩個(gè)地方需要注意一下:
1.LockScreenView的寬不能用getMeasuredWidth方法來獲取,因?yàn)檫@里只是把LockScreenView創(chuàng)建了出來,還沒有對(duì)它進(jìn)行測(cè)量,故通過getMeasuredWidth方法只能得到0,這里直接把LockScreenView中大圓的直徑當(dāng)作它的寬(因?yàn)檫@里動(dòng)態(tài)添加的時(shí)候用了wrap_content, 并且沒有設(shè)padding)
2.重寫onMeasure方法的時(shí)候不能把super.onMeasure方法刪掉,因?yàn)檫@里面會(huì)進(jìn)行子View寬高的測(cè)量,刪了子View就畫不出來了
觸摸事件的消耗在onTouchEvent中處理(在這個(gè)案例中也可以在dispatchTouchEvent方法中處理,因?yàn)樽覸iew的狀態(tài)由LockScreenViewGroup告訴它了,子View不需要處理觸摸事件)。
在onTouchEvent方法中對(duì)Down、Move、Up三種不同的觸摸狀態(tài)分別做了處理。
(1)首先,在Down狀態(tài)時(shí),需要對(duì)之前的狀態(tài)做一些重置:

其中,mCurrentViews用來保存當(dāng)前選中的LockScreenView的id,mCurrentPath用來保存圖像點(diǎn)間線段的路徑,skyStartX、skyStartY分別是懸空線段起始的x和y。
(2)在Move狀態(tài)時(shí),判斷是否在LockScreenView區(qū)域,如果在某個(gè)LockScreenView區(qū)域且這個(gè)LockScreenView之前沒有被選中,則將這個(gè)LockScreenView設(shè)置為選中狀態(tài)。另外在onMove中還做了圖案點(diǎn)間線段路徑和懸空線段起點(diǎn)和終點(diǎn)(mTempX、mTempY)的更新,懸空線段的起點(diǎn)就是上一個(gè)被選中的LockScreenView的中心點(diǎn)。

(3)在Up狀態(tài)時(shí),根據(jù)答案的正確與否,對(duì)LockScreenView設(shè)置不同的狀態(tài),并且對(duì)懸空線段起始點(diǎn)進(jìn)行重置

在onTouchEvent方法最后會(huì)調(diào)用invalidate方法對(duì)視圖進(jìn)行重繪,這時(shí)會(huì)調(diào)用dispatchDraw方法進(jìn)行子View的繪制。
在dispatchDraw方法中進(jìn)行圖像點(diǎn)間的線段路徑以及懸空線段的繪制:

這里要注意,在重寫dispatchDraw方法時(shí),不能把super.dispatchDraw方法刪掉,因?yàn)檫@里會(huì)繪制LockScreenViewGroup的子View(即,LockScreenView們),如果刪了,動(dòng)態(tài)添加的LockScreenView就會(huì)顯示不出來(重寫的時(shí)候不小心刪了,排查好久才發(fā)現(xiàn)是這里的問題,都是淚orz)
最后附上github源碼地址:LockScreenVIew