Swift组合动画实现类似钉钉效果

最近看了组合动画,简单的实现了类似钉钉效果,效果图如下
[图片上传中...(Ding_left.gif-1e28bd-1515725969222-0)]


Ding_left.gif

Ding_bottom.gif

Ding_right.gif

实现思路:

整体是一个View,通过点击View中的Tap按钮,添加或者隐藏另外的几个按钮,点击时,主要实现另外按钮的缩放,位置的变化就行了。

实现过程:

1. 添加枚举,代表按钮展开的方向
enum BubbleDirection {
    case top
    case left
    case right
    case bottom
}
2. 展开时,根据展开方向修改button的位置和缩放大小
switch self.direction {
        case .top:
            originPosition = CGPoint(x: self.frame.width * 0.5, y: self.frame.size.height - itemWidth * 0.5)
            finalPosition = CGPoint(x: self.frame.width * 0.5, y: self.frame.size.height - (itemWidth + spacing) * CGFloat(i + 1) - spacing)
        case .left:
            originPosition = CGPoint(x: self.frame.width - itemWidth * 0.5, y: itemWidth * 0.5)
            finalPosition = CGPoint(x: self.frame.size.width - (itemWidth + spacing) * CGFloat(i + 1) - spacing, y: itemWidth * 0.5)
        case .bottom:
            originPosition = CGPoint(x: self.frame.width * 0.5, y: itemWidth * 0.5)
            finalPosition = CGPoint(x: itemWidth * 0.5, y: self.frame.size.height - (itemWidth + spacing) * CGFloat(i) - spacing)
        case .right:
            originPosition = CGPoint(x: itemWidth * 0.5, y: itemWidth * 0.5)
            finalPosition = CGPoint(x: self.frame.size.width - (itemWidth + spacing) * CGFloat(i) - spacing, y: itemWidth * 0.5)
        
        }
        
        //position
        positionAnimation.duration = animationDuration
        positionAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        positionAnimation.fromValue = NSValue(cgPoint: originPosition)
        positionAnimation.toValue = NSValue(cgPoint: finalPosition)
        positionAnimation.beginTime = CACurrentMediaTime() + Double(animationDuration) / Double(buttonsArray.count) * Double(i)
        positionAnimation.fillMode = kCAFillModeForwards
        positionAnimation.isRemovedOnCompletion = false
        button.layer.add(positionAnimation, forKey: "positionAnimation")
        button.layer.position = finalPosition
        
        
        //scale
        let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
        scaleAnimation.duration = animationDuration
        scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        scaleAnimation.beginTime = CACurrentMediaTime() + Double(animationDuration) / Double(buttonsArray.count) * Double(i) + 0.1
        scaleAnimation.fromValue = NSNumber(value: 0.01)
        scaleAnimation.toValue = NSNumber(value: 1.0)
        scaleAnimation.fillMode = kCAFillModeForwards
        scaleAnimation.isRemovedOnCompletion = false
        button.layer.add(scaleAnimation, forKey: "scaleAnimation")

        button.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
3. 整体代码
import UIKit

enum BubbleDirection {
    case top
    case left
    case right
    case bottom
}

class BubbleMenuView: UIView {

var titles: [String] = []
var direction = BubbleDirection.top

private var buttonsArray: [UIButton] = [UIButton]()
private var isExpand: Bool = false
private var spacing: CGFloat = 15
private var animationDuration: TimeInterval = 0.25
private var itemWidth: CGFloat = 30
private var orginalFrame: CGRect!

private var label: UILabel!

override init(frame: CGRect) {
    super.init(frame: frame)
}

convenience init(frame: CGRect, direction: BubbleDirection = .left, titles: [String] = ["A", "B", "C"]) {
    self.init(frame: frame)
    
    self.titles = titles
    self.direction = direction
    setupUI()
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

@objc func tap() {
    if isExpand {
       hideMenuButtons()
    }else {
        showMenuButtons()
    }
}

func setupUI() {
    self.orginalFrame = frame

    config()
    addButtons()
}

func config() {
    itemWidth = self.frame.size.width

    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tap))
    self.addGestureRecognizer(tapGesture)
    
    self.layer.cornerRadius = self.frame.size.width * 0.5
    self.layer.masksToBounds = true
    self.backgroundColor = UIColor.white
    
    label = UILabel(frame: CGRect(x: 0, y: 0, width: itemWidth, height: itemWidth))
    label.text = "Tap"
    label.textAlignment = .center
    label.font = UIFont.systemFont(ofSize: 15)
    label.layer.cornerRadius = 20
    label.layer.masksToBounds = true
    label.textColor = UIColor.white
    label.backgroundColor = UIColor.orange
    self.addSubview(label)
    
    label.snp.makeConstraints { (make) in
        make.width.height.equalTo(itemWidth)
        if direction == .right {
            make.left.equalToSuperview()
        }else {
            make.right.equalToSuperview()
        }
        
        if direction == .bottom {
            make.top.equalToSuperview()
        }else {
            make.bottom.equalToSuperview()
        }
    }

}

func addButtons() {
    for i in 0..<titles.count {
        let btn = UIButton(frame: CGRect(x: 0, y: 0, width: itemWidth, height: itemWidth))
        btn.layer.cornerRadius = self.frame.size.height * 0.5
        btn.layer.masksToBounds = true
        btn.setTitle(titles[i], for: UIControlState.normal)
        btn.titleLabel?.font = UIFont.systemFont(ofSize: 15)
        btn.titleLabel?.textColor = UIColor.white
        btn.titleLabel?.textAlignment = .center
        btn.backgroundColor = UIColor.orange
        btn.isHidden = true
        self.addSubview(btn)
        buttonsArray.append(btn)
    }
}

func showMenuButtons() {
    isExpand = true
    adjustExpandHeight()
    
    CATransaction.begin()
    CATransaction.setAnimationDuration(animationDuration)
    
    for i in 0..<buttonsArray.count {
        let button = buttonsArray[i]
        button.isHidden = false
        
        //positionAnimation
        let positionAnimation = CABasicAnimation(keyPath: "position")
        var originPosition = CGPoint.zero
        var finalPosition = CGPoint.zero
        
        switch self.direction {
        case .top:
            originPosition = CGPoint(x: self.frame.width * 0.5, y: self.frame.size.height - itemWidth * 0.5)
            finalPosition = CGPoint(x: self.frame.width * 0.5, y: self.frame.size.height - (itemWidth + spacing) * CGFloat(i + 1) - spacing)
        case .left:
            originPosition = CGPoint(x: self.frame.width - itemWidth * 0.5, y: itemWidth * 0.5)
            finalPosition = CGPoint(x: self.frame.size.width - (itemWidth + spacing) * CGFloat(i + 1) - spacing, y: itemWidth * 0.5)
        case .bottom:
            originPosition = CGPoint(x: self.frame.width * 0.5, y: itemWidth * 0.5)
            finalPosition = CGPoint(x: itemWidth * 0.5, y: self.frame.size.height - (itemWidth + spacing) * CGFloat(i) - spacing)
        case .right:
            originPosition = CGPoint(x: itemWidth * 0.5, y: itemWidth * 0.5)
            finalPosition = CGPoint(x: self.frame.size.width - (itemWidth + spacing) * CGFloat(i) - spacing, y: itemWidth * 0.5)
        
        }
        
        //position
        positionAnimation.duration = animationDuration
        positionAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        positionAnimation.fromValue = NSValue(cgPoint: originPosition)
        positionAnimation.toValue = NSValue(cgPoint: finalPosition)
        positionAnimation.beginTime = CACurrentMediaTime() + Double(animationDuration) / Double(buttonsArray.count) * Double(i)
        positionAnimation.fillMode = kCAFillModeForwards
        positionAnimation.isRemovedOnCompletion = false
        button.layer.add(positionAnimation, forKey: "positionAnimation")
        button.layer.position = finalPosition
        
        
        //scale
        let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
        scaleAnimation.duration = animationDuration
        scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        scaleAnimation.beginTime = CACurrentMediaTime() + Double(animationDuration) / Double(buttonsArray.count) * Double(i) + 0.1
        scaleAnimation.fromValue = NSNumber(value: 0.01)
        scaleAnimation.toValue = NSNumber(value: 1.0)
        scaleAnimation.fillMode = kCAFillModeForwards
        scaleAnimation.isRemovedOnCompletion = false
        button.layer.add(scaleAnimation, forKey: "scaleAnimation")

        button.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
    }
    CATransaction.commit()
}

//MARK:- 隐藏按钮
func hideMenuButtons() {
    isExpand = false
    
    CATransaction.begin()
    CATransaction.setAnimationDuration(animationDuration)
    CATransaction.setCompletionBlock {
        self.frame = self.orginalFrame
        
        for button in self.buttonsArray {
            button.isHidden = true
            button.transform = CGAffineTransform.identity
        }
    }
    
    for i in 0..<buttonsArray.count {
        let button = buttonsArray[i]
        button.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
        
        //positionAnimation
        let positionAnimation = CABasicAnimation(keyPath: "position")
        let originPosition = button.layer.position
        var finalPosition = CGPoint.zero
        
        switch self.direction {
        case .top:
            finalPosition = CGPoint(x: self.frame.width * 0.5, y: self.frame.size.height - itemWidth)
        case .left:
            finalPosition = CGPoint(x: self.frame.width - itemWidth, y: itemWidth * 0.5)
        case .bottom:
            finalPosition = CGPoint(x: itemWidth * 0.5, y: itemWidth)
        case .right:
            finalPosition = CGPoint(x: itemWidth, y: itemWidth * 0.5)
        }
        
        //position
        positionAnimation.duration = animationDuration
        positionAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        positionAnimation.fromValue = NSValue(cgPoint: originPosition)
        positionAnimation.toValue = NSValue(cgPoint: finalPosition)
        positionAnimation.beginTime = CACurrentMediaTime() + Double(animationDuration) / Double(buttonsArray.count) * Double(i)
        positionAnimation.fillMode = kCAFillModeForwards
        positionAnimation.isRemovedOnCompletion = false
        button.layer.add(positionAnimation, forKey: "positionAnimation")
        
        //scale
        let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
        scaleAnimation.duration = animationDuration
        scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        scaleAnimation.beginTime = CACurrentMediaTime() + Double(animationDuration) / Double(buttonsArray.count) * Double(i) + 0.1
        scaleAnimation.fromValue = NSNumber(value: 1.0)
        scaleAnimation.toValue = NSNumber(value: 0.01)
        scaleAnimation.fillMode = kCAFillModeForwards
        scaleAnimation.isRemovedOnCompletion = false
        button.layer.add(scaleAnimation, forKey: "scaleAnimation")
   
    }
    
    CATransaction.commit()
    
}

//MARK:- 调整展开位置
func adjustExpandHeight() {
    let buttonHeight = (self.frame.height + spacing) * CGFloat(buttonsArray.count)
    var frame = self.frame
    
    switch direction {
    case .top:
        frame.origin.y -= buttonHeight
        frame.size.height += buttonHeight
        self.frame = frame
        
    case .left:
        frame.origin.x -= buttonHeight
        frame.size.width += buttonHeight
        self.frame = frame
        
    case .bottom:
        frame.size.height += buttonHeight
        self.frame = frame
        
    case .right:
        frame.size.width += buttonHeight
        self.frame = frame
    }
}
}
4. 使用
let menuView = BubbleMenuView(frame: CGRect(x: 200, y: 300, width: 40, height: 40), direction: .right)
    view.addSubview(menuView)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容