AwesomeMenu的Swift版改写之旅:SDiffuseMenu

转载自:http://3code.info/2017/01/18/SDiffuseMenu/

源码在这里:https://github.com/mythkiven/DiffuseMenu_Swift


实际效果如下:

一、使用方法如下:

添加协议(动画状态回调) -> 设置选项数组 -> 设置菜单按钮 -> 动画属性配置 -> .addSubview(menu)

1、添加协议

classViewController:UIViewController, SDiffuseMenuDelegate{varmenu:SDiffuseMenu!}

2、设置菜单的选项按钮数据

guardletstoryMenuItemImage=UIImage(named:"menuitem-normal.png")else{fatalError("图片加载失败") }guardletstoryMenuItemImagePressed=UIImage(named:"menuitem-highlighted.png")else{fatalError("图片加载失败") }guardletstarImage=UIImage(named:"star.png")else{fatalError("图片加载失败") }guardletstarItemNormalImage=UIImage(named:"addbutton-normal.png")else{fatalError("图片加载失败") }guardletstarItemLightedImage=UIImage(named:"addbutton-highlighted.png")else{fatalError("图片加载失败") }guardletstarItemContentImage=UIImage(named:"plus-normal.png")else{fatalError("图片加载失败") }guardletstarItemContentLightedImage=UIImage(named:"plus-highlighted.png")else{fatalError("图片加载失败") }varmenus=[SDiffuseMenuItem]()for_in0..<9{letstarMenuItem=SDiffuseMenuItem(image: storyMenuItemImage,highlightedImage: storyMenuItemImagePressed,contentImage: starImage,highlightedContentImage:nil)    menus.append(starMenuItem)}

3、设置菜单按钮

letstartItem=SDiffuseMenuItem(image: starItemNormalImage,highlightedImage: starItemLightedImage,contentImage: starItemContentImage,highlightedContentImage: starItemContentLightedImage)

4、添加SDiffuseMenu

letmenuRect=CGRect.init(x:self.menuView.bounds.size.width/2,y:self.menuView.bounds.size.width/2,width:self.menuView.bounds.size.width,height:self.menuView.bounds.size.width)menu=SDiffuseMenu(frame:menuRect,startItem:startItem,menusArray:menusasNSArray)menu.center=self.menuView.centermenu.delegate=selfself.menuView.addSubview(menu)

5、动画配置

动画中半径的变化:0--> 最大farRadius--> 最小nearRadius--> 结束endRadius

//动画时长menu.animationDuration=CFTimeInterval(animationDrationValue.text!)//最小半径menu.nearRadius=CGFloat((nearRadiusValue.text!asNSString).floatValue)//结束半径menu.endRadius=CGFloat((endRadiusValue.text!asNSString).floatValue)//最大半径menu.farRadius=CGFloat((farRadiusValue.text!asNSString).floatValue)//单个动画间隔时间menu.timeOffset=CFTimeInterval(timeOffSetValue.text!)!//整体角度menu.menuWholeAngle=CGFloat((menuWholeAngleValue.text!asNSString).floatValue)//整体偏移角度menu.rotateAngle=CGFloat(0.0)//展开时自旋角度menu.expandRotation=CGFloat(M_PI)//结束时自旋角度menu.closeRotation=CGFloat(M_PI*2)//是否旋转菜单按钮menu.rotateAddButton=rotateAddButton.isOn//菜单按钮旋转角度menu.rotateAddButtonAngle=CGFloat((rotateAddButtonAngleValue.text!asNSString).floatValue)//..

6、动画过程监听

funcSDiffuseMenuDidSelectMenuItem(_menu: SDiffuseMenu,didSelectIndexindex:Int) {print("选中按钮at index:\(index)is:\(menu.menuItemAtIndex(index))")}funcSDiffuseMenuDidClose(_menu: SDiffuseMenu) {print("菜单关闭动画结束")}funcSDiffuseMenuDidOpen(_menu: SDiffuseMenu) {print("菜单展开动画结束")}funcSDiffuseMenuWillOpen(_menu: SDiffuseMenu) {print("菜单将要展开")}funcSDiffuseMenuWillClose(_menu: SDiffuseMenu) {print("菜单将要关闭")}

二、Swift转写之旅

总的来说,动画的原理还是比较简单的,主要涉及到的内容是CABasicAnimation、CAKeyframeAnimation以及事件响应链相关知识,下边分两部分介绍之。

1、CAPropertyAnimation动画

在SDiffuseMenu中动画用CAPropertyAnimation的子类CABasicAnimation和CAKeyframeAnimation来实现,关于这两个子类简述如下:

CABasicAnimation其实可以看作是一种特殊的关键帧动画,只有头尾两个关键帧,可实现移动、旋转、缩放等基本动画;

CAKeyframeAnimation则可以支持任意多个关键帧,关键帧有两种方式来指定,使用path或values;

- path可以是CGPathRef、CGMutablePathRef或者贝塞尔曲线,注意的是:设置了path之后values就无效了;values则相对灵活, 可以指定任意关键帧帧值;

- keyTimes可以为values中的关键帧设置一一对应对应的时间点,其取值范围为0到1.0,keyTimes没有设置的时候,各个关键帧的时间是平分的;

- ..

更多的动画知识请戳此处CoreAnimation_guide

相关的指南、示例代码可以通过点击页面右上角搜索按钮进行搜索,官方文档大多点到为止,挺适合入门学习的,更深的还是需要在实践中摸索总结。

2、动画分析

不论多么复杂的动画,都是由简单的动画组成的,大家先看看SDiffuseMenu中单选项动画:

仔细分析发现可以将整个动画可以拆分为三大部分:

菜单按钮的自旋转,通过transform属性即可实现;

选项按钮的整体展开动画,实际是在定时器中依次添加单个选项按钮的动画组,控制timeInterval来实现动画的先后执行顺序;

单个选项按钮的动画则拆分为3部分:展开动画、结束动画和点击动画,都是动画组,下边以结束动画为例,简单介绍其实现过程。

2.1、单个选项关闭动画分析:

动画过程:点击菜单关闭动画 -> 菜单旋转复位;选项按钮自旋+从endRadius移动到farRadius ->选项按钮到达farRadius之后:开始返回+同时自旋转 -> 然后回到起始点。

1、自旋

大家仔细看会发现展开动画和结束动画的自旋转是有差异的,因为关键帧设置的不同。

展开动画中设置的关键帧如下,0.3对应expandRotation展开自选角度,0.4对应0°,所以在0.3 -> 0.4的时间会出现快速的自旋。

rotateAnimation.values=[CGFloat(expandRotation),CGFloat(0.0)]rotateAnimation.keyTimes=[NSNumber(value:0.3asFloat),NSNumber(value:0.4asFloat)]

而关闭的动画中,我设置如下,细化了关键帧,可以看出自旋的动画细节丰富一些,0 -> 0.4 慢速自旋,0.4 -> 0.5 快速自旋。

rotateAnimation.values=[CGFloat(0.0),CGFloat(closeRotation),CGFloat(0.0)]rotateAnimation.keyTimes=[NSNumber(value:0.0asFloat),NSNumber(value:0.4asFloat),NSNumber(value:0.5asFloat)]

2、移动

移动的控制源于path是怎样设定的,代码中我写了两种方法,其中一种是注释掉了。

letpositionAnimation=CAKeyframeAnimation(keyPath:"position")positionAnimation.duration=animationDuration

1)\使用贝塞尔曲线作为path,从代码中可以明显的看出移动的路径:endPoint -> farPoint -> startPoint

letpath=UIBezierPath.init()path.move(to:CGPoint(x: item.endPoint.x,y: item.endPoint.y))path.addLine(to:CGPoint(x: item.farPoint.x,y: item.farPoint.y))path.addLine(to:CGPoint(x: item.startPoint.x,y: item.startPoint.y))positionAnimation.path=path.cgPath

2)\使用CGPathRef或GCMutablePathRef设置路径

letpath=CGMutablePath()path.move(to:CGPoint(x: item.endPoint.x,y: item.endPoint.y))path.addLine(to:CGPoint(x: item.farPoint.x,y: item.farPoint.y))path.addLine(to:CGPoint(x: item.startPoint.x,y: item.startPoint.y))positionAnimation.path=path

自旋和平移都有了,接下来要加入到动画组中:

letanimationgroup=CAAnimationGroup()animationgroup.animations=[positionAnimation, rotateAnimation]animationgroup.duration=animationDuration//动画结束后,layer保持最终的状态animationgroup.fillMode=kCAFillModeForwards//速度控制我设置的如此,大家根据需要自行修改即可animationgroup.timingFunction=CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseIn)//代理是为了获取到动画结束的信号animationgroup.delegate=self

最添加进layer即可

item.layer.add(animationgroup,forKey:"Close")

其余的动画原理和上述的关闭动画其实是一样的,基于属性的动画,通过操作帧来实现我们想要的效果,小伙伴们直接看代码吧~

这里插一句,不知道小伙伴们有没有注意到一点,就是layer为什么叫CALayer,而且和动画的关系还这么紧密?

2.2、整体动画的控制

注意,整体动画的控制以上并未表述,在这个地方也需要注意下,为了让整体动画在一个合适的角度展示出来,就需要从整体上控制角度。

从上图中可以看出,整体的角度是由menuWholeAngle和rotateAngle共同控制的。

menuWholeAngle: 控制整体动画的范围角度;

rotateAngle: 用于控制整体的偏移角度

为了方便理解整体角度的控制,我以结束位置为例画了CAD图,如下:提醒:下文所述的坐标计算都是基于笛卡儿坐标系,注意与UIKit中坐标系的异同。

关于上图,说明如下:

1、图中有5个选项按钮和一个菜单按钮,整体角度是menuWholeAngle,选项中心夹角β;

2、假设偏移角度rotateAngle=0,则以红色线为坐标轴XY,下文先以此为准进行坐标计算;

3、假设整体偏移角度rotateAngle!=0,那么以蓝色为坐标轴XY,其中偏移角度就是rotateAngle。

////β = ti * menuWholeAngle / icount - CGFloat(1.0)//β是两个选项按钮的中心夹角;//计算β正弦余弦值:letsinValue=CGFloat(sinf(Float(ti*menuWholeAngle/icount-CGFloat(1.0))))letcosValue=CGFloat(cosf(Float(ti*menuWholeAngle/icount-CGFloat(1.0) )))//结束点坐标:varx=startPoint.x+CGFloat(endRadius)*sinValuevary=(CGFloat(startPoint.y)-endRadius*cosValue)letendPoint=CGPoint(x: x,y: y)item.endPoint=endPoint//_rotateCGPointAroundCenter(endPoint, center: startPoint, angle: rotateAngle)//最近点坐标,计算方法同CAD图中的结束点坐标x=startPoint.x+nearRadius*CGFloat(sinValue)y=startPoint.y-nearRadius*CGFloat(cosValue)letnearPoint=CGPoint(x: x,y: y)item.nearPoint=nearPoint//_rotateCGPointAroundCenter(nearPoint, center: startPoint, angle: rotateAngle)//最远点坐标,计算方法同CAD图中的结束点坐标letfarPoint=CGPoint(x: startPoint.x+farRadius*sinValue,y: startPoint.y-farRadius*cosValue)item.farPoint=farPoint//_rotateCGPointAroundCenter(farPoint, center: startPoint, angle: rotateAngle)

OK,上边计算了每个选项的坐标,从而确定了每个选项的end坐标,可以实现一个整体的动画效果。但是,请注意,上边我注释了对 '_rotateCGPointAroundCenter '的调用,使得动画的整体偏移角度为0。如果放开注释,结果会怎样?

最终我们要实现的效果是可以围绕菜单选项展开任意角度的整体动画,那么只需要在以上的基础,加上坐标轴系的旋转即可。请看上图的绿色线,假设其为新的坐标系,让红色坐标系绕其旋转rotateAngle,就相当于选项按钮整体偏移rotateAngle,这样就可以做到任意方向的动画,如下图:

偏移代码如下:

privatefunc_rotateCGPointAroundCenter(_point: CGPoint,center: CGPoint,angle: CGFloat)->CGPoint {lettranslation=CGAffineTransform(translationX: center.x,y: center.y)letrotation=CGAffineTransform(rotationAngle: angle)lettransformGroup=translation.inverted().concatenating(rotation).concatenating(translation)returnpoint.applying(transformGroup)}

那些看似复杂的动画,但如果细细分析,其实也不难哦~

3、事件响应链

其实这里并没有直接使用hitTest寻找响应View,而是在两处使用相关的知识

3.1、利用'point(inside point: CGPoint, with event: UIEvent?) -> Bool'来控制touch事件的分发

overridefuncpoint(insidepoint: CGPoint,withevent: UIEvent?)->Bool{//动画中禁止touchif(_isAnimating) {returnfalse}//展开时可以touch任意按钮elseif(true==expanding) {returntrue}//除上述情况外,仅菜单按钮可点击else{return_startButton.frame.contains(point)    }}

3.2、增大按钮的点击区域

在OC中,经常遇到放大按钮点击区域或者限制touch区域的问题,一般可以通过设置frame或者利用hitTest处理,在Swift中也是一样的。在SDiffuseMenu中,对于点击范围的处理如下:

overridefunctouchesEnded(_touches:Set,withevent: UIEvent?) {self.isHighlighted=falseletlocation=((touchesasNSSet).anyObject()!asAnyObject).location(in:self)//点击范围if(SDiffuseMenuItem.ScaleRect(self.bounds,n: kDiffuseMenuItemDefaultTouchRange).contains(location)) {        delegate?.SDiffuseMenuItemTouchesEnd(self)    }}classfuncScaleRect(_rect:CGRect,n:CGFloat)->CGRect {letx=(rect.size.width-rect.size.width*n)/2lety=(rect.size.height-rect.size.height*n)/2letwidth=rect.size.width*nletheight=rect.size.height*nreturnCGRect(x: x ,y: y ,width: width ,height: height)}//其中ScaleRect方法的playground版见下图//增大点击范围,还可以在point方法中判断,不过就需要SDiffuseMenu.swift跟着调整了,这一期先不采用第二种方法,下期再尝试。

下图是ScaleRect方法小测试,看着是不是很好用啊😁😁

这一版的SDiffuseMenu和AwesomeMenu基本是一样的,接下来的一版我会增加多方向的直线弹出排列动画,喜欢的朋友还请给个star哦,我会努力优化的~

还有上边问题的答案,我猜测是Core Animation Layer。

最后分享下Swift学习心得:

我基本是以官方文档为主的,有疑问就google;

参照官方给出的demo,以及修订blog;

使用Playground,这个真好用,下边附图;

参考资料我也总结了下,请戳此处

做个广告:针对Swift3.0.1,我写了小教程放在个人博客,只是近期修改了设置访问有点问题..

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容

  • #define kBlackColor [UIColor blackColor] //.h //划线 + (voi...
    CHADHEA阅读 788评论 0 1
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,469评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,101评论 5 13
  • Core Animation Core Animation,中文翻译为核心动画,它是一组非常强大的动画处理API,...
    45b645c5912e阅读 3,016评论 0 21
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,365评论 0 17