更新的文章及版本见 https://github.com/mythkiven/DiffuseMenu_Swift

本动画是 Swift 版的 AwesomeMenu,如需OC版还请移步这里。 pod 'SDiffuseMenu', '~> 1.2.1'

动画效果如下:

配置图如下:

版本记录

  • V1.2.1 修复代码,以便更好的支持 CocoaPods

  • V1.2.0 支持 CocoaPods 嵌入代码因访问权限问题致部分功能无法使用,已在1.2.1版修复

  • V1.1.0 新增任意方向的直线弹出动画\新增常用方向的枚举..

  • 更多记录请戳一下

一、使用方法:

1.使用 pod 方式嵌入项目: pod 'SDiffuseMenu','~> 1.2.1'

2.直接下载 zip 包内含:

1)SDiffuseMenuDebugDemo.xcodeproj: 调试 demo

2)SDiffuseMenu 文件夹:内含源文件

3)SDiffuseMenuDemo.xcworkspace:CocoaPods 调试 demo ,位于Source 文件夹内

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

1、添加协议

class ViewController: UIViewController, SDiffuseMenuDelegate {var menu: SDiffuseMenu!
}

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

// 加载图片
guard let storyMenuItemImage            =  UIImage(named:"menuitem-normal.png")         else { fatalError("图片加载失败") }
guard let storyMenuItemImagePressed     =  UIImage(named:"menuitem-highlighted.png")    else { fatalError("图片加载失败") }
guard let starImage                     =  UIImage(named:"star.png")                    else { fatalError("图片加载失败") }
guard let starItemNormalImage           =  UIImage(named:"addbutton-normal.png")        else { fatalError("图片加载失败") }
guard let starItemLightedImage          =  UIImage(named:"addbutton-highlighted.png")   else { fatalError("图片加载失败") }
guard let starItemContentImage          =  UIImage(named:"plus-normal.png")             else { fatalError("图片加载失败") }
guard let starItemContentLightedImage   =  UIImage(named:"plus-highlighted.png")        else { fatalError("图片加载失败") }var menus = [SDiffuseMenuItem]()for _ in 0 ..< 6 {let starMenuItem =  SDiffuseMenuItem(image: storyMenuItemImage,highlightedImage: storyMenuItemImagePressed, contentImage: starImage,highlightedContentImage: nil)menus.append(starMenuItem)
}

3、设置菜单按钮

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

4、添加 SDiffuseMenu

let menuRect  = 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: menus as NSArray,grapyType: SDiffuseMenu.SDiffuseMenuGrapyType.arc)
menu.center   = self.menuView.center
menu.delegate = self
self.menuView.addSubview(menu)

5、动画配置

  • 如果配置弧线形动画,则动画中弧线半径变化为:0--> 最大 farRadius--> 最小 nearRadius--> 结束 endRadius

  • 如果配置直线形动画,则动画中半径就是直线段的长度,变化为:0--> 最大 farRadius--> 最小 nearRadius-->结束 endRadius

// 动画时长
menu.animationDuration  = CFTimeInterval(animationDrationValue.text!)
// 最小半径
menu.nearRadius         = CGFloat((nearRadiusValue.text! as NSString).floatValue)
// 结束半径
menu.endRadius          = CGFloat((endRadiusValue.text! as NSString).floatValue)
// 最大半径
menu.farRadius          = CGFloat((farRadiusValue.text! as NSString).floatValue)
// 单个动画间隔时间
menu.timeOffset         = CFTimeInterval(timeOffSetValue.text!)!
// 整体角度
menu.menuWholeAngle     = CGFloat((menuWholeAngleValue.text! as NSString).floatValue)
// 整体偏移角度
menu.rotateAngle        = CGFloat((rotateAngleValue.text! as NSString).floatValue)
// 展开时自旋角度
menu.expandRotation     = CGFloat(M_PI)
// 结束时自旋角度
menu.closeRotation      = CGFloat(M_PI * 2)
// 是否旋转菜单按钮
menu.rotateAddButton    = rotateAddButton.isOn
// 菜单按钮旋转角度
menu.rotateAddButtonAngle = CGFloat((rotateAddButtonAngleValue.text! as NSString).floatValue)
// 菜单展示的形状:直线 or 弧形
menu.sDiffuseMenuGrapyType = isLineGrapyType.isOn == true ? .line : .arc// 为方便使用,V1.1.0版本已枚举常见方位,可直接使用,无需再次设置 rotateAngle && menuWholeAngle
// 若对于 rotateAngle\menuWholeAngle 不熟悉,建议查看 source 目录下的配置图片
menu.sDiffuseMenuDirection = .above // 上方180°
//        menu.sDiffuseMenuDirection = .left // 左方180°
//        menu.sDiffuseMenuDirection = .below // 下方180°
//        menu.sDiffuseMenuDirection = .right // 右方180°
//        menu.sDiffuseMenuDirection = .upperRight // 右上方90°
//        menu.sDiffuseMenuDirection = .lowerRight // 右下方90°
//        menu.sDiffuseMenuDirection = .upperLeft // 左上方90°
//        menu.sDiffuseMenuDirection = .lowerLeft // 左下方90°

6、动画过程监听

func SDiffuseMenuDidSelectMenuItem(_ menu: SDiffuseMenu, didSelectIndex index: Int) {print("选中按钮 at index:\(index) is: \(menu.menuItemAtIndex(index)) ")
}func SDiffuseMenuDidClose(_ menu: SDiffuseMenu) {print("菜单关闭动画结束")
}func SDiffuseMenuDidOpen(_ menu: SDiffuseMenu) {print("菜单展开动画结束")
}func SDiffuseMenuWillOpen(_ menu: SDiffuseMenu) {print("菜单将要展开")
}func SDiffuseMenuWillClose(_ 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、动画分析

在 V1.1.0 版本中,已扩展动画的形状:新加入直线型,其原理及计算方法同弧线形,下文不做过多介绍,详情参见版本记录

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

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

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

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

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

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

单选项按钮关闭动画过程如下:

  • 自旋

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

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

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

而关闭的动画中,设置为0 -> 0.4 慢速自旋,0.4 -> 0.5 快速自旋

rotateAnimation.values   = [CGFloat(0.0),CGFloat(closeRotation),CGFloat(0.0)]rotateAnimation.keyTimes = [NSNumber(value: 0.0 as Float),NSNumber(value: 0.4 as Float),NSNumber(value: 0.5 as Float)]
  • 移动

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

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

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

let path = 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 设置路径

let path =  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

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

let animationgroup              =  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")

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

2.2整体动画的控制

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

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

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

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

为了方便理解整体角度的控制,我以结束位置为例画了CAD图,如下:

提醒:下文所述的坐标计算都是基于笛卡儿坐标系,注意与UIKit中坐标系的异同。

关于上图,说明如下:

  • 图中有5个选项按钮和一个菜单按钮,整体角度是 menuWholeAngle,选项中心夹角β(见代码注释);

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

  • 假设整体偏移角度 rotateAngle!=0,那么以绿为坐标轴XY,其中偏移角度就是 rotateAngle

//
// β = ti * menuWholeAngle / icount - CGFloat(1.0)
// β 是两个选项按钮的中心夹角
// 计算 β 正弦余弦值
let sinValue  = CGFloat(sinf(Float(ti * menuWholeAngle / icount - CGFloat(1.0))))
let cosValue  = CGFloat(cosf(Float(ti * menuWholeAngle / icount - CGFloat(1.0) )))// 结束点坐标
var x         = startPoint.x + CGFloat(endRadius) * sinValue
var y         = (CGFloat(startPoint.y) - endRadius * cosValue)
let endPoint  =  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)
let nearPoint  =  CGPoint(x: x, y: y)
item.nearPoint = nearPoint // _rotateCGPointAroundCenter(nearPoint, center: startPoint, angle: rotateAngle)// 最远点坐标,计算方法同CAD图中的结束点坐标
let farPoint   =  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,这样就可以做到任意方向的动画,如下图:

偏移代码如下:

private func _rotateCGPointAroundCenter( _ point: CGPoint, center: CGPoint, angle: CGFloat) -> CGPoint {let translation     = CGAffineTransform(translationX: center.x, y: center.y)let rotation        = CGAffineTransform(rotationAngle: angle)let transformGroup  = translation.inverted().concatenating(rotation).concatenating(translation)return point.applying(transformGroup)
}

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

3、事件响应链

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

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

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {// 动画中禁止 touchif (_isAnimating) {return false}// 展开时可以 touch 任意按钮else if (true == expanding) {return true} // 除上述情况外,仅菜单按钮可点击else {return _startButton.frame.contains(point)}
}

3.2 增大按钮的点击区域

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

override func touchesEnded(_ touches: Set, with event: UIEvent?) {self.isHighlighted = falselet location = ((touches as NSSet).anyObject()! as AnyObject).location(in: self) // 点击范围if (SDiffuseMenuItem.ScaleRect(self.bounds, n: kDiffuseMenuItemDefaultTouchRange).contains(location)) {delegate?.SDiffuseMenuItemTouchesEnd(self)}
}
class func ScaleRect( _ rect:CGRect, n:CGFloat) -> CGRect {let x       = (rect.size.width - rect.size.width * n) / 2let y       = (rect.size.height - rect.size.height * n) / 2let width   = rect.size.width * nlet height  = rect.size.height * nreturn CGRect(x: x , y: y ,width: width ,height: height)
}
// 其中ScaleRect方法的playground版见下图// 增大点击范围,还可以在point方法中判断,不过就需要SDiffuseMenu.swift跟着调整了

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

喜欢的朋友还请给个star哦,后续我会持续优化的!

推荐:炫酷菜单弹射动画:SDiffuseMenu(AwesomeMenu)相关推荐

  1. html5波浪线条,HTML5 svg炫酷波浪线条动画插件

    这是一款HTML5 svg炫酷波浪线条动画插件.该波浪动画插件基于tweenMax和SVG,也可以作为jQuery插件来使用,可以制作出漂亮的波浪线条动画特效. 使用方法 在页面中引入jquery和T ...

  2. android自定义图片过渡效果,教你做出炫酷的Android动画效果

    前言 Android动画也是Android系统中一个很重要的模块, 在平时开发中, 为了做出炫酷的效果, 动画可以说是必不可少的; 本文将总结Android中与动画相关的部分, 文中部分内容整理自文末 ...

  3. php星空背景动态,纯CSS3炫酷3D星空动画特效

    简要教程 这是一款使用纯CSS3制作的炫酷3D星空动画特效.该特效中,以飞船向前快速移动为视角,所有的星星都快速的变大并向后移动,效果非常逼真. 使用方法 HTML结构 该3D星空特效只使用一个 元素 ...

  4. 纯CSS3炫酷3D星空动画特效

    效果: 源码: <!DOCTYPE html> <html lang="zh"> <head><meta charset="UT ...

  5. Canvas炫酷3D线条动画背景

    下载地址 Canvas炫酷3D线条动画背景,可以变色的颜色渐变网页动态背景特效. dd:

  6. android 气球动画,Android TV使用贝赛尔曲线制作炫酷的开场动画

    目录 前言 很多App启动的时候会用到炫酷的开场动画.Android TV端也一样,每一个不同的模块,产品经理都可能设计了不同的开场动画.对于这些复杂的开场动画,最重要的是学会拆分,只要拆分得当,就会 ...

  7. 10款炫酷的HTML5动画特效,附源码

    HTML5确实非常强大,很多时候我们可以利用HTML5中的新技术实现非常炫酷效果时,这些效果也非常消耗电脑的CPU,但是这些HTML5效果确实能给用户带来不一样的用户体验. 今天我要跟大家分享一些HT ...

  8. 西门子界面官方精美触摸屏+WINCC程序模板 西门子官方触摸屏程序模板,炫酷的扁平式动画效果,脚本动画,自动生成二维码,可仿真,堪比智能手机,有精简,精致,wincc,无线面板等包含了所有西门子人机界

    西门子界面官方精美触摸屏+WINCC程序模板 西门子官方触摸屏程序模板,炫酷的扁平式动画效果,脚本动画,自动生成二维码,可仿真,堪比智能手机,有精简,精致,wincc,无线面板等包含了所有西门子人机界 ...

  9. 超炫酷canvas 龙卷风动画

    超炫酷canvas 龙卷风动画 所有代码 最后来看一下效果图 所有代码 JavaScript 代码片. function project3D(x, y, z, vars) {var p, d;x -= ...

最新文章

  1. maven实现依赖的“全局排除”
  2. __add__,关于运算符重载(用户权限)
  3. 7-4 N皇后 (28 分)(思路+详解)
  4. SharePoint 2013 workflow cannot start automatically when you logged in site as a system account
  5. java学习(130):treemap类
  6. 竞赛数据清洗缺失值_Kaggle 数据清洗挑战 Day 1 - 手把手教你五步处理缺失值
  7. 什么是Makefile.am和Makefile.in?
  8. 2021牛客寒假算法基础集训营2,签到题FHIJ
  9. 智能优化算法:跳蛛优化算法-附代码
  10. chromedriver下载链接以及对应版本
  11. 泛函分析笔记08:有界线性算子和泛函介绍与共鸣定理
  12. java设计模式之-策略模式
  13. golang使用技巧与易错点总结
  14. 程序员如何管理自己的代码
  15. android自定义百度地图导航,Android百度定位导航之基于百度地图移动获取位置和自动定位...
  16. 转行学软件测试,月薪5k到30k,给IT赶路人一些个人建议...
  17. java实现给MultipartFile添加水印
  18. Ubuntu16.04安装CH340驱动
  19. shopex之phpunit环境搭建
  20. python通过Excel表格读取存储数据

热门文章

  1. Tessellation Shader(细分曲面着色器)
  2. 白鹭小游戏-成语挑战-游戏页面-字块
  3. 预装win8/win10新款电脑设置U盘启动
  4. 搞个好玩的东西,爬虫爬取网络流行语录,并通过python自动发送给qq好友,实现有技术的刷屏哈哈。
  5. 关于WINDOW7使用淘宝:800A138F错误…
  6. sersync+rsync实现数据文件实时同步
  7. Matlab画图plot(x1,y1,‘ro‘,x1,y1)
  8. 有道云笔记同步失败解决方案
  9. 【愚公系列】2023年06月 网络安全高级班 025.HW护网行动攻防演练介绍和工具
  10. C#下查找并杀死子进程(进程树)