第 18 章:图标菜单

原文链接
作者:C4 开源项目
译者:Crystal Sun
全部章节请关注此文集C4教程翻译
校对后的内容请看这里

接下来,想把图标添加到菜单里,然后实现动画效果。图标的设计理念非常直接:一开始像是一个点,慢慢变大,移动到最终的位置,出现全部的形状,如下图:

1. 定位

这部分有技巧的地方不是动画和移动效果,这些都简单,而是每个姓朱的位置,这样才能一开始是个点,慢慢转变成全部的形状。有一些方法可以实现这种效果:

  1. 创建一个点和一个形状(shape),一直隐藏形状知道点移动到自己的位置,隐藏点,显示形状。
  2. 创建一个点,让这个点成为形状的一部分,移动点到某个位置,然后把点转变成完全的形状。
  3. 有一个点,但不是真的点,只是看起来像是一个点,设置形状的 strokeEnd 在右边,形状的 lineCap.Round

我们使用的是第三种方法 strokeEnd 来管理图标的动画。然而,问题也出现了,就是每个形状的“初始”点各不同 —— 我们需要知道每个形状,在点状态下或者在全部出现状态下的偏移量

下面是摩羯座的图标,开始点已经高亮标注出了:

开始点的位置是 {30.0,12.2},对应的 frame 是 {0.750,0.387}

创建一个“点”的效果,我们只需要给每个图标设置:

shape.strokeEnd = 0.001
shape.lineCap = .Round

如果我们用图标的中心点定位,结束和开始状态下的菜单看起来会是下图这样:

如果我们使用第一个点作为形状的 anchorPoint,我们会得到下面这样的效果:

我们的想法是,在结束状态下使用 anchorPoint,另外一个状态使用 center。不过,如果我们不停的转换位置,实现起来会很复杂。所以我们必须使用 anchorPoint 计算实际的 center,使用这个 center 在不同的状态下来换转换。

开始行动吧。

2. 获取图标

第一步就是从符号库里获取图标,更新图标的锚点。

打开 MenuIcons.swift 文件,在类里添加下列代码:

func taurus() -> Shape {
    let shape = AstrologicalSignProvider.sharedInstance.taurus().shape
  shape.anchorPoint = Point()
  return shape
}

func aries() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.aries().shape
  shape.anchorPoint = Point(0.0777,0.536)
  return shape
}

func gemini() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.gemini().shape
  shape.anchorPoint = Point(0.996,0.0)
  return shape
}

func cancer() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.cancer().shape
  shape.anchorPoint = Point(0.0,0.275)
  return shape
}

func leo() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.leo().shape
  shape.anchorPoint = Point(0.379,0.636)
  return shape
}

func virgo() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.virgo().shape
  shape.anchorPoint = Point(0.750,0.387)
  return shape
}

func libra() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.libra().shape
  shape.anchorPoint = Point(1.00,0.559)
  return shape
}

func pisces() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.pisces().shape
  shape.anchorPoint = Point(0.099,0.004)
  return shape
}

func aquarius() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.aquarius().shape
  shape.anchorPoint = Point(0.0,0.263)
  return shape
}

func sagittarius() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.sagittarius().shape
  shape.anchorPoint = Point(1.0,0.349)
  return shape
}

func capricorn() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.capricorn().shape
  shape.anchorPoint = Point(0.288,0.663)
  return shape
}

func scorpio() -> Shape {
  let shape = AstrologicalSignProvider.sharedInstance.scorpio().shape
  shape.anchorPoint = Point(0.255,0.775)
  return shape
} 

每个方法都从符号库里获取了图标,设置锚点为图标的开始点。我们不需要知道符号的其他信息了(比如:大/小 点和线等等)。所以我们只需返回修改过锚点的形状。

接下来,我们还想能存储使用的符号的副本,之后我们会对这些符号进行操作。所以,创建一个形状词典变量来存储符号和方法,这些方法会给符号创建格子的风格:

var signIcons : [String:Shape]!

接着,把下列方法添加到类里:

func createSignIcons() {
    signIcons = [String:Shape]()
    signIcons["aries"] = aries()
    signIcons["taurus"] = taurus()
    signIcons["gemini"] = gemini()
    signIcons["cancer"] = cancer()
    signIcons["leo"] = leo()
    signIcons["virgo"] = virgo()
    signIcons["libra"] = libra()
    signIcons["scorpio"] = scorpio()
    signIcons["sagittarius"] = sagittarius()
    signIcons["capricorn"] = capricorn()
    signIcons["aquarius"] = aquarius()
    signIcons["pisces"] = pisces()
    
    for shape in [Shape](self.signIcons.values) {
        shape.strokeEnd = 0.001 //in combination with the next two settings
        shape.lineCap = .Round  //strokeEnd 0.001 makes a round dot at
        shape.lineJoin = .Round //the beginning of the shape's path
        
        shape.transform = Transform.makeScale(0.64, 0.64, 1.0)
        shape.lineWidth = 2
        shape.strokeColor = white
        shape.fillColor = clear
    }
}

我们从库里拿出来的符号还是原始符号,我们需要缩小一下,如果我们不进行这行操作:hape.transform = Transform.makeScale(0.64, 0.64, 1.0),符号的形状就会超级大,如下图:

3. 目标位置

接下来,我们需要计算每个形状的目标位置,存储到两个数组中,在类里添加下面的代码:

var innerTargets : [Point]!
var outerTargets : [Point]!

接着添加下列方法:

func positionSignIcons() {
    innerTargets = [Point]()
    let provider = AstrologicalSignProvider.sharedInstance
    let r = 10.5
    let dx = canvas.center.x
    let dy = canvas.center.y
    for i in 0..<provider.order.count {
        let ϴ = M_PI/6 * Double(i)
        let name = provider.order[i]
        if let sign = signIcons[name] {
            sign.center = Point(r * cos(ϴ) + dx, r * sin(ϴ) + dy)
            canvas.add(sign)
            sign.anchorPoint = Point(0.5,0.5)
            innerTargets.append(sign.center)
        }
    }
    
    outerTargets = [Point]()
    for i in 0..<provider.order.count {
        let r = 129.0
        let ϴ = M_PI/6 * Double(i) + M_PI/12.0
        outerTargets.append(Point(r * cos(ϴ) + dx, r * sin(ϴ) + dy))
    }
}

上述过程使用了以下概念:

  1. 创建形状,通过 anchorPoint 方法来给每个形状设置起始点
  2. 调用 shape.center 实际上返回的是形状的位置 anchorPoint,相对于每个形状的 superview
  3. 调整 anchorPoint 到形状的中心(例如 {0.5,0.5}),不会改变形状的位置
  4. 重置 anchorPoint 后,我们能够获取形状实际的中心位置,作为目标

最后,在 createSignIcons 的最后,给所有的符号都设置了风格后,添加下列代码:

positionSignIcons()

现在,让我们看一下如何将这些排列在一起。

3.1 检查一下

将菜单的 backgroundColor 值改成 C4Purple,创建符号图标,setup() 方法应该像下面这样:

public override func setup() {
    canvas.backgroundColor = COSMOSbkgd
    createSignIcons()
}

WorkSpace 里,更新 setup() 方法,如下:

override func setup() {
    canvas.add(MenuIcons().canvas)
}

运行程序,效果如下:

4 图标的动画效果

有四个不同的动画需要创建:位置的出/入,形状的出现/隐藏。所以,创建四个动画变量:

var signIconsOut : ViewAnimation!
var signIconsIn : ViewAnimation!
var revealSignIcons : ViewAnimation!
var hideSignIcons : ViewAnimation!

增加下列方法:

func createSignIconAnimations() {
}

需要四个动画是由于设计原因,的吧是线在出入时不同的方向有不同的顺序。

出:移动,出现;入:隐藏,移动。

由于模式正好相反,当动画开始后,我们需要获取单独的动画来应对移动和隐藏,形成序列。在方法里添加如下内容:

revealSignIcons = ViewAnimation(duration: 0.5) {
    for sign in [Shape](self.signIcons.values) {
        sign.strokeEnd = 1.0
    }
}
revealSignIcons?.curve = .EaseOut

hideSignIcons = ViewAnimation(duration: 0.5) {
    for sign in [Shape](self.signIcons.values) {
        sign.strokeEnd = 0.001
    }
}
hideSignIcons?.curve = .EaseOut

需要标注一下:

  1. 将每个动画分开,更容易在每一步进行自定义
  2. 能够明确每个方向的持续时间
  3. 把 stroke 设置回 0.001,保存点

现在,把下列代码添加到 createSignIconAnimations

signIconsOut = ViewAnimation(duration: 0.33) {
    for i in 0..<AstrologicalSignProvider.sharedInstance.order.count {
        let name = AstrologicalSignProvider.sharedInstance.order[i]
        if let sign = self.signIcons[name] {
            sign.center = self.outerTargets[i]
        }
    }
}
signIconsOut?.curve = .EaseOut

//把图标移动到结束位置
signIconsIn = ViewAnimation(duration: 0.33) {
    for i in 0..<AstrologicalSignProvider.sharedInstance.order.count {
        let name = AstrologicalSignProvider.sharedInstance.order[i]
        if let sign = self.signIcons[name] {
            sign.center = self.innerTargets[i]
        }
    }
}
signIconsIn?.curve = .EaseOut

我们在这里获取了图标,设置图标在内环时的中心点和在外环时的中心点。

4.1 查看一下效果

调用出现和移动动画来测试一下,如下:

func animOut() {
    delay(1.0) {
        self.signIconsOut?.animate()
    }

    delay(1.5) {
        self.revealSignIcons?.animate()
    }
    
    delay(2.5) {
        self.animIn()
    }
}

func animIn() {
    delay(0.25) {
        self.hideSignIcons?.animate()
    }

    delay(1.0) {
        self.signIconsIn?.animate()
    }

    delay(2.5) {
        self.animOut()
    }
}

setup() 里调用 animOut(),如下:

public override func setup() {
    canvas.backgroundColor = COSMOSbkgd
    createSignIcons()
    createSignIconAnimations()
    animOut()
}

效果如下:

5. 打扫一下

删除 animInanimOut 方法,修改 setup() 方法如下:

public override func setup() {
    canvas.frame = Rect(0,0,80,80)
    canvas.backgroundColor = clear
    createSignIcons()
    createSignIconAnimations()
}

获取 MenuIcons.swift.

干净利索!

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,900评论 25 707
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,478评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,105评论 5 13
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,068评论 4 62
  • 即使不见面,不说话,不发信息,心里总会留一个位置,安安稳稳的放着一个人。一句话,一件事,时间或长或短的变数,但有的...
    何无所求阅读 407评论 0 0