系統(tǒng)的控件有UIButton募强、UISlider、UITextField等崇摄,它們共有的特點是能夠響應(yīng)觸摸事件擎值,并作出相應(yīng)的變化。
這些系統(tǒng)控件都繼承自UIControl逐抑,顧名思義鸠儿,UIControl能夠處理各種觸摸事件,它的頭文件中列出了所有能夠處理的事件厕氨。
創(chuàng)建自定義控件的基本思路是:先在-drawRect:
方法中畫出Normal狀態(tài)下的樣子进每,用戶觸發(fā)觸摸事件時,根據(jù)觸摸事件畫出相應(yīng)的圖形命斧,這時控件的樣子會隨觸摸事件改變田晚,調(diào)用-sendActionsForControlEvents:
方法向外部發(fā)送觸摸事件。
現(xiàn)在制作一個如下圖的自定義控件国葬。
一肉瓦、繼承UIControl遭京,重寫方法
UIControl是抽象類,只提供API接口泞莉,并不能直接使用哪雕。所以先創(chuàng)建一個類,繼承自UIControl鲫趁,然后重寫它的一些方法:
- (void)drawRect:(CGRect)rect;
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)cancelTrackingWithEvent:(UIEvent *)event;
-drawRect:
方法中繪制控件斯嚎,控件就是這個方法畫出來的
-beginTrackingWithTouch:withEvent:
是監(jiān)測到觸摸事件并開始追蹤
-continueTrackingWithTouch:withEvent:
是持續(xù)監(jiān)測觸摸事件
-endTrackingWithTouch:withEvent:
是監(jiān)測結(jié)束
-cancelTrackingWithEvent:
是監(jiān)測到取消觸摸事件
-drawRect:
方法負責(zé)圖形顯示,另外三個方法負責(zé)交互挨厚。交互的方法被用戶觸發(fā)之后堡僻,調(diào)用-setNeedsDisplay
來間接調(diào)用-drawRect:
方法進行繪圖。注意不要直接手動調(diào)用drawRect疫剃,要通過setNeedsDisplay方法讓系統(tǒng)異步調(diào)用drawRect钉疫,否則可能會發(fā)生錯誤。
二巢价、在-drawRect:中繪圖
控件的最底部是三個白色半透明的圓弧牲阁,上面是白色不透明的圓弧,每個圓弧上有前后兩個標簽壤躲,白色不透明圓弧隨標簽的滑動而改變城菊,標簽外側(cè)是說明文字。其實還是蠻好畫的碉克,不算很復(fù)雜凌唬。
- (void)drawRect:(CGRect)rect {
for (NSInteger i = 0; i < _sectors.count; i ++) {
// 取出三個弧形區(qū)域?qū)ο? Sector *sector = [_sectors objectAtIndex:i];
CGContextRef context = UIGraphicsGetCurrentContext();
// 畫圓弧
CGContextSetLineWidth(context, 25);
UIColor *bottomColor = [UIColor colorWithWhite:1 alpha:0.5];
UIColor *topColor = [UIColor colorWithWhite:1 alpha:1];
[bottomColor setStroke];
CGContextAddArc(context, sector.centerPoint.x, sector.centerPoint.y, Circle_Width, sector.startAngle, sector.startAngle+0.6*M_PI, 0);
CGContextStrokePath(context);
[topColor setStroke];
CGContextAddArc(context, sector.centerPoint.x, sector.centerPoint.y, Circle_Width, sector.beginAngle, sector.endAngle, 0);
CGContextStrokePath(context);
// 畫直線
[sector.color setStroke];
CGContextSetLineWidth(context, 2.0);
CGPoint beginSmallPoint = polarToDecart(sector.centerPoint, Circle_Width-15, sector.beginAngle);
CGPoint beginBigPoint = polarToDecart(sector.centerPoint, Circle_Width+15, sector.beginAngle);
CGContextMoveToPoint(context, beginSmallPoint.x, beginSmallPoint.y);
CGContextAddLineToPoint(context, beginBigPoint.x, beginBigPoint.y);
CGContextStrokePath(context);
CGPoint endSmallPoint = polarToDecart(sector.centerPoint, Circle_Width-15, sector.endAngle);
CGPoint endBigPoint = polarToDecart(sector.centerPoint, Circle_Width+15, sector.endAngle);
CGContextMoveToPoint(context, endSmallPoint.x, endSmallPoint.y);
CGContextAddLineToPoint(context, endBigPoint.x, endBigPoint.y);
CGContextStrokePath(context);
// 畫扇形
CGContextSetLineWidth(context, 2.0);
[sector.color setStroke];
[sector.color setFill];
CGContextMoveToPoint(context, beginBigPoint.x, beginBigPoint.y);
CGContextAddArc(context, beginBigPoint.x, beginBigPoint.y, 10, sector.beginAngle-0.2*M_PI, sector.beginAngle+0.2*M_PI, 0);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFillStroke);
CGContextMoveToPoint(context, endBigPoint.x, endBigPoint.y);
CGContextAddArc(context, endBigPoint.x, endBigPoint.y, 10, sector.endAngle-0.2*M_PI, sector.endAngle+0.2*M_PI, 0);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFillStroke);
NSString *startString = @"end";
NSString *endString = @"start";
// 直角坐標轉(zhuǎn)極坐標
PolarCoordinate strBeginPolar = decartToPolar(sector.centerPoint, sector.beginPoint);
// 極坐標轉(zhuǎn)直角坐標
CGPoint strBeginPoint = polarToDecart(sector.centerPoint, strBeginPolar.radius+50, strBeginPolar.angle);
PolarCoordinate strEndPolar = decartToPolar(sector.centerPoint, sector.endPoint);
CGPoint strEndPoint = polarToDecart(sector.centerPoint, strEndPolar.radius+50, strEndPolar.angle);
// 畫文字
[self drawString:startString
withFont:[UIFont systemFontOfSize:17]
andColor:[UIColor whiteColor]
andCenter:strBeginPoint
andTag:sector.tag
radius:strBeginPolar.radius+50];
[self drawString:endString
withFont:[UIFont systemFontOfSize:17]
andColor:[UIColor whiteColor]
andCenter:strEndPoint
andTag:sector.tag
radius:strEndPolar.radius+50];
}
}
這樣控件就畫好了,對象初始化之后會自動調(diào)用一次drawRect繪制出來漏麦。
三客税、處理觸摸事件
控件畫出來了,接下來就要處理觸摸事件撕贞,這個自定義控件的觸摸事件類似UISlider更耻,是滑動式的事件。
控件有三個主要區(qū)域麻掸,每個區(qū)域有兩個滑動標簽酥夭,只要捕獲到用戶觸摸區(qū)域在滑動標簽周圍赐纱,就說明用戶要開始滑動標簽了脊奋,然后讓標簽跟隨用戶的觸摸點移動,這個自定義控件就“活”了疙描。
1.如何知道用戶的觸摸點是否在滑動標簽周圍呢诚隙?
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
方法最先捕獲到用戶觸摸事件,touch
對象包含了觸摸區(qū)域起胰,通過[touch locationInView:self]
方法可以獲取觸摸點的坐標久又。
簡單的自定義控件可以通過CGRectContainsPoint(rect, point)
判斷point是否在rect內(nèi)巫延,但這個控件就不行,因為兩個區(qū)域之間的距離太近地消,標簽的frame會有部分區(qū)域重合炉峰。這里我使用的方法是創(chuàng)建一個梯形貝塞爾路徑,正好把標簽包含住而又不重合脉执,當然代碼也多了不少疼阔。效果如下,是不是很贊半夷?
使用方法[beginPath containsPoint:touchPoint];
來判斷觸摸區(qū)域是否在滑動標簽周圍婆廊,是的話就返回YES繼續(xù)處理,否則返回NO終止處理巫橄。
2.繼續(xù)處理觸摸事件
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
是繼續(xù)處理的方法淘邻。
用戶觸摸的位置是任意的,但我們的控件卻是圓的湘换,所以這里就需要一個函數(shù)把觸摸點所在的直角坐標轉(zhuǎn)換成極坐標宾舅,這樣就能得到觸摸點的角度,得到角度之后立即繪制該角度對應(yīng)的圖像枚尼。比如滑動標簽1原始位置是0度贴浙,用戶觸摸的位置是20度,就立即調(diào)用setNeedsDisplay方法把滑動標簽1繪制到20度的位置署恍。
在- continueTrackingWithTouch:withEvent:
方法中崎溃,用戶滑動的坐標基本是連續(xù)的,我們不斷地調(diào)用setNeedsDisplay盯质,系統(tǒng)就不斷地重繪袁串,這就產(chǎn)生了用戶滑到哪里,滑動標簽就跟隨到哪里的效果呼巷。
手動調(diào)用[self sendActionsForControlEvents:UIControlEventValueChanged];
方法囱修,就能在每次改變值之后向外發(fā)通知,我們在外部就能通過[custom addTarget:self action:@selector(sectorValueChanged:) forControlEvents:UIControlEventValueChanged];
來實時獲取值了王悍。
3.觸摸事件結(jié)束
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
表示用戶離開屏幕破镰,不再進行觸摸。
到這里压储,自定義控件基本就創(chuàng)建好了鲜漩,當然創(chuàng)建自定義控件是要使用的,所以得對外暴露一些接口集惋,比如設(shè)置顏色孕似、角度、數(shù)值等刮刑。
結(jié)束了嗎喉祭?還沒有养渴。
這種直接繪制的方法太粗暴,下面用一種優(yōu)雅的方式創(chuàng)建它泛烙。
四理卑、優(yōu)雅地創(chuàng)建自定義控件
最好的方法是仿照蘋果,將顯示與交互分開蔽氨,CALayer負責(zé)圖形顯示傻工,控件本身負責(zé)交互。重繪代碼分離到CALayer中孵滞,交互代碼留在UIControl中中捆,代碼結(jié)構(gòu)清晰、邏輯明了坊饶。
這個自定義控件的三個區(qū)域其實是一個樣子的泄伪,只是位置和顏色不同,上面的方法是一次性繪制三個區(qū)域匿级,代碼比較臃腫蟋滴,如果使用CALayer繪制一個區(qū)域的Layer,使用的時候創(chuàng)建三個分別放在不同位置痘绎,這樣就簡單多了津函。
1.創(chuàng)建UIControl子類,重寫交互方法
與上面一樣孤页,先創(chuàng)建一個類繼承于UIControl尔苦,重寫begin/continue/end/cancel四個方法,主要在begin和continue中處理觸摸邏輯與數(shù)據(jù)計算行施,有些復(fù)雜的控件這里的判斷代碼可能寫起來比較困難允坚。
2.創(chuàng)建Layer,繪制控件
continue方法中蛾号,數(shù)據(jù)處理完畢之后手動調(diào)用layer的-setNeedsDisplay
方法進行重繪稠项,重繪的代碼與上面類似。CALayer中的重繪方法與UIView不同鲜结,它是在- (void)drawInContext:(CGContextRef)ctx
中進行的展运。
要注意的是,重繪需要的所有參數(shù)都是由自定義控件傳過來的精刷,比如半徑拗胜、顏色、寬度等贬养,正是這些數(shù)據(jù)將兩個類關(guān)聯(lián)了起來挤土,有必要重寫這些參數(shù)的setter方法琴庵,發(fā)現(xiàn)修改任意參數(shù)的時候就進行重繪误算。
自定義控件就寫這么多吧仰美,具體的源碼請參考我的GitHub。