Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68881fb3cc | |||
| 83d32f754b | |||
| 628a4bab8a | |||
| 2c9a57acdc | |||
| db7ca734e9 | |||
| f4f645548d | |||
| c520680100 | |||
| 04e27502df | |||
| 9f211f33af | |||
| 083d952ad7 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'CircleMenu'
|
||||
s.version = '3.0.1'
|
||||
s.version = '3.0.5'
|
||||
s.summary = 'Amazing animation with buttons'
|
||||
s.homepage = 'https://github.com/Ramotion/circle-menu'
|
||||
s.license = 'MIT'
|
||||
|
||||
@@ -221,7 +221,6 @@
|
||||
84F248AF1C58E65F008F12C1 /* Sources */,
|
||||
84F248B01C58E65F008F12C1 /* Frameworks */,
|
||||
84F248B11C58E65F008F12C1 /* Resources */,
|
||||
846980FB1C59F398002D77BE /* swift lint */,
|
||||
8403F5851CFF2C2E007D0BD1 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
@@ -325,23 +324,6 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
846980FB1C59F398002D77BE /* swift lint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "swift lint";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
8403F5741CFF2C2E007D0BD1 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
|
||||
@@ -26,7 +26,7 @@ import UIKit
|
||||
|
||||
// MARK: helpers
|
||||
|
||||
func Init<Type>(_ value: Type, block: (_ object: Type) -> Void) -> Type {
|
||||
func customize<Type>(_ value: Type, block: (_ object: Type) -> Void) -> Type {
|
||||
block(value)
|
||||
return value
|
||||
}
|
||||
@@ -88,15 +88,29 @@ open class CircleMenu: UIButton {
|
||||
@IBInspectable open var distance: Float = 100
|
||||
/// Delay between show buttons
|
||||
@IBInspectable open var showDelay: Double = 0
|
||||
/// Start angle of the circle
|
||||
@IBInspectable open var startAngle: Float = 0
|
||||
/// End angle of the circle
|
||||
@IBInspectable open var endAngle: Float = 360
|
||||
|
||||
// Pop buttons radius, if nil use center button size
|
||||
open var subButtonsRadius: CGFloat?
|
||||
|
||||
// Show buttons event
|
||||
open var showButtonsEvent: UIControlEvents = UIControlEvents.touchUpInside {
|
||||
didSet {
|
||||
addActions(newEvent: showButtonsEvent, oldEvent: oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// The object that acts as the delegate of the circle menu.
|
||||
@IBOutlet open var delegate: AnyObject? // CircleMenuDelegate?
|
||||
@IBOutlet open weak var delegate: AnyObject? // CircleMenuDelegate?
|
||||
|
||||
var buttons: [UIButton]?
|
||||
weak var platform: UIView?
|
||||
|
||||
fileprivate var customNormalIconView: UIImageView?
|
||||
fileprivate var customSelectedIconView: UIImageView?
|
||||
public var customNormalIconView: UIImageView?
|
||||
public var customSelectedIconView: UIImageView?
|
||||
|
||||
/**
|
||||
Initializes and returns a circle menu object.
|
||||
@@ -119,7 +133,7 @@ open class CircleMenu: UIButton {
|
||||
super.init(frame: frame)
|
||||
|
||||
if let icon = normalIcon {
|
||||
setImage(UIImage(named: icon), for: UIControlState())
|
||||
setImage(UIImage(named: icon), for: .normal)
|
||||
}
|
||||
|
||||
if let icon = selectedIcon {
|
||||
@@ -140,15 +154,14 @@ open class CircleMenu: UIButton {
|
||||
}
|
||||
|
||||
fileprivate func commonInit() {
|
||||
addActions()
|
||||
addActions(newEvent: showButtonsEvent)
|
||||
|
||||
customNormalIconView = addCustomImageView(state: UIControlState())
|
||||
customNormalIconView = addCustomImageView(state: .normal)
|
||||
|
||||
customSelectedIconView = addCustomImageView(state: .selected)
|
||||
if customSelectedIconView != nil {
|
||||
customSelectedIconView?.alpha = 0
|
||||
}
|
||||
setImage(UIImage(), for: UIControlState())
|
||||
customSelectedIconView?.alpha = 0
|
||||
|
||||
setImage(UIImage(), for: .normal)
|
||||
setImage(UIImage(), for: .selected)
|
||||
}
|
||||
|
||||
@@ -167,7 +180,7 @@ open class CircleMenu: UIButton {
|
||||
|
||||
buttonsAnimationIsShow(isShow: false, duration: duration, hideDelay: hideDelay)
|
||||
|
||||
tapBounceAnimation()
|
||||
tapBounceAnimation(duration: 0.5)
|
||||
tapRotatedAnimation(0.3, isSelected: false)
|
||||
}
|
||||
|
||||
@@ -197,12 +210,18 @@ open class CircleMenu: UIButton {
|
||||
fileprivate func createButtons(platform: UIView) -> [UIButton] {
|
||||
var buttons = [UIButton]()
|
||||
|
||||
let step: Float = 360.0 / Float(buttonsCount)
|
||||
let step = getArcStep()
|
||||
for index in 0 ..< buttonsCount {
|
||||
|
||||
let angle: Float = Float(index) * step
|
||||
let angle: Float = startAngle + Float(index) * step
|
||||
let distance = Float(bounds.size.height / 2.0)
|
||||
let button = Init(CircleMenuButton(size: bounds.size, platform: platform, distance: distance, angle: angle)) {
|
||||
let buttonSize: CGSize
|
||||
if let subButtonsRadius = self.subButtonsRadius {
|
||||
buttonSize = CGSize(width: subButtonsRadius * 2, height: subButtonsRadius * 2)
|
||||
} else {
|
||||
buttonSize = bounds.size
|
||||
}
|
||||
let button = customize(CircleMenuButton(size: buttonSize, platform: platform, distance: distance, angle: angle)) {
|
||||
$0.tag = index
|
||||
$0.addTarget(self, action: #selector(CircleMenu.buttonHandler(_:)), for: UIControlEvents.touchUpInside)
|
||||
$0.alpha = 0
|
||||
@@ -217,7 +236,7 @@ open class CircleMenu: UIButton {
|
||||
return nil
|
||||
}
|
||||
|
||||
let iconView = Init(UIImageView(image: image)) {
|
||||
let iconView = customize(UIImageView(image: image)) {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.contentMode = .center
|
||||
$0.isUserInteractionEnabled = false
|
||||
@@ -241,7 +260,7 @@ open class CircleMenu: UIButton {
|
||||
}
|
||||
|
||||
fileprivate func createPlatform() -> UIView {
|
||||
let platform = Init(UIView(frame: .zero)) {
|
||||
let platform = customize(UIView(frame: .zero)) {
|
||||
$0.backgroundColor = .clear
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
@@ -275,13 +294,37 @@ open class CircleMenu: UIButton {
|
||||
|
||||
// MARK: configure
|
||||
|
||||
fileprivate func addActions() {
|
||||
addTarget(self, action: #selector(CircleMenu.onTap), for: UIControlEvents.touchUpInside)
|
||||
fileprivate func addActions(newEvent: UIControlEvents, oldEvent: UIControlEvents? = nil) {
|
||||
if let oldEvent = oldEvent { removeTarget(self, action: #selector(CircleMenu.onTap), for: oldEvent) }
|
||||
addTarget(self, action: #selector(CircleMenu.onTap), for: newEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves the incremental lengths between buttons. If the arc length is 360 degrees or more, the increments
|
||||
will evenly space out in a full circle. If the arc length is less than 360 degrees, the last button will be
|
||||
placed on the endAngle.
|
||||
*/
|
||||
fileprivate func getArcStep() -> Float {
|
||||
var arcLength = endAngle - startAngle
|
||||
var stepCount = buttonsCount
|
||||
|
||||
if arcLength < 360 {
|
||||
stepCount -= 1
|
||||
} else if arcLength > 360 {
|
||||
arcLength = 360
|
||||
}
|
||||
|
||||
return arcLength / Float(stepCount)
|
||||
}
|
||||
|
||||
// MARK: actions
|
||||
|
||||
private var isBounceAnimating: Bool = false
|
||||
|
||||
@objc func onTap() {
|
||||
guard isBounceAnimating == false else { return }
|
||||
isBounceAnimating = true
|
||||
|
||||
if buttonsIsShown() == false {
|
||||
let platform = createPlatform()
|
||||
buttons = createButtons(platform: platform)
|
||||
@@ -291,7 +334,7 @@ open class CircleMenu: UIButton {
|
||||
let duration = isShow ? 0.5 : 0.2
|
||||
buttonsAnimationIsShow(isShow: isShow, duration: duration)
|
||||
|
||||
tapBounceAnimation()
|
||||
tapBounceAnimation(duration: 0.5) { [weak self] _ in self?.isBounceAnimating = false }
|
||||
tapRotatedAnimation(0.3, isSelected: isShow)
|
||||
}
|
||||
|
||||
@@ -299,9 +342,16 @@ open class CircleMenu: UIButton {
|
||||
guard let platform = self.platform else { return }
|
||||
|
||||
delegate?.circleMenu?(self, buttonWillSelected: sender, atIndex: sender.tag)
|
||||
|
||||
let strokeWidth: CGFloat
|
||||
if let radius = self.subButtonsRadius {
|
||||
strokeWidth = radius * 2
|
||||
} else {
|
||||
strokeWidth = bounds.size.height
|
||||
}
|
||||
|
||||
let circle = CircleMenuLoader(radius: CGFloat(distance),
|
||||
strokeWidth: bounds.size.height,
|
||||
strokeWidth: strokeWidth,
|
||||
platform: platform,
|
||||
color: sender.backgroundColor)
|
||||
|
||||
@@ -310,23 +360,22 @@ open class CircleMenu: UIButton {
|
||||
container.superview?.bringSubview(toFront: container)
|
||||
}
|
||||
|
||||
if let buttons = buttons {
|
||||
circle.fillAnimation(duration, startAngle: -90 + Float(360 / buttons.count) * Float(sender.tag)) { [weak self] in
|
||||
self?.buttons?.forEach { $0.alpha = 0 }
|
||||
}
|
||||
circle.hideAnimation(0.5, delay: duration) { [weak self] in
|
||||
if self?.platform?.superview != nil { self?.platform?.removeFromSuperview() }
|
||||
}
|
||||
|
||||
hideCenterButton(duration: 0.3)
|
||||
showCenterButton(duration: 0.525, delay: duration)
|
||||
|
||||
if customNormalIconView != nil && customSelectedIconView != nil {
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: {
|
||||
self.delegate?.circleMenu?(self, buttonDidSelected: sender, atIndex: sender.tag)
|
||||
})
|
||||
}
|
||||
let step = getArcStep()
|
||||
circle.fillAnimation(duration, startAngle: -90 + startAngle + step * Float(sender.tag)) { [weak self] in
|
||||
self?.buttons?.forEach { $0.alpha = 0 }
|
||||
}
|
||||
circle.hideAnimation(0.5, delay: duration) { [weak self] in
|
||||
if self?.platform?.superview != nil { self?.platform?.removeFromSuperview() }
|
||||
}
|
||||
|
||||
hideCenterButton(duration: 0.3)
|
||||
showCenterButton(duration: 0.525, delay: duration)
|
||||
|
||||
if customNormalIconView != nil && customSelectedIconView != nil {
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: {
|
||||
self.delegate?.circleMenu?(self, buttonDidSelected: sender, atIndex: sender.tag)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,13 +386,12 @@ open class CircleMenu: UIButton {
|
||||
return
|
||||
}
|
||||
|
||||
let step: Float = 360.0 / Float(buttonsCount)
|
||||
let step = getArcStep()
|
||||
for index in 0 ..< buttonsCount {
|
||||
guard case let button as CircleMenuButton = buttons[index] else { continue }
|
||||
let angle: Float = Float(index) * step
|
||||
if isShow == true {
|
||||
delegate?.circleMenu?(self, willDisplay: button, atIndex: index)
|
||||
|
||||
let angle: Float = startAngle + Float(index) * step
|
||||
button.rotatedZ(angle: angle, animated: false, delay: Double(index) * showDelay)
|
||||
button.showAnimation(distance: distance, duration: duration, delay: Double(index) * showDelay)
|
||||
} else {
|
||||
@@ -359,14 +407,14 @@ open class CircleMenu: UIButton {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func tapBounceAnimation() {
|
||||
fileprivate func tapBounceAnimation(duration: TimeInterval, completion: ((Bool)->())? = nil) {
|
||||
transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 5,
|
||||
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 5,
|
||||
options: UIViewAnimationOptions.curveLinear,
|
||||
animations: { () -> Void in
|
||||
self.transform = CGAffineTransform(scaleX: 1, y: 1)
|
||||
},
|
||||
completion: nil)
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
fileprivate func tapRotatedAnimation(_ duration: Float, isSelected: Bool) {
|
||||
@@ -387,13 +435,13 @@ open class CircleMenu: UIButton {
|
||||
toOpacity = 1
|
||||
}
|
||||
|
||||
let rotation = Init(CABasicAnimation(keyPath: "transform.rotation")) {
|
||||
let rotation = customize(CABasicAnimation(keyPath: "transform.rotation")) {
|
||||
$0.duration = TimeInterval(duration)
|
||||
$0.toValue = (toAngle.degrees)
|
||||
$0.fromValue = (fromAngle.degrees)
|
||||
$0.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
}
|
||||
let fade = Init(CABasicAnimation(keyPath: "opacity")) {
|
||||
let fade = customize(CABasicAnimation(keyPath: "opacity")) {
|
||||
$0.duration = TimeInterval(duration)
|
||||
$0.fromValue = fromOpacity
|
||||
$0.toValue = toOpacity
|
||||
@@ -401,7 +449,7 @@ open class CircleMenu: UIButton {
|
||||
$0.fillMode = kCAFillModeForwards
|
||||
$0.isRemovedOnCompletion = false
|
||||
}
|
||||
let scale = Init(CABasicAnimation(keyPath: "transform.scale")) {
|
||||
let scale = customize(CABasicAnimation(keyPath: "transform.scale")) {
|
||||
$0.duration = TimeInterval(duration)
|
||||
$0.toValue = toScale
|
||||
$0.fromValue = fromScale
|
||||
@@ -441,7 +489,7 @@ open class CircleMenu: UIButton {
|
||||
},
|
||||
completion: nil)
|
||||
|
||||
let rotation = Init(CASpringAnimation(keyPath: "transform.rotation")) {
|
||||
let rotation = customize(CASpringAnimation(keyPath: "transform.rotation")) {
|
||||
$0.duration = TimeInterval(1.5)
|
||||
$0.toValue = 0
|
||||
$0.fromValue = (Float(-180).degrees)
|
||||
@@ -450,7 +498,7 @@ open class CircleMenu: UIButton {
|
||||
$0.beginTime = CACurrentMediaTime() + delay
|
||||
}
|
||||
|
||||
let fade = Init(CABasicAnimation(keyPath: "opacity")) {
|
||||
let fade = customize(CABasicAnimation(keyPath: "opacity")) {
|
||||
$0.duration = TimeInterval(0.01)
|
||||
$0.toValue = 0
|
||||
$0.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
@@ -458,7 +506,7 @@ open class CircleMenu: UIButton {
|
||||
$0.isRemovedOnCompletion = false
|
||||
$0.beginTime = CACurrentMediaTime() + delay
|
||||
}
|
||||
let show = Init(CABasicAnimation(keyPath: "opacity")) {
|
||||
let show = customize(CABasicAnimation(keyPath: "opacity")) {
|
||||
$0.duration = TimeInterval(duration)
|
||||
$0.toValue = 1
|
||||
$0.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
|
||||
@@ -60,7 +60,7 @@ internal class CircleMenuButton: UIButton {
|
||||
// MARK: configure
|
||||
|
||||
fileprivate func createContainer(_ size: CGSize, platform: UIView) -> UIView {
|
||||
let container = Init(UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: size))) {
|
||||
let container = customize(UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: size))) {
|
||||
$0.backgroundColor = UIColor.clear
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
|
||||
@@ -200,7 +200,7 @@ internal extension CircleMenuButton {
|
||||
// MARK: layer animation
|
||||
|
||||
internal func rotationAnimation(_ angle: Float, duration: Double) {
|
||||
let rotation = Init(CABasicAnimation(keyPath: "transform.rotation")) {
|
||||
let rotation = customize(CABasicAnimation(keyPath: "transform.rotation")) {
|
||||
$0.duration = TimeInterval(duration)
|
||||
$0.toValue = (angle.degrees)
|
||||
$0.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
|
||||
@@ -66,7 +66,7 @@ internal class CircleMenuLoader: UIView {
|
||||
endAngle: CGFloat.pi * 2,
|
||||
clockwise: true)
|
||||
|
||||
let circle = Init(CAShapeLayer()) {
|
||||
let circle = customize(CAShapeLayer()) {
|
||||
$0.path = circlePath.cgPath
|
||||
$0.fillColor = UIColor.clear.cgColor
|
||||
$0.strokeColor = color?.cgColor
|
||||
@@ -105,7 +105,7 @@ internal class CircleMenuLoader: UIView {
|
||||
}
|
||||
|
||||
internal func createRoundView(_ rect: CGRect, color: UIColor?) {
|
||||
let roundView = Init(UIView(frame: rect)) {
|
||||
let roundView = customize(UIView(frame: rect)) {
|
||||
$0.backgroundColor = UIColor.black
|
||||
$0.layer.cornerRadius = rect.size.width / 2.0
|
||||
$0.backgroundColor = color
|
||||
@@ -125,7 +125,7 @@ internal class CircleMenuLoader: UIView {
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setCompletionBlock(completion)
|
||||
let animation = Init(CABasicAnimation(keyPath: "strokeEnd")) {
|
||||
let animation = customize(CABasicAnimation(keyPath: "strokeEnd")) {
|
||||
$0.duration = CFTimeInterval(duration)
|
||||
$0.fromValue = 0
|
||||
$0.toValue = 1
|
||||
@@ -137,7 +137,7 @@ internal class CircleMenuLoader: UIView {
|
||||
|
||||
internal func hideAnimation(_ duration: CGFloat, delay: Double, completion: @escaping () -> Void) {
|
||||
|
||||
let scale = Init(CABasicAnimation(keyPath: "transform.scale")) {
|
||||
let scale = customize(CABasicAnimation(keyPath: "transform.scale")) {
|
||||
$0.toValue = 1.2
|
||||
$0.duration = CFTimeInterval(duration)
|
||||
$0.fillMode = kCAFillModeForwards
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
**Looking for developers for your project?**<br>
|
||||
This project is maintained by Ramotion, Inc. We specialize in the designing and coding of custom UI for Mobile Apps and Websites.
|
||||
|
||||
<a href="https://ramotion.com/?utm_source=gthb&utm_medium=special&utm_campaign=circle-menu-contact-us/#Get_in_Touch">
|
||||
<a href="https://dev.ramotion.com/?utm_source=gthb&utm_medium=special&utm_campaign=circle-menu-contact-us">
|
||||
<img src="https://github.com/ramotion/gliding-collection/raw/master/contact_our_team@2x.png" width="187" height="34"></a> <br>
|
||||
|
||||
|
||||
@@ -112,11 +112,11 @@ Try this UI component and more like this in our mobile app. Contact us if intere
|
||||
|
||||
<a href="https://itunes.apple.com/app/apple-store/id1182360240?pt=550053&ct=circle-menu&mt=8" >
|
||||
<img src="https://github.com/ramotion/gliding-collection/raw/master/app_store@2x.png" width="117" height="34"></a>
|
||||
<a href="https://ramotion.com/?utm_source=gthb&utm_medium=special&utm_campaign=circle-menu-contact-us/#Get_in_Touch">
|
||||
<a href="https://dev.ramotion.com/?utm_source=gthb&utm_medium=special&utm_campaign=circle-menu-contact-us">
|
||||
<img src="https://github.com/ramotion/gliding-collection/raw/master/contact_our_team@2x.png" width="187" height="34"></a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
Follow us for the latest updates<br>
|
||||
[](https://twitter.com/intent/tweet?text=https://github.com/ramotion/circle-menu)
|
||||
[](https://twitter.com/ramotion)
|
||||
<a href="https://goo.gl/rPFpid" >
|
||||
<img src="https://i.imgur.com/ziSqeSo.png/" width="156" height="28"></a>
|
||||
|
||||
Reference in New Issue
Block a user