轉(zhuǎn)載自:http://3code.info/2017/01/18/SDiffuseMenu/
源碼在這里:https://github.com/mythkiven/DiffuseMenu_Swift
實(shí)際效果如下:
添加協(xié)議(動(dòng)畫狀態(tài)回調(diào)) -> 設(shè)置選項(xiàng)數(shù)組 -> 設(shè)置菜單按鈕 -> 動(dòng)畫屬性配置 -> .addSubview(menu)
classViewController:UIViewController, SDiffuseMenuDelegate{varmenu:SDiffuseMenu!}
2爪模、設(shè)置菜單的選項(xiàng)按鈕數(shù)據(jù)
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)}
letstartItem=SDiffuseMenuItem(image: starItemNormalImage,highlightedImage: starItemLightedImage,contentImage: starItemContentImage,highlightedContentImage: starItemContentLightedImage)
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)
動(dòng)畫中半徑的變化:0--> 最大farRadius--> 最小nearRadius--> 結(jié)束endRadius
//動(dòng)畫時(shí)長(zhǎng)menu.animationDuration=CFTimeInterval(animationDrationValue.text!)//最小半徑menu.nearRadius=CGFloat((nearRadiusValue.text!asNSString).floatValue)//結(jié)束半徑menu.endRadius=CGFloat((endRadiusValue.text!asNSString).floatValue)//最大半徑menu.farRadius=CGFloat((farRadiusValue.text!asNSString).floatValue)//單個(gè)動(dòng)畫間隔時(shí)間menu.timeOffset=CFTimeInterval(timeOffSetValue.text!)!//整體角度menu.menuWholeAngle=CGFloat((menuWholeAngleValue.text!asNSString).floatValue)//整體偏移角度menu.rotateAngle=CGFloat(0.0)//展開(kāi)時(shí)自旋角度menu.expandRotation=CGFloat(M_PI)//結(jié)束時(shí)自旋角度menu.closeRotation=CGFloat(M_PI*2)//是否旋轉(zhuǎn)菜單按鈕menu.rotateAddButton=rotateAddButton.isOn//菜單按鈕旋轉(zhuǎn)角度menu.rotateAddButtonAngle=CGFloat((rotateAddButtonAngleValue.text!asNSString).floatValue)//..
6审葬、動(dòng)畫過(guò)程監(jiān)聽(tīng)
funcSDiffuseMenuDidSelectMenuItem(_menu: SDiffuseMenu,didSelectIndexindex:Int) {print("選中按鈕at index:\(index)is:\(menu.menuItemAtIndex(index))")}funcSDiffuseMenuDidClose(_menu: SDiffuseMenu) {print("菜單關(guān)閉動(dòng)畫結(jié)束")}funcSDiffuseMenuDidOpen(_menu: SDiffuseMenu) {print("菜單展開(kāi)動(dòng)畫結(jié)束")}funcSDiffuseMenuWillOpen(_menu: SDiffuseMenu) {print("菜單將要展開(kāi)")}funcSDiffuseMenuWillClose(_menu: SDiffuseMenu) {print("菜單將要關(guān)閉")}
總的來(lái)說(shuō)祝闻,動(dòng)畫的原理還是比較簡(jiǎn)單的占卧,主要涉及到的內(nèi)容是CABasicAnimation遗菠、CAKeyframeAnimation以及事件響應(yīng)鏈相關(guān)知識(shí),下邊分兩部分介紹之华蜒。
1辙纬、CAPropertyAnimation動(dòng)畫
在SDiffuseMenu中動(dòng)畫用CAPropertyAnimation的子類CABasicAnimation和CAKeyframeAnimation來(lái)實(shí)現(xiàn),關(guān)于這兩個(gè)子類簡(jiǎn)述如下:
CABasicAnimation其實(shí)可以看作是一種特殊的關(guān)鍵幀動(dòng)畫,只有頭尾兩個(gè)關(guān)鍵幀,可實(shí)現(xiàn)移動(dòng)叭喜、旋轉(zhuǎn)贺拣、縮放等基本動(dòng)畫;
CAKeyframeAnimation則可以支持任意多個(gè)關(guān)鍵幀,關(guān)鍵幀有兩種方式來(lái)指定,使用path或values;
- path可以是CGPathRef、CGMutablePathRef或者貝塞爾曲線,注意的是:設(shè)置了path之后values就無(wú)效了;values則相對(duì)靈活, 可以指定任意關(guān)鍵幀幀值;
- keyTimes可以為values中的關(guān)鍵幀設(shè)置一一對(duì)應(yīng)對(duì)應(yīng)的時(shí)間點(diǎn),其取值范圍為0到1.0,keyTimes沒(méi)有設(shè)置的時(shí)候,各個(gè)關(guān)鍵幀的時(shí)間是平分的;
- ..
更多的動(dòng)畫知識(shí)請(qǐng)戳此處CoreAnimation_guide
相關(guān)的指南捂蕴、示例代碼可以通過(guò)點(diǎn)擊頁(yè)面右上角搜索按鈕進(jìn)行搜索譬涡,官方文檔大多點(diǎn)到為止,挺適合入門學(xué)習(xí)的啥辨,更深的還是需要在實(shí)踐中摸索總結(jié)涡匀。
不論多么復(fù)雜的動(dòng)畫溉知,都是由簡(jiǎn)單的動(dòng)畫組成的陨瘩,大家先看看SDiffuseMenu中單選項(xiàng)動(dòng)畫:
仔細(xì)分析發(fā)現(xiàn)可以將整個(gè)動(dòng)畫可以拆分為三大部分:
菜單按鈕的自旋轉(zhuǎn),通過(guò)transform屬性即可實(shí)現(xiàn)级乍;
選項(xiàng)按鈕的整體展開(kāi)動(dòng)畫拾酝,實(shí)際是在定時(shí)器中依次添加單個(gè)選項(xiàng)按鈕的動(dòng)畫組,控制timeInterval來(lái)實(shí)現(xiàn)動(dòng)畫的先后執(zhí)行順序卡者;
單個(gè)選項(xiàng)按鈕的動(dòng)畫則拆分為3部分:展開(kāi)動(dòng)畫蒿囤、結(jié)束動(dòng)畫和點(diǎn)擊動(dòng)畫,都是動(dòng)畫組崇决,下邊以結(jié)束動(dòng)畫為例材诽,簡(jiǎn)單介紹其實(shí)現(xiàn)過(guò)程。
2.1恒傻、單個(gè)選項(xiàng)關(guān)閉動(dòng)畫分析:
動(dòng)畫過(guò)程:點(diǎn)擊菜單關(guān)閉動(dòng)畫 -> 菜單旋轉(zhuǎn)復(fù)位脸侥;選項(xiàng)按鈕自旋+從endRadius移動(dòng)到farRadius ->選項(xiàng)按鈕到達(dá)farRadius之后:開(kāi)始返回+同時(shí)自旋轉(zhuǎn) -> 然后回到起始點(diǎn)。
1盈厘、自旋
大家仔細(xì)看會(huì)發(fā)現(xiàn)展開(kāi)動(dòng)畫和結(jié)束動(dòng)畫的自旋轉(zhuǎn)是有差異的睁枕,因?yàn)殛P(guān)鍵幀設(shè)置的不同。
展開(kāi)動(dòng)畫中設(shè)置的關(guān)鍵幀如下沸手,0.3對(duì)應(yīng)expandRotation展開(kāi)自選角度外遇,0.4對(duì)應(yīng)0°,所以在0.3 -> 0.4的時(shí)間會(huì)出現(xiàn)快速的自旋契吉。
rotateAnimation.values=[CGFloat(expandRotation),CGFloat(0.0)]rotateAnimation.keyTimes=[NSNumber(value:0.3asFloat),NSNumber(value:0.4asFloat)]
而關(guān)閉的動(dòng)畫中跳仿,我設(shè)置如下,細(xì)化了關(guān)鍵幀捐晶,可以看出自旋的動(dòng)畫細(xì)節(jié)豐富一些菲语,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山上、移動(dòng)
移動(dòng)的控制源于path是怎樣設(shè)定的眼耀,代碼中我寫了兩種方法,其中一種是注釋掉了佩憾。
letpositionAnimation=CAKeyframeAnimation(keyPath:"position")positionAnimation.duration=animationDuration
1)\使用貝塞爾曲線作為path,從代碼中可以明顯的看出移動(dòng)的路徑: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設(shè)置路徑
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
自旋和平移都有了哮伟,接下來(lái)要加入到動(dòng)畫組中:
letanimationgroup=CAAnimationGroup()animationgroup.animations=[positionAnimation, rotateAnimation]animationgroup.duration=animationDuration//動(dòng)畫結(jié)束后,layer保持最終的狀態(tài)animationgroup.fillMode=kCAFillModeForwards//速度控制我設(shè)置的如此鸯屿,大家根據(jù)需要自行修改即可animationgroup.timingFunction=CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseIn)//代理是為了獲取到動(dòng)畫結(jié)束的信號(hào)animationgroup.delegate=self
最添加進(jìn)layer即可
item.layer.add(animationgroup,forKey:"Close")
其余的動(dòng)畫原理和上述的關(guān)閉動(dòng)畫其實(shí)是一樣的澈吨,基于屬性的動(dòng)畫,通過(guò)操作幀來(lái)實(shí)現(xiàn)我們想要的效果寄摆,小伙伴們直接看代碼吧~
這里插一句谅辣,不知道小伙伴們有沒(méi)有注意到一點(diǎn),就是layer為什么叫CALayer婶恼,而且和動(dòng)畫的關(guān)系還這么緊密桑阶?
注意勾邦,整體動(dòng)畫的控制以上并未表述蚣录,在這個(gè)地方也需要注意下,為了讓整體動(dòng)畫在一個(gè)合適的角度展示出來(lái)眷篇,就需要從整體上控制角度萎河。
從上圖中可以看出,整體的角度是由menuWholeAngle和rotateAngle共同控制的蕉饼。
menuWholeAngle: 控制整體動(dòng)畫的范圍角度虐杯;
rotateAngle: 用于控制整體的偏移角度
為了方便理解整體角度的控制,我以結(jié)束位置為例畫了CAD圖昧港,如下:提醒:下文所述的坐標(biāo)計(jì)算都是基于笛卡兒坐標(biāo)系擎椰,注意與UIKit中坐標(biāo)系的異同。
關(guān)于上圖创肥,說(shuō)明如下:
1达舒、圖中有5個(gè)選項(xiàng)按鈕和一個(gè)菜單按鈕,整體角度是menuWholeAngle叹侄,選項(xiàng)中心夾角β巩搏;
2、假設(shè)偏移角度rotateAngle=0圈膏,則以紅色線為坐標(biāo)軸XY塔猾,下文先以此為準(zhǔn)進(jìn)行坐標(biāo)計(jì)算;
3稽坤、假設(shè)整體偏移角度rotateAngle!=0丈甸,那么以藍(lán)色為坐標(biāo)軸XY,其中偏移角度就是rotateAngle尿褪。
////β = ti * menuWholeAngle / icount - CGFloat(1.0)//β是兩個(gè)選項(xiàng)按鈕的中心夾角睦擂;//計(jì)算β正弦余弦值:letsinValue=CGFloat(sinf(Float(ti*menuWholeAngle/icount-CGFloat(1.0))))letcosValue=CGFloat(cosf(Float(ti*menuWholeAngle/icount-CGFloat(1.0) )))//結(jié)束點(diǎn)坐標(biāo):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)//最近點(diǎn)坐標(biāo),計(jì)算方法同CAD圖中的結(jié)束點(diǎn)坐標(biāo)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)//最遠(yuǎn)點(diǎn)坐標(biāo)杖玲,計(jì)算方法同CAD圖中的結(jié)束點(diǎn)坐標(biāo)letfarPoint=CGPoint(x: startPoint.x+farRadius*sinValue,y: startPoint.y-farRadius*cosValue)item.farPoint=farPoint//_rotateCGPointAroundCenter(farPoint, center: startPoint, angle: rotateAngle)
OK顿仇,上邊計(jì)算了每個(gè)選項(xiàng)的坐標(biāo),從而確定了每個(gè)選項(xiàng)的end坐標(biāo)摆马,可以實(shí)現(xiàn)一個(gè)整體的動(dòng)畫效果臼闻。但是,請(qǐng)注意囤采,上邊我注釋了對(duì) '_rotateCGPointAroundCenter '的調(diào)用述呐,使得動(dòng)畫的整體偏移角度為0。如果放開(kāi)注釋蕉毯,結(jié)果會(huì)怎樣乓搬?
最終我們要實(shí)現(xiàn)的效果是可以圍繞菜單選項(xiàng)展開(kāi)任意角度的整體動(dòng)畫,那么只需要在以上的基礎(chǔ)代虾,加上坐標(biāo)軸系的旋轉(zhuǎn)即可进肯。請(qǐng)看上圖的綠色線,假設(shè)其為新的坐標(biāo)系棉磨,讓紅色坐標(biāo)系繞其旋轉(zhuǎn)rotateAngle江掩,就相當(dāng)于選項(xiàng)按鈕整體偏移rotateAngle,這樣就可以做到任意方向的動(dòng)畫乘瓤,如下圖:
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)}
那些看似復(fù)雜的動(dòng)畫环形,但如果細(xì)細(xì)分析,其實(shí)也不難哦~
其實(shí)這里并沒(méi)有直接使用hitTest尋找響應(yīng)View斟赚,而是在兩處使用相關(guān)的知識(shí)
3.1、利用'point(inside point: CGPoint, with event: UIEvent?) -> Bool'來(lái)控制touch事件的分發(fā)
overridefuncpoint(insidepoint: CGPoint,withevent: UIEvent?)->Bool{//動(dòng)畫中禁止touchif(_isAnimating) {returnfalse}//展開(kāi)時(shí)可以touch任意按鈕elseif(true==expanding) {returntrue}//除上述情況外差油,僅菜單按鈕可點(diǎn)擊else{return_startButton.frame.contains(point)? ? }}
3.2拗军、增大按鈕的點(diǎn)擊區(qū)域
在OC中,經(jīng)常遇到放大按鈕點(diǎn)擊區(qū)域或者限制touch區(qū)域的問(wèn)題蓄喇,一般可以通過(guò)設(shè)置frame或者利用hitTest處理发侵,在Swift中也是一樣的。在SDiffuseMenu中妆偏,對(duì)于點(diǎn)擊范圍的處理如下:
overridefunctouchesEnded(_touches:Set,withevent: UIEvent?) {self.isHighlighted=falseletlocation=((touchesasNSSet).anyObject()!asAnyObject).location(in:self)//點(diǎn)擊范圍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版見(jiàn)下圖//增大點(diǎn)擊范圍刃鳄,還可以在point方法中判斷,不過(guò)就需要SDiffuseMenu.swift跟著調(diào)整了钱骂,這一期先不采用第二種方法叔锐,下期再嘗試挪鹏。
下圖是ScaleRect方法小測(cè)試,看著是不是很好用啊????
這一版的SDiffuseMenu和AwesomeMenu基本是一樣的愉烙,接下來(lái)的一版我會(huì)增加多方向的直線彈出排列動(dòng)畫讨盒,喜歡的朋友還請(qǐng)給個(gè)star哦,我會(huì)努力優(yōu)化的~
還有上邊問(wèn)題的答案步责,我猜測(cè)是Core Animation Layer返顺。
最后分享下Swift學(xué)習(xí)心得:
我基本是以官方文檔為主的,有疑問(wèn)就google蔓肯;
參照官方給出的demo遂鹊,以及修訂blog;
使用Playground,這個(gè)真好用蔗包,下邊附圖;
參考資料我也總結(jié)了下秉扑,請(qǐng)戳此處
做個(gè)廣告:針對(duì)Swift3.0.1,我寫了小教程放在個(gè)人博客气忠,只是近期修改了設(shè)置訪問(wèn)有點(diǎn)問(wèn)題..