下個(gè)周又要投入到公司項(xiàng)目的開發(fā)中去了,今天抽空寫一個(gè)類似于桌面懸停的菜單.當(dāng)移到底部的時(shí)候效果看起來有點(diǎn)像一個(gè)小烏龜哦!O(∩_∩)O~.還是"花瓣"菜單好聽些.
先來看一下效果
是不是覺得挺好玩的呀.
通過這篇文章你可以學(xué)到:
- 1.系統(tǒng)UITableView的部分設(shè)計(jì)思想
- 2.自定義控件常用設(shè)計(jì)思路
- 3.動(dòng)畫的具體使用
- 4.手勢(shì)的具體使用
- 4.裝逼一點(diǎn),良好的代碼風(fēng)格
- 5......
開始碼
- 隨機(jī)顏色
為了快速區(qū)分視圖,這里用了隨機(jī)顏色來區(qū)分,生成隨機(jī)顏色的方式比較多.
常見的獲取方法為宏如下:
#define RandomColor [UIColor colorWithRed:arc4random_uniform(255)/255.0 green:arc4random_uniform(255)/255.0 blue:arc4random_uniform(255)/255.0 alpha:1]
通過類方法實(shí)現(xiàn):
+ (UIColor *)randomColor{
static BOOL seed = NO;
if (!seed) {
seed = YES;
srandom((uint)time(NULL));
}
CGFloat red = (CGFloat)random()/(CGFloat)RAND_MAX;
CGFloat green = (CGFloat)random()/(CGFloat)RAND_MAX;
CGFloat blue = (CGFloat)random()/(CGFloat)RAND_MAX;
return [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];//alpha為1.0,顏色完全不透明
}
基本設(shè)計(jì)
我們?cè)谧龉部丶臅r(shí)候,可以把要做的部分捋一捋.其實(shí)我們?cè)谧隹蛻舳碎_發(fā)可以類比網(wǎng)頁(yè)的開發(fā).做的事情無非就是拿到服務(wù)端給的數(shù)據(jù),通過不同的方式展示出來.其中就涉及到:
- 1.數(shù)據(jù):從客戶端來看一般就是服務(wù)端給的json格式的數(shù)據(jù)
- 2.樣式:從客戶端開發(fā)來看就是設(shè)置各個(gè)控件的各種屬性
- 3.交互:
我暫且把這三樣映射到UITableView上
數(shù)據(jù)對(duì)應(yīng)著DataSource代理,樣式對(duì)應(yīng)著我們拿到數(shù)據(jù)之后自定義的cell不同類型(其實(shí)就是設(shè)置不同屬性為不同值),交互對(duì)應(yīng)著Delegate代理.
接下來我們也仿照則TabelView的代理寫
系統(tǒng)TableView的DataSource代理
@protocol UITableViewDataSource<NSObject>
@required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@optional
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; // Default is 1 if not implemented
- (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section; // fixed font style. use custom view (UILabel) if you want something different
- (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section;
// Editing
// Individual rows can opt out of having the -editing property set for them. If not implemented, all rows are assumed to be editable.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath;
// Moving/reordering
// Allows the reorder accessory view to optionally be shown for a particular row. By default, the reorder control will be shown only if the datasource implements -tableView:moveRowAtIndexPath:toIndexPath:
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath;
// Index
- (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView __TVOS_PROHIBITED; // return list of section titles to display in section index view (e.g. "ABCD...Z#")
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index __TVOS_PROHIBITED; // tell table which section corresponds to section title/index (e.g. "B",1))
// Data manipulation - insert and delete support
// After a row has the minus or plus button invoked (based on the UITableViewCellEditingStyle for the cell), the dataSource must commit the change
// Not called for edit actions using UITableViewRowAction - the action's handler will be invoked instead
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;
// Data manipulation - reorder / moving support
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;
@end
當(dāng)然我們也沒必要把系統(tǒng)的代理一個(gè)一個(gè)仿照則寫完,只要自己能夠理解到如何根據(jù)系統(tǒng)API的設(shè)計(jì)思想來設(shè)計(jì)自己寫的代碼就行了.
自己設(shè)計(jì)的DataSource代理
@protocol XLCircleMenuDataSource <NSObject>
@required
- (NSInteger)numberOfCircleViewForCircleMenu:(XLCircleMenu *)circleMenu;
- (UIButton *)circleMenu:(XLCircleMenu *)circleMenu circleViewAtIndex:(NSInteger)index;
@optional
- (CGFloat)lengthForCircleMenu:(XLCircleMenu *)circleMenu;
- (UIView *)centerViewForCircleMenu:(XLCircleMenu *)circleMenu;
@end
@protocol XLCircleMenuDelegate <NSObject>
@optional
- (void)circleMenu:(XLCircleMenu *)circleMenu didClickCircleView:(UIButton *)circleView;
@end
注釋我就沒有加了,因?yàn)镺C最好的就是見名知意.
設(shè)計(jì)類
我們?cè)谠O(shè)計(jì)類的時(shí)候,做得比較好的,需要考慮屬性的讀寫情況,一般只把需要暴露給外部知道的才暴露出去.
然后在為類添加屬性的時(shí)候,需要考慮界面和功能,界面和功能需要在寫代碼之前就應(yīng)該清楚的.舉個(gè)例子:
- 1.具體有多少個(gè)可點(diǎn)的小圓,應(yīng)該通過代理來傳遞的,并且小圓的個(gè)數(shù)應(yīng)該不止在一個(gè)地方用到,所以可以定義為屬性,而且中間有一個(gè)大圓也是通過代理傳遞的,也需要定義一個(gè)屬性來接收.于是可以定義出兩個(gè)屬性.
有哪些屬性我們還可以直接從功能和界面上直接去思考.
- 2.根據(jù)上面的分析依次考慮我們界面上的元素和我們需要控制的屬性.大致定義出了如下屬性(實(shí)現(xiàn)的思路很多,不一定非要這樣定義)
@property (nonatomic, weak) id<XLCircleMenuDataSource> dataSource;
@property (nonatomic, weak) id<XLCircleMenuDelegate> delegate;
@property (nonatomic, assign, readonly) CGPoint centerPoint;
@property (nonatomic, assign, readonly) CGFloat menuLength;
@property (nonatomic, assign, readonly) NSInteger numberOfCircleView;
@property (nonatomic, strong, readonly) UIView *centerCircleView;
@property (nonatomic, strong, readonly) UIView *circleMenuView;
- 2.來看一下需要進(jìn)行哪些操作吧
首先肯定是顯示和隱藏了,如果考慮得多一點(diǎn),我們可以在顯示或者隱藏之后做一個(gè)回調(diào)給使用則
者.
然后就是點(diǎn)擊的各種處理,在定義代理的時(shí)候,我們已經(jīng)仿照系統(tǒng)的TableView的Delegate寫了一個(gè)代理了.所以點(diǎn)擊操作可以直接通過代理去處理
簡(jiǎn)單一點(diǎn)來說初始化的話,我們就讓使用者把需要的參數(shù)都傳入進(jìn)來吧.最終設(shè)計(jì)出的方法如下:
- (instancetype)initFromPoint:(CGPoint)centerPoint
withDataSource:(id<XLCircleMenuDataSource>)dataSource
andDelegate:(id<XLCircleMenuDelegate>)delegate;
- (void)showMenu;
- (void)showMenuWithCompletion:(void(^)()) completion;
- (void)closeMenu;
- (void)closeMenuWithCompletion:(void(^)()) completion;
到目前為止整個(gè)類的架子基本就打好了.
類的實(shí)現(xiàn)
現(xiàn)在該去具體實(shí)現(xiàn)我們的設(shè)計(jì)了
第一步定義屬于的私有屬性
第二步開始寫方法吧
- 初始化方法
- 子視圖的創(chuàng)建
- 手勢(shì)添加
- 實(shí)現(xiàn)動(dòng)畫
接下來把用到的主要技術(shù)和方式
拖拽的是實(shí)現(xiàn)
視圖的拖拽是通過UITapGestureRecognizer實(shí)現(xiàn)的這一章關(guān)于iOS手勢(shì)相關(guān)的介紹可以參考一下這篇文章:
iOS手勢(shì)識(shí)別
- 添加手勢(shì)到指定視圖,設(shè)置手勢(shì)代理,根據(jù)需要特殊處理
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(closeCircelMenu:)];
[self addGestureRecognizer:tapGesture];
tapGesture.delegate = self;
這里判斷如果點(diǎn)擊的是button,則不用接收了
#pragma mark - UIGestureRecognizerDelegate
-(BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch{
BOOL should = YES;
if([touch.view isKindOfClass:[UIButton class]]){
should = NO;
}
return should;
}
下面是就是拖拽部分的代碼,用到的是transform(放射變換)
一旦移動(dòng),就改變視圖的frame
if ((panGesture.state == UIGestureRecognizerStateChanged) || (panGesture.state == UIGestureRecognizerStateEnded)) {
CGPoint translation = [panGesture translationInView:self];
CGRect radialMenuRect = self.circleMenuView.frame;
radialMenuRect.origin.x += translation.x;
radialMenuRect.origin.y += translation.y;
self.circleMenuView.frame = radialMenuRect;
[self placeRadialMenuElementsAnimated:NO];
[panGesture setTranslation:CGPointZero inView:self];
}
調(diào)用代理的時(shí)間
一般在設(shè)計(jì)代理返回參數(shù)的時(shí)候都會(huì)設(shè)計(jì)一個(gè)屬性用來保存代理返回的參數(shù),比如:
_menuLength = 50;
if(self.dataSource && [self.dataSource respondsToSelector:@selector(lengthForCircleMenu:)]){
_menuLength = [self.dataSource lengthForCircleMenu:self];
}
_numberOfCircleView = [self.dataSource numberOfCircleViewForCircleMenu:self];
這里就通過是否有代理來確定屬性的值,當(dāng)然如果代理是必須的就沒必要去判斷了(respondsToSelector),相當(dāng)于通過代理來給屬性賦值.
當(dāng)我們想傳遞事件給代理的時(shí)候,可以通過添加事件給子視圖,然后代理出去,如下:
UIButton *element = [self.dataSource circleMenu:self circleViewAtIndex:i];
if(self.maxW < element.frame.size.width) {
self.maxW = element.frame.size.width;
}else {
}
element.userInteractionEnabled = YES;
element.alpha = 0;
element.tag = i;
[element addTarget:self
action:@selector(didTapButton:)
forControlEvents:UIControlEventTouchUpInside];
[self.elementsArray addObject:element];
在處理事件的時(shí)候調(diào)用代理
-(void)didTapButton:(UIButton *)sender {
[self.delegate circleMenu:self didClickCircleView:sender];
}
布局和創(chuàng)建視圖分開
由于視圖的布局和拖動(dòng)的效果是相關(guān),所以布局和創(chuàng)建應(yīng)該獨(dú)立出來.其實(shí)我們實(shí)際開發(fā)中也應(yīng)該這樣做.在用frame布局的時(shí)候,我一般習(xí)慣把布局的操作放在layoutSubview里面,是的創(chuàng)建要不在初始化的時(shí)候創(chuàng)建完成,要不用懶加載額形式創(chuàng)建.
先來看看如果不把布局和手勢(shì)關(guān)聯(lián)是怎樣的效果.
看起來是不是特別的僵硬,下面就詳細(xì)講一講使用到的布局和動(dòng)畫
布局和動(dòng)畫
這種花瓣形的布局是當(dāng)時(shí)比較頭疼的,牽涉到了角度計(jì)算(asinf:逆正弦函數(shù),acosf:逆余弦函數(shù)),長(zhǎng)度百分比換成角度百分比
先看圖:
當(dāng)時(shí)搞這個(gè)的時(shí)候,反正我是基本把這些東西還給了初中老師.
為了實(shí)現(xiàn)能夠當(dāng)菜單靠邊的時(shí)候,小圓能夠適應(yīng)自動(dòng)旋轉(zhuǎn)角度,我們需要考慮當(dāng)前邊緣是哪個(gè)方向.類似于:
具體思路:
- 根據(jù)當(dāng)前菜單的x,y的正,負(fù)決定是在哪個(gè)方向上的邊緣.
- 根據(jù)x,y負(fù)數(shù)的絕對(duì)值能夠知道當(dāng)前偏移了屏幕多少
- 根據(jù)x,y偏移的程度改變整個(gè)可見的弧度,得到可變的弧度范圍
- 遍歷小圓,改變各個(gè)小圓的中心點(diǎn)
上代碼吧:
// 頂部邊緣
if(self.circleMenuView.frame.origin.y < 0 &&
self.circleMenuView.frame.origin.x > 0 &&
CGRectGetMaxX(self.circleMenuView.frame) < self.frame.size.width){
// 部分顯示
fullCircle = NO;
// 得到頂部偏移多少
CGFloat d = -(self.circleMenuView.frame.origin.y + self.menuLength);
// 獲得起始角度的位置
startingAngle = asinf((d + (self.maxW / 2.0) + 5) / (self.menuLength+radiusToAdd));
// 獲取總共顯示的晚飯
usableAngle = M_PI - (2 * startingAngle);
}
// 左邊
if(self.circleMenuView.frame.origin.x < 0){
fullCircle = NO;
// 開始的角度
if(self.circleMenuView.frame.origin.y > 0){
CGFloat d = -(self.circleMenuView.frame.origin.x + self.menuLength);
startingAngle = -acosf((d + 5) / (self.menuLength + radiusToAdd));
} else {
CGFloat d = -(self.circleMenuView.frame.origin.y + self.menuLength);
startingAngle = asinf((d + self.maxW / 2.0+ 5) / (self.menuLength + radiusToAdd));
}
// 結(jié)束角度
if(CGRectGetMaxY(self.circleMenuView.frame) <= self.frame.size.height){
if(self.circleMenuView.frame.origin.y > 0){
usableAngle = -2 * startingAngle;
} else {
CGFloat d = -(self.circleMenuView.frame.origin.x + self.menuLength);
CGFloat virtualAngle = acosf((d + 5) / (self.menuLength + radiusToAdd));
usableAngle = 2 * virtualAngle -(virtualAngle+startingAngle);
}
} else {
CGFloat d = (CGRectGetMaxY(self.circleMenuView.frame) - self.frame.size.height -self.menuLength);
CGFloat virtualAngle = -asinf((d + 5) / (self.menuLength + radiusToAdd));
usableAngle = -startingAngle+virtualAngle;
}
}
底部和右邊的實(shí)現(xiàn)方法同頂部和左邊的思路是一樣的
最后開始布局各個(gè)小圓
for(int i = 0; i < [self.elementsArray count]; i++){
UIButton *element = [self.elementsArray objectAtIndex:i];
element.center = CGPointMake(self.circleMenuView.frame.size.width / 2.0, self.circleMenuView.frame.size.height / 2.0);
double delayInSeconds = 0.025*i;
void (^elementPositionBlock)(void) = ^{
element.alpha = 1;
[self.circleMenuView bringSubviewToFront:element];
// 這一段比較復(fù)雜,參考的了別人寫的
CGPoint endPoint = CGPointMake(self.circleMenuView.frame.size.width/2.0+(_menuLength+radiusToAdd)*(cos(startingAngle+usableAngle/(self.numberOfCircleView-(fullCircle ? 0 :1))*(float)i)), self.circleMenuView.frame.size.height/2.0+(_menuLength+radiusToAdd)*(sin(startingAngle+usableAngle/(self.numberOfCircleView-(fullCircle ? 0 :1))*(float)i)));
element.center = endPoint;
};
if(animated) {
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// 延遲一下做動(dòng)畫的時(shí)間
[UIView animateWithDuration:0.25 animations:elementPositionBlock];
});
} else {
elementPositionBlock();
};
}
消失動(dòng)畫
消息動(dòng)畫比較簡(jiǎn)單,就是改變各個(gè)子視圖的center.和透明度,然后漸變消失.動(dòng)畫做完之后再里面移除視圖就可以了
for(int i = 0; i < [self.elementsArray count]; i++){
UIButton *element = [self.elementsArray objectAtIndex:i];
double delayInSeconds = 0.025*i;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[UIView animateWithDuration:0.25 animations:^{
element.alpha = 0;
element.center = CGPointMake(self.centerCircleView.frame.size.width/2.0, self.centerCircleView.frame.size.height/2.0);
}];
});
}
double delayInSeconds = 0.25+0.025*[self.elementsArray count];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[UIView animateWithDuration:0.25 animations:^{
self.centerCircleView.alpha = 0;
self.alpha = 0;
} completion:^(BOOL finished) {
[self.centerCircleView removeFromSuperview];
[self removeFromSuperview];
if(completion) completion();
}];
});
參考項(xiàng)目:
AwesomeMenu