先来看一下效果

XLCircleMenu.gif

是不是觉得挺好玩的呀.

通过这篇文章你可以学到:

  • 1.系统UITableView的部分设计思想

  • 2.自定义控件常用设计思路

  • 3.动画的具体使用

  • 4.手势的具体使用

  • 4.装逼一点,良好的代码风格

  • 5......


开始码

  • 随机颜色
    为了快速区分视图,这里用了随机颜色来区分,生成随机颜色的方式比较多.
    常见的获取方法为宏如下:

#define RandomColor [UIColor colorWithRed:arc4random_uniform(255)/255.0 green:arc4random_uniform(255)/255.0 blue:arc4random_uniform(255)/255.0 alpha:1]

通过类方法实现:

+ (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,颜色完全不透明
}

基本设计

我们在做公共控件的时候,可以把要做的部分捋一捋.其实我们在做客户端开发可以类比网页的开发.做的事情无非就是拿到服务端给的数据,通过不同的方式展示出来.其中就涉及到:

  • 1.数据:从客户端来看一般就是服务端给的json格式的数据

  • 2.样式:从客户端开发来看就是设置各个控件的各种属性

  • 3.交互:
    我暂且把这三样映射到UITableView上
    数据对应着DataSource代理,样式对应着我们拿到数据之后自定义的cell不同类型(其实就是设置不同属性为不同值),交互对应着Delegate代理.
    接下来我们也仿照则TabelView的代理写

系统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

当然我们也没必要把系统的代理一个一个仿照则写完,只要自己能够理解到如何根据系统API的设计思想来设计自己写的代码就行了.

自己设计的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

注释我就没有加了,因为OC最好的就是见名知意.

设计类

我们在设计类的时候,做得比较好的,需要考虑属性的读写情况,一般只把需要暴露给外部知道的才暴露出去.

然后在为类添加属性的时候,需要考虑界面和功能,界面和功能需要在写代码之前就应该清楚的.举个例子:

  • 1.具体有多少个可点的小圆,应该通过代理来传递的,并且小圆的个数应该不止在一个地方用到,所以可以定义为属性,而且中间有一个大圆也是通过代理传递的,也需要定义一个属性来接收.于是可以定义出两个属性.

有哪些属性我们还可以直接从功能和界面上直接去思考.

  • 2.根据上面的分析依次考虑我们界面上的元素和我们需要控制的属性.大致定义出了如下属性(实现的思路很多,不一定非要这样定义)

@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.来看一下需要进行哪些操作吧
    首先肯定是显示和隐藏了,如果考虑得多一点,我们可以在显示或者隐藏之后做一个回调给使用则
    者.
    然后就是点击的各种处理,在定义代理的时候,我们已经仿照系统的TableView的Delegate写了一个代理了.所以点击操作可以直接通过代理去处理

简单一点来说初始化的话,我们就让使用者把需要的参数都传入进来吧.最终设计出的方法如下:

- (instancetype)initFromPoint:(CGPoint)centerPoint  withDataSource:(id<XLCircleMenuDataSource>)dataSource                   andDelegate:(id<XLCircleMenuDelegate>)delegate;
- (void)showMenu;
- (void)showMenuWithCompletion:(void(^)()) completion;
- (void)closeMenu;
- (void)closeMenuWithCompletion:(void(^)()) completion;

到目前为止整个类的架子基本就打好了.

类的实现

现在该去具体实现我们的设计了
第一步定义属于的私有属性
第二步开始写方法吧

  • 初始化方法

  • 子视图的创建

  • 手势添加

  • 实现动画

接下来把用到的主要技术和方式


拖拽的是实现

视图的拖拽是通过UITapGestureRecognizer实现的这一章关于iOS手势相关的介绍可以参考一下这篇文章:
iOS手势识别:http://www.cnblogs.com/kenshincui/p/3950646.html

添加手势到指定视图,设置手势代理,根据需要特殊处理

UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(closeCircelMenu:)];    
[self addGestureRecognizer:tapGesture];    
tapGesture.delegate = self;

这里判断如果点击的是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(放射变换)
一旦移动,就改变视图的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];    
}

移动.gif

调用代理的时间

一般在设计代理返回参数的时候都会设计一个属性用来保存代理返回的参数,比如:

    _menuLength = 50;    
   if(self.dataSource && [self.dataSource respondsToSelector:@selector(lengthForCircleMenu:)]){        
       _menuLength = [self.dataSource lengthForCircleMenu:self];    
   }     _numberOfCircleView = [self.dataSource numberOfCircleViewForCircleMenu:self];

这里就通过是否有代理来确定属性的值,当然如果代理是必须的就没必要去判断了(respondsToSelector),相当于通过代理来给属性赋值.
当我们想传递事件给代理的时候,可以通过添加事件给子视图,然后代理出去,如下:

  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];

在处理事件的时候调用代理

-(void)didTapButton:(UIButton *)sender {    
   [self.delegate circleMenu:self didClickCircleView:sender];
}

布局和创建视图分开

由于视图的布局和拖动的效果是相关,所以布局和创建应该独立出来.其实我们实际开发中也应该这样做.在用frame布局的时候,我一般习惯把布局的操作放在layoutSubview里面,是的创建要不在初始化的时候创建完成,要不用懒加载额形式创建.

先来看看如果不把布局和手势关联是怎样的效果.

僵硬的感觉.gif

看起来是不是特别的僵硬,下面就详细讲一讲使用到的布局和动画

布局和动画

这种花瓣形的布局是当时比较头疼的,牵涉到了角度计算(asinf:逆正弦函数,acosf:逆余弦函数),长度百分比换成角度百分比
先看图:

逆正弦函数

逆余弦函数.png

当时搞这个的时候,反正我是基本把这些东西还给了初中老师.

为了实现能够当菜单靠边的时候,小圆能够适应自动旋转角度,我们需要考虑当前边缘是哪个方向.类似于:

具体思路:

  • 根据当前菜单的x,y的正,负决定是在哪个方向上的边缘.

  • 根据x,y负数的绝对值能够知道当前偏移了屏幕多少

  • 根据x,y偏移的程度改变整个可见的弧度,得到可变的弧度范围

  • 遍历小圆,改变各个小圆的中心点

上代码吧:


// 顶部边缘    
    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));        
        }        // 结束角度        
        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;        
    }    
}

底部和右边的实现方法同顶部和左边的思路是一样的

最后开始布局各个小圆

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];            // 这一段比较复杂,参考的了别人写的            
       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){// 延迟一下做动画的时间                
           [UIView animateWithDuration:0.25 animations:elementPositionBlock];            
       });        
   } else {            
       elementPositionBlock();        
   };    
}

消失动画

消息动画比较简单,就是改变各个子视图的center.和透明度,然后渐变消失.动画做完之后再里面移除视图就可以了

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();        
       }];    
});
原文地址:http://mp.weixin.qq.com/s?__biz=MzIwOTQ3NzU0Mw==&mid=2247483823&idx=1&sn=5080a6061968f9d65eb7201048bd7987&scene=0#wechat_redirect

自定义一个"花瓣"菜单-b的更多相关文章

  1. iOS 下拉菜单 FFDropDownMenu自定义下拉菜单样式实战-b

    Demo地址:https://github.com/chenfanfang/CollectionsOfExampleFFDropDownMenu框架地址:https://github.com/chen ...

  2. javascript自定义浏览器右键菜单

    javascript自定义浏览器右键菜单   在书上看到document对象还有一个contextmenu事件,但是不知为什么w3school中找不到这个耶... 利用这个特性写了个浏览器的右键菜单, ...

  3. 玩玩微信公众号Java版之四:自定义公众号菜单

    序: 微信公众号基本的菜单很难满足个性化及多功能的实现,那么微信能否实现自定菜单呢,具体的功能又如何去实现么?下面就来学习一下微信自定义公众号菜单吧! 自定义菜单接口可实现多种类型按钮,如下: 1.c ...

  4. android 自定义下拉菜单

    本实例的自定义下拉菜单主要是继承PopupWindow类来实现的弹出窗体,各种布局效果可以根据自己定义设计.弹出的动画效果主要用到了translate.alpha.scale,具体实现步骤如下: 先上 ...

  5. JS自定义鼠标右击菜单

    自定义鼠标右击菜单要素: 禁止页面默认右击事件 设置右击菜单的样式以及菜单出现的位置(通过捕获鼠标点击位置来确定菜单的位置) 鼠标在指定控件(区域)上右击时显示菜单(默认菜单隐藏,点击鼠标右键时显示) ...

  6. html自定义垂直导航菜单(多级导航菜单,去掉font-awesome图标,添加自己的箭头图标)

    这次在原先html自定义垂直导航菜单的基础上做了比较大的改动: 1.去掉了font-awesome图标,上级菜单右边的箭头是自己用css写的,具体参考<css三角箭头>. 2.去掉了初始化 ...

  7. html自定义垂直导航菜单(加强版--自定义传入menu参数,支持JSONArray、JSArray、JSONObject、JSObject)

    在上一篇中我简单写了个html自定义垂直导航菜单,缺点很明显,里面的数据是固定死的,不能动态更改数据. 这里我重写了一个修改版的垂直二级导航菜单,将原先的menuBox.init(config);修改 ...

  8. html自定义垂直导航菜单

    html自定义垂直导航菜单(目前只支持上级+下级两级菜单) 由于工作的需要,昨天花了三个多小时的事件整理了一份关于垂直导航二级菜单,可以通过js配置的方式初始化菜单box(测试环境:chrome 49 ...

  9. 完美拖拽 &&仿腾讯微博效果&& 自定义多级右键菜单

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

随机推荐

  1. javascript笔记03:易犯错的比较运算

    1.比较两个值是否相等的常见运算: " //false == "" //true == " //true false == "false" ...

  2. 网络请求OkHttp的使用

     okHttp: 1.okHttp是squere公司出的,在Android4.0以后盛行 2.httpClient 在5.0中弃用,在6.0以Google的sdk中没有该api 3.httpUrlCo ...

  3. /lib /usr/lib /usr/local/lib 区别

    简单说,/lib是内核级的,/usr/lib是系统级的,/usr/local/lib是用户级的. /lib/ — 包含许多被 /bin/ 和 /sbin/ 中的程序使用的库文件.目录 /usr/lib ...

  4. [转]Install App to SD

    本文转自:http://www.douban.com/group/topic/29597344/ If you want to move more apps to the SD card, you'l ...

  5. symonfy 项目根目录下没有 bin/console 文件的解决方法

    “Could not open input file: bin/console” Error comes when try to Run the Symfony Application 在采纳的答案中 ...

  6. MySQL flush privileges 명령어

    INSERT나 UPDATE, DELETE문을 이용해서 MySQL의 사용자를 추가,삭제하거나, 사용자 권한 등을 변경하였을 때, MySQL에 변경사항을 적용하기 위해서 사용하는 명령 ...

  7. 【转】MyBatis学习总结(三)——优化MyBatis配置文件中的配置

    [转]MyBatis学习总结(三)——优化MyBatis配置文件中的配置 一.连接数据库的配置单独放在一个properties文件中 之前,我们是直接将数据库的连接配置信息写在了MyBatis的con ...

  8. linux字符设备驱动学习笔记(一):简单的字符设备驱动

    最近在鼓捣lnux字符设备驱动,在网上搜集的各种关于linux设备驱动的代码和注释,要么是针对2.4的,要么是错误百出,根本就不能运行成功,真希望大家在发博客的时候能认真核对下代码的正确性,特别是要把 ...

  9. js调用本地 exe

      js方法 function Run(strPath) //only for ie { try { var objShell = new ActiveXObject("wscript.sh ...

  10. PHP JQuery Json

    PHP: echo Json_encode($result); Jquery: $.get( 'process.php', {'p1':'aa','p2':'dd', 'data_type':'jso ...