好长时间没有关于 Epoch 的消息了
放寒假后 因为工作站迟迟没有到位 而我的 Mac 性能又不足以坚持去做虚幻引擎 以及一些其他的原因 我就转换回 Unity 工作了 反正整个游戏一直处于原型的状态 两个引擎的原型版本都做过 也没有什么前功尽弃一说
Freelancer 和 Galaxy on Fire 2 这两款游戏都比较好 我在参考他们的游戏模式 顺带一说 GOF2 HD 这款当初给 iPhone 5 的游戏 现在用 iPad 玩 很明显能看出 Skybox 的分辨率不够 而且 后面的星球是一个平面的贴图 甚至都不是一个 Sphere
这两天的工作结果:
修复坚果2.5系统刷第三方Rec变砖
情况如下:
系统挂了 进入不了Recovery
开机黑屏自动进fastboot 自然adb也挂了
不过fastboot devices
有显示设备
但fastboot flash recovery
的时候 出现如下错误:
target reported max download size of 268435456 bytes
sending 'recovery' (16384 KB)...
OKAY [ 0.517s]
writing 'recovery'...
FAILED (remote: unknown reason)
finished. total time: 0.522s
解决:
使用fastboot boot
加载一个系统的Recovery 然后进入系统Rec后就简单多了 开启sideload 后刷进去老版本的坚果Smartisan OS
2016.1.11
现在在北京T2的肯德基里面 吃着第二份餐
swift大会。
见到了KevinZhow,人挺逗,黑人技术和Martin一个级别的,hhhh,其实要不是上学我挺想去他们那写unity。
见到了MartinRGB,把我黑得太惨了。如果暑假Martin还在RavenTech我就去那里实习。
见到了Cee,和菊苣一个房间~。
见到了烧碱,今年估计还有很多机会去杭州hhhh。
此外,见到了梁杰和SwiftGG的很多人,比如爱画画的MMoary,也算是正式加入了很污的翻译组,哈哈哈。
唐巧大大采访了陈叔和Martin(这个过程Martin还不忘黑我,hhh)。
我没买到喵神新书 不过和喵神拍了合照,挺激动的。喵神这次的ppt很有趣 :-) 此外喵神还对stillness提了些建议,感觉可以寒假做个新版本了。
陈叔是作为大会工作人员一直在忙(当然一直和martin互黑),然后今天结束后很早就睡了。
后面的事情。
再过四个小时飞回长沙。
再过一天去补考大物。
再过几天回到北京参加Uber的hackathon。
然后应该是回家。不过也有可能去厦门,某些外包方面的事宜。
还有很多计划中的,个人或团队的Project。
有趣的生活。
坚果手机使用记
型号: 坚果手机 (标准版)32G 红色背壳
订购时间: 2015年最后一天
价格: ¥799
使用了一个星期 简单谈下坚果手机的优缺点
为了避免不必要的麻烦 我说下 我这个体验更多地是以一个 喜欢折腾的 视角来写的 因此并不一定适合所有人
TL;DR. :
如果你和我一样 身份是开发者 是想看下锤子的 额 怎么说 设计风格吧 那么请买台 Nexus 后自己安装锤子桌面 看腻了就可以随时卸载 干净利索
如果你是设计师 没有像我一样奇奇怪怪的手机用法 只是想体验下 Smartisan 我觉得很适合
如果你是打算买一台给父母/老年人 那么你可以在红米和坚果之间选一个 坚果给老年人用的好处很明显 界面拟物风格 学习成本很低 而且如果你还凑巧有另一台锤子 可以用远程遥控
Pros:
- : 锤子桌面 算是坚果手机最具有溢价的内容了 使用体验不错 主要体现在桌面的交互和重新绘制的图标上
- : 背壳设计 漂亮的设计 而且并没有不耐脏一说 虽然我用的时间很短 但是我用电子产品以不爱惜著称 以我的使用风格来看 并不会变脏或者纳垢 因为本身纹路就非常浅
- : 相机 满足需求 甚至锐度有些出乎我的意料 当然我估计是因为我用惯我那个相机进水后自带柔光滤镜的 iPhone 了 所以看其他手机相机都很清晰 因此这点不是很确定
Cons:
- : 系统稳定性 拿到手后升级 Smartisan 2.5版本(基于 Android 5.1) 结果手机无限重启 这对于我来说不算什么太大的事情 因为我能进recovery刷回 Smartisan 2.1版本 不过对于很多不是熟悉安卓操作的人来说 如果手机出现这类情况就只能送修 刚到手就出毛病这种事情实际上是很打击信心的 我当时也很烦躁 甚至想退掉这款手机
- : 割裂感 界面的确是漂亮 不过很多时候安卓定制 UI 都会遇到一个情况 你做再多的风格化定制 也是不可能变成 iOS 的 特别是像锤子这种设计风格和谷歌官方完全割裂的东西 就算你重绘了几万个 icon 也只是沧海一粟 而且系统软件和很多 MD 风格的第三方软件虽然图标统一了 进入界面后还是完全不一样的交互逻辑 特别是锤子学 MIUI 改了系统控件的风格 比如 toast 变成了淡黄色背景 在 MD 软件上显示 toast 时这种割裂就更加明显
其他&后续
前面说给父母用不错 为什么不直接推荐父母用 iPhone ?
我推荐过 特别是现在安卓的确不如 iOS 安全 不过我的父母并没有接受
我爸从功能机用到 Windows Mobile 再用到 Android 可以算是父母这辈人里面的 Power User 了
但问题就出现在这个 Power User 的定位上 很多时候 iOS 的使用是傻瓜化的 这反倒让我爸的思维定势没法习惯 比如:我爸习惯于自己动手转码电影 自己刻盘 这些操作都是一个内容产生者的身份 而不是消费者的身份 而内容产生者是强调掌控的 因此我爸在手机上也是习惯于获得对手机的掌控
比如通过 360 的内存加速球来获得掌控感(虽然那个加速球并没有什么用)
我妈接触过 iPad 后其实能看得出挺喜欢 iOS 的操作 不过扁平化的风格让她第一开始也有很多困惑 比如什么可以按 什么没法按 不过在接受程度上比我爸好很多
那么对于我来说 坚果手机就是一个不错的选择:本身是安卓 我爸可以按照自己的喜好装音乐和电影 然后开开加速球 而且风格是拟物 我妈很适合用 她用MIUI的老版本就很顺手 相信Smartisan这个交互反馈更突出的系统也很顺手
我用坚果做什么?(我对坚果的需求定位?)
我 iPhone 的流量不够 买个双卡手机 拿来插卡当随身Wi-Fi
装些奇奇怪怪的 iPhone 上没有的软件
用这个手机听音乐 因为我的 iPhone 实在是没有空间可用了
在我换 iPhone 之前 做一个备用机 当作衔接和过渡
这个手机接下来要怎么用?
我也不太清楚 先用到我寒假回家 然后看父母有没有兴趣接手 不过如果界面对他们不是很吸引的话 他们估计宁愿继续用 Galaxy Note 毕竟手机性能不是一个等级的
我尝试过把坚果当 Nexus 用 不过并不行 光是 root 就有些费劲 首先要刷第三方recovery 然后刷superSU 然后你才能考虑去安装Xposed
不过系统定制(限制)的东西太多 我现在想用Google即时桌面都没有办法 因为按home键并没有出现选择默认Launcher的窗口 只能认为锤子做了一些限制 虽然肯定可以突破 但是太费劲了 我懒得搞
(我是喜欢折腾 不过我对折腾的定义是 按照喜好发挥手机的最大效用 而如果系统做了如此之多限制 那么其实并没有折腾的必要了 因为有这个时间 完全可以买台 Nexus 了 上手即rooted 然后后面的事情才叫折腾)
照这么看 我估计还是需要一台 Nexus
一个奇怪的氛围
另外 记一个很奇怪的事情
锤子论坛里面对root的反应很奇怪
凡是类似的帖子下 都会有人说 没必要root 这个手机不需要root 你root就没保修了 甚至有人说 你不要为了root而root
这和 XDA-developer里面的氛围很不一样 甚至和一般品牌手机论坛都不一样 我只能认为 这里的人不爱折腾
我是不是可以进一步认为 坚果手机原来就是给老年人用的 而不是像宣传里面是给年轻人用的
我用手机是我的自由 你提醒我root后不保修一次就行 我自己知道我在做什么
我需要用Xposed Greenify 甚至偶尔玩下zANTI 这些东西都是要root才能发挥作用的东西 即使这是个老人机 我花了钱买的 我想把它改装成超级计算机都不需要人来管我
如果要写小说的话
有朋友和我说,她一直想写以自己为蓝本的小说,但是总是半途而废。
实际上,我以前也有过类似的念头。所以我很能明白为什么半途而废。
小说毕竟是一种作者手动梳理出的现实,其内容是经过筛选的。而少有作者能把平淡的生活描绘的有趣,大部分作者在筛选的过程中都只能尽力把激动人心的事情聚合在一起,因此看上去足够吸引人,但永远不会发生在自己身上。
因此,有的人会为自己的生活不够精彩而苦恼,认为没有什么可以写的。
不过如果作者越是在意这点,那么作品就越是会不可避免地堕落下去。如果要以自己为蓝本写东西,就要克服很多夹杂着自我感动的情感,而如果很在意自己有什么可以写的,在思索中就很有可能凭空自我感动起来,但这种感动是不能传递给读者的 —— 自我感动是情绪堆积起来的,因此是短暂的,也是个人化的。
因为自我感动很短暂,所以很多人隔一段时间看以前自己写的东西,都会觉得很矫情,包括我在内。又因为自我感动是个人化的,所以作者很难分辨出什么才是构建出这种感动的要素 —— 如果不能把真正构建情感的事物一一写下,或是写了一堆无关的事物,读者就不可能通过文字构建出相似的情感,这种语境上的相似既然不能构成,便也无从让读者真正地理解自己的感动,换句话说,会让读者觉得矫情。
那么如何解决这种事情呢,一个是就当做在平平淡淡地写日记,不用极力搜寻情感式的东西。事情有不有趣其实还是看个人如何看待。
另一个就是什么也不写,本来也就没什么好写的,如果你也这么觉得的话。当然我没有这么和我的朋友说,但我目前是觉得我没有把普通事物描绘得有趣的能力。
那我为什么还在我的博客上不停地写东西呢,正是为了不让自己愈发地无趣,毕竟谁也不希望自己沦落到给键盘都没话说的地步,多锻炼还是好的。
(我发现我今天少有地用了标点符号。)
Substance
今年最后一个周末是在西湖边上度过的 和陈叔一起 在美院参加hackathon
简介下结果 遇见了来自四川的做电子音乐的一位老师和他的学生 以及美院的一位读博的学姐 然后一起做了一个很有趣的项目 大致可以见 iOS 部分 Github 地址
写了一个风格很漂亮的iOS端应用 见识了电子音乐专业的软件和技术 认识了一些有趣的人 而且拿到了二等奖(需要提及的是 这次比赛没有一等奖)
附带一说 对美院整体印象很不错 有时候在想如果当时没有停下来素描 走艺术生那一条路 是不是有可能变化很大
这一年下来 我发现我其实是一个很不安分的人 相对于呆在学校上课 我更喜欢做有趣的事情 然后就会有很多冲突
父母之前一直希望我能在学校好好读书 但令他们沮丧的是 我在大学就没怎么认真读过书 大一参加了学长的创业公司 大二频繁出去参加hackathon 学校的事情我能逃就逃 然后我就挂了几门科 我其实也觉得挂科无所谓了 既然我有真正喜欢的事情去做 学校的事情算什么呢
然后 让我觉得比较宽慰的是 我的父母也很开明地适应了我的变化 只要求我能拿到中南的毕业证就行
父母总是说用人单位还是要看毕业证的 这话其实在他们的语境下是成立的:中小城市的企事业单位肯定是要大学毕业证的 不过我并不打算去这类地方发展 真正适合我的地方也许并不需要毕业证 而是拿出 github 和 dribbble 页面就能进公司
其实这还涉及到一个问题 就是你是否能够牛逼到让公司直接要你的地步 而现实中的一个共识是 大多数人都肯定是平庸的 在不能确定自己是不是那种天才的时候 我想大部分人肯定选择最稳妥的方案
但是很有可能事实是这样的 天才并不一定天资聪颖 而是天才选择了不保守的道路 所以才成为了他人口中的天才 毕竟 承认自己天资不够 总比承认自己不够勇敢更加容易些
话归如此 这个世界还是成王败寇的 选择不保守道路然后挂掉的人其实也大有人在 至于能不能成功 完全靠自己
然后就是 大二暑假的实习 如果没有大变化的话 就已经确定了
其实我内心还盘算着能不能整个大三都呆在北京 需要考试再回学校 对于我来说 我已经知道自己目前喜欢做什么 擅长做什么 而学校对于我来说并没有太多用处 —— 我的技能不是上课习得的 我的兴趣爱好也不是学校引导建立的
参加hackathon 在公司工作 做些三维和游戏 或者写开源的东西 都是远比学校有趣的事情
学校是一个短时间内看不到成果的地方 只有理论 —— 而我恰好是那种信奉 quick hack 的人 什么事情都是不看理论先实践一番 有需要的知识再查找 我并没有觉得我这种方式有什么错
很多人说你现在学的知识都是给后面做储备 但是我不明白这是什么逻辑 —— 这是说毕业就不能学了么 自学的能力在哪里?现在学的知识要留到以后用 那么为什么必须现在去学习?
更为重要的是 学校本身是一个眼界有限的地方 —— 这并不是说学校的资源有限 而是说 真正能接触到学校资源的人总是少数 学校里各类名额总是仅有的那么几个 导致很多时候学校里大部分人并不能得到足够的机会去一览更高的境界 因此总体上 学校学生的眼界是极其落后而有限的 呆在这种环境 不如出去多见识
说实话 三个月前我是不敢想象我现在能够认识这么多大神 当时还是一个小透明一样的
然后参加hackathon 写开源 竟然加上了很多以前默默关注的人 有时候觉得很多变化不过一两个月
所以说 事情顺其自然 反倒会有所进展
我和陈叔(加了高斯模糊) 哈哈哈
从Sketch到AE/Prototype
前言
我不清楚其他人是怎么做的 因为我是个野生的做UI的 算不上设计师 不知道真正设计师圈子里面怎么做这个事情... 我就记录下我是怎么做的
设计一个图-导出到AE或原型工具-调整-出GIF
这个是我做原型的Workflow 当然如果要开始写代码的话是另外一套 在导出这个环节有些差异 也有相似
设计一个图-导出到xasset-代码实现-录屏出GIF
好了 代码实现就先不谈 先说AE和原型工具 而原型工具里面 我比较喜欢Principle
主要是 我对于原型工具的要求:
1. 不能只做到简单的Button/Push/Pop(说的就是你,Axure)
2. 而是要能够实现基本的一些效果 比如有Drag/Scroll/Page/补间动画
3. 但是呢 我又不想写代码来做这些事情 写代码很烦呐 而且我要说写代码 不如直接上ObjC咯 为什么还要学js(说的就是你,Framer.js)
那么分类如下:
比Storyboard都弱的腊鸡: Pop/Axure/Flinto
不写代码还能做些好玩的: Principle/QC/Keynote
要写我不会的Scripts的: Avocado/Framer
好 那么看Principle/QC/Keynote 这个完全是个人喜好了
MartinRGB说QC很接近程序员思维 但是我看QC的时候 刚好是我学iOS之前 所以我当时觉得很难 就放弃了 转而直接看iOS...
然后Keynote的话 用好“神奇移动”功能可以做得很屌 但是Keynote的线性逻辑(只有上一张和下一张 不能够像Storyboard那样有多个逻辑连线)不容易用来做实时交互 所以我一般用Keynote来做幻灯片吓唬只见过Prezi的老师用
那就谈Principle(有点安利的感觉)这个软件基本逻辑也是靠Storyboard 但是又有Keynote那种在两张图之间的补间过渡动画 还有自带的Scroll功能 我觉得刚好能做些快速的原型 实践下想法
什么?你说AE?...AE太强大了 放在最后说(逃
在Principle/AE里模拟 最主要的就是把层级关系做好了 导出需要的图片 或者在之前就做好相应的遮罩 这样在后面的软件中就不用费心思考虑了
比如这两个动画...
案例一:用Principle
这个怎么做呢 首先考虑下如何把元素分开:
1.页面内容直接用一整张图就好 但是记得不要把状态栏和底部的三个按钮给一起导到图里面 因为从GIF里面可以看到 它们的运动轨迹是不跟随页面内容的(状态栏一直保持在最上面 底部按钮也是各自的轨迹)
那么我们就准备三个不带状态栏和底部按钮的图作为页面内容:
2.底部的三个按钮 明显返回键和其他两个是不一样的 所以分开导出 (黑色底色为了看清楚)
3.导出状态栏 (灰色底色为了看清楚)
4.Page分页标记(三根竖线 随便画一下 没图)
图片元素区分/整理到这就OK了 其实只要整理做好了 后面导入Principle就很容易做了 这个动画只要四个Board就能完成
案例二:用AE
这个其实需要考虑到图片整理的地方很少 因为很明显 主要界面是个3D的 还有炫光 这个是你用任何现有的原型软件都做不出来的 只能上AE 带三维摄像机 带Optical Flare来做
主要要说的地方 就是Navigation Bar这个透明效果(translucent) 这个没法用Sketch画出来导出 因为Sketch的Background Blur效果是对于当前图片静态产生的 但是AE里面一旦Bar下面内容有变化 Bar的透明效果就必须要动
简单说下怎么做实时的translucent效果
使用调整图层 放一个在图层最上面 设置蒙板为整个Navigation Bar的大小 然后添加高斯模糊 这个时候 整个translucent效果是白色的
如果要设置translucent的tint color 就要在调整图层上面再加一个形状图层 颜色就是你的tint color 比如这个例子里面是黑色 然后设置透明度为66%左右 就OK啦
2015
距离今年过期还有一个月
一月 | 复习 考试 放假 回家 准备做合肥一中的加油视频 从Mapbox上抓了很多地图 好像还在讨论微电影的拍摄预算问题
二月 | 去了趟高中聚会 发布了加油视频 买了域名 过年 在生日当天吵架 在生日第二天回学校
三月 | 开学 学Unity 翘了一星期课 CreationBox上线 觉得缺钱花 又不知道怎么赚
四月 | 买了苹果开发者帐号 上架EVE Model Viewer 参加了学校的一个软件外包比赛 被刷
五月 | 继续参加学校所谓的创新比赛 被刷 开始做手机小游戏
六月 | Stillness上线 天气变热 买了星球大战的T-shirt
七月 | 暑假 待在寝室 做外包 熬夜 吃烤串 听各种电子乐 回家 剪掉留四五个月的头发 高中聚会
八月 | 过暑假 吃 睡 看小说 另一款EVE Model Viewer上架 后半个月跑去游泳 换了新Mac 回学校
九月 | 学MODO建模 上课
十月 | 在Twitter上认识了一些有趣的人 去北京 和陈叔一起参加了黑客马拉松 想明白了一些事情
十一月 | 有了Dribbble_Player帐号 开始写一些开源的动画实现 认识了很多大神
词条
-
分享 获得信息多少的层级分化 大多起源于人为刻意构筑的学习成本 在任何知识领域 零学习成本的知识都是最珍贵的 因为这给任何人在相关专业上一个向上流动的机会 所以保持分享信息的习惯 不论用的是开源协议还是Creative Commons
-
遮掩 自己认为对的事情 怎么做都不需要对别人遮掩 如果有人觉得写博客很矫情 那是他的认为 如果有人觉得看社科很装逼 那也是他的认为
-
武断的评价 聆听别人的话语 不要着急用自己的价值观进行武断的评价 你不需要对任何刚听到的事情都评价 进而获得对自己的肯定 那是自卑的表现 更何况 很多事情并不如你所料
-
利己主义 我并不排斥利己主义 很多别人口中“精致的利己主义者” 其实并没有什么道义上的缺陷 只不过吃相难看了点 话说回来这个社会谁不是利己的 我很喜欢利己主义 毕竟现在我连自己都没能养活 连自己都没能养活的人 说什么不齿利己主义 简直是笑掉大牙
-
眼界 眼界是辅助一个人做判断的关键 什么可能 什么不可能 什么是最有价值的 这些东西都要到一定层度才可以分辨
-
重复 重复工作而没有新知识获得 基本上就是失败的 要么不要做重复的工作 要么做得多样 否则就毫无价值
-
原教旨 在讨论日常生活的一些原教旨情节时 记得思考 是不是因为自称原教旨让你获得了更高贵的感觉 所以才如此 这点 无论是对于小众音乐 冷门电影 还是写代码 都是如此
-
错过 不要一直想着去听其他人在说什么 不要怕因为没有听别人在说什么而漏过错过什么事情 很多人的话语其实信息量很低 不听也罢
tvOS视差按钮的ObjC实现
这是为Animatious 一起动画开源组写的文章 请关注我们!!!
介绍
苹果在最新发布的Apple TV里引入了有趣的图标设计
具体说来 图标由2-5个分层图层构成 在图标被选中的时候 图标内每个图层进行不同幅度的位移 从而形成视觉上具有深度距离感的视差效果 图标构成和效果可以见视频:
原理
假设我们已经有了以下图片:(你可以从下载链接下载已经分层的四张图片)
基于这四张图片 我们该怎么对其进行变换来达到tvOS的视差效果呢?
重新观察上文中苹果官方的例子视频 我们可以得出以下结论:
1. 总图层在旋转 但不同于一般在屏幕平面上的旋转 而是相对屏幕具有一定夹角的旋转
如果不太了解这种旋转是怎么发生的 我们可以看一张有关 CATransform3D 的图:
我们常用的 CGRect 有 X 和 Y 两个位置参数 而 CATransform3D 可以理解为在日常的两个轴以外加了 Z 轴 方向为从手机上表面竖直向下 如图
这么一来 我们日常所见的在屏幕平面上的旋转(比如屏幕旋转)其实是绕 Z 轴旋转
而绕 X 和 Y 轴的旋转 便是让 CALayer 具有相对屏幕具有一定夹角的旋转 具体表现就是如同tvOS按钮一样有远近效果(其实在透视效果上还是有些不一样 后面会提怎么解决)
CATransform3DMakeRotation
就是这么通过旋转角度定义的:
CATransform3D CATransform3DMakeRotation (
CGFloat angle, //绕着向量(x,y,z)旋转的角度
CGFloat x, //x轴分量
CGFloat y, //y轴分量
CGFloat z //z轴分量
);
2.除去旋转 每个图层都在进行不同半径的圆周移动
为什么有移动?
透视是创造三维深度感觉的关键 而透视效果最直白的话来说 就是“近大远小”
让我们来看个例子吧 :-)
如果只有总图层自转 分图层不进行移动 那么整个按钮虽然有自转效果 但是看起来还是平的
如果要保证有三维效果 就要有视差 即近大远小 让远处的图层移动的距离很小 近处的图层移动距离很大(大家可以自行想象同样速度远处近处的汽车 看起来移动的距离也不一样)
因此 就要令分图层进行圆周移动 离我们近的图层 圆周半径要更大些 保证看起来移动的距离更大
我们简单地用 Principle 做了一个原型 大体效果应该是这个样子的 中间的圆点是移动的轴心
3.总的图层也会移动
看到我们刚才那个简陋的效果了没?你有可能会想 为什么看起来和tvOS差别那么大?
原因是 tvOS实现的效果 整个按钮并没有明显地移动 而是近似于固定在某个位置的 这么一来 就要求我们在分图层移动的时候 总图层叠加一项反方向的移动 保证按钮固定住
于是我们又用 Principle 做了一个原型 大体效果应该是这个样子的
4.高光的移动方向恰好相反
高光就是我们在tvOS的图标上看到的白色反光 这个部分其实很简单:
用PS画一个白色的圆 加上模糊效果 就是一个 高光图层
让图层在移动的时候于其他图层方向相反 即让图层叠加之后的效果为 高光永远在离我们最近的地方 这里说起来会有点困惑 但是用代码实现的时候就自然明白了 ^_^
实现
注:实现部分限于篇幅 不可能将所有代码都粘贴出来 只是在几个关键的地方粘贴出来加以说明
完整代码见 https://github.com/JustinFincher/JZtvOSParallaxButton
1.按钮基本
按照我们的计划 这个按钮默认并没有三维效果 就是很多UIImage叠加起来 只有当我们长按的时候 才会有各种旋转和移动
这里动画方式分为两种 第一种是自动动画 首先会移动和旋转到一个特定的角度 然后便开始周期移动了 第二种是手动动画 按钮会跟随手指Drag进行旋转和移动
让我们先新建一个UIButton吧 :-)
//JZParallaxButton.h
# import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, RotateMethodType)
{
AutoRotate = 0, //自动动画
WithFinger, //跟随手指
WithFingerReverse, //跟随手指 但反向
};
@interface JZParallaxButton : UIButton
@end
//JZParallaxButton.m
# define LongPressInterval 0.5 //自动动画状态下的长按判断时间
@interface UIButton ()<UIGestureRecognizerDelegate>
@end
@implementation JZParallaxButton
@end
写一个自己的init方法 然后在里面加入长按的手势判断
//JZParallaxButton.m
- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
WithLayerArray:(NSMutableArray *)ArrayOfLayer
WithRoundCornerEnabled:(BOOL)isRoundCorner
WithCornerRadiusifEnabled:(CGFloat)Radius
WithRotationFrames:(int)Frames
WithRotationInterval:(CGFloat)Interval
{
self = [super initWithFrame:RectInfo];
//...省略...
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(selfLongPressed:)];
longPress.delegate = self;
longPress.minimumPressDuration = LongPressInterval;
//self就是UIButton了 所以可以对self add
[self addGestureRecognizer:longPress];
return self;
}
//JZParallaxButton.m
//长按会触发的方法
- (void)selfLongPressed:(UILongPressGestureRecognizer *)sender
{
CGPoint PressedPoint = [sender locationInView:self];
NSLog(@"%F , %F",PressedPoint.x,PressedPoint.y);
//可以读取当前按下的位置
}
2.层级关系和逻辑判断
我们的按钮在实现后应有以下层级:
ParallaxButton:UIButton //我们建立的UIButton SubClass
|-BoundsView:UIView //总视图
|--LayerImageView1:UIImageView //分视图 是总视图的SubView
|--LayerImageView2:UIImageView
|--LayerImageView3:UIImageView
|--LayerImageView4:UIImageView
|--LayerImageView5:UIImageView
|-- ....
|--LayerImageViewX:UIImageView
有的同学有可能会觉得 为什么需要总视图这个 BoundsView
呢 直接将所有的UIImageView都划归为UIButton的SubView不就好了么?
实验过直接将UIImageView添加到UIButton为SubView后 我有一个相对合理的解释:
我们之前分析原理的时候 说明其实是只有总图层(即 BoundsView
)在旋转的 分图层只需处理移动问题
如果去除了总图层 就只能让每个分图层(即 LayerImageView
)在移动的同时都旋转 这势必带来一个问题 那就是会有“厚度”的感觉
让我们实验下 如果层级关系如下图 会是什么结果
可以看到 这里的图层移动方式和原型里的效果已经很接近了 但是因为分图层移动半径不一 并且没有总图层进行约束 导致分图层的显示区域不在一个长方形里 看起来像是有厚度了一样 整个按钮实际看起来并没有tvOS按钮里的那种轻盈感
因此 需要有总图层进行约束 即将分图层添加为总图层的SubView 并设置总图层Layer的MasksToBounds为YES 这时 所有分图层的可见区域都受限制于总图层 无论怎么旋转和移动都不会出现厚度感了
我们现在将视图层级需要的一些变量写出来 然后再实现一些逻辑判断的代码 比如长按后需要做什么
//JZParallaxButton.h
# import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, ParallaxMethodType)
{
Linear = 0,
EaseIn,
EaseOut,
};
@interface JZParallaxButton : UIButton
//数组用于记录当前Button包含的所有ImageLayer 即分图层
@property (nonatomic,strong) NSMutableArray *LayerArray;
//button圆角
@property (nonatomic,assign) BOOL RoundCornerEnabled;
//button圆角
@property (nonatomic,assign) CGFloat RoundCornerRadius;
//是否在Parallax
@property (nonatomic,assign) BOOL isParallax;
@property (nonatomic,assign) int RotationAllSteps;
@property (nonatomic,assign) CGFloat RotationInterval;
@property (nonatomic,assign) CGFloat ScaleBase;
@property (nonatomic,assign) CGFloat ScaleAddition;
@property (nonatomic,assign) ParallaxMethodType ParallaxMethod;
@property (nonatomic,assign) RotateMethodType RotateMethod;
- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
WithLayerArray:(NSMutableArray *)ArrayOfLayer
WithRoundCornerEnabled:(BOOL)isRoundCorner
WithCornerRadiusifEnabled:(CGFloat)Radius
WithRotationFrames:(int)Frames
WithRotationInterval:(CGFloat)Interval;
//JZParallaxButton.m
# define RotateParameter 0.5 //用于调整旋转幅度
# define SpotlightOutRange 20.0f //用于高光距离中心的长度
# define zPositionMax 800 //Core Layer变换时摄像机默认z轴位置
# define BoundsVieTranslation 50 //总图层的移动幅度
# define LayerVieTranslation 20 //分图层的移动幅度
# define LongPressInterval 0.5 //自动动画状态下的长按判断时间
@interface UIButton ()<UIGestureRecognizerDelegate>
@property (nonatomic,assign) int RotationNowStep; //记录动画的当前状态
@property (nonatomic,weak)NSTimer *RotationTimer; //动画计时器
@property (nonatomic,strong) UIImageView *SpotLightView; //高光图层
@property (nonatomic,strong) UIView *BoundsView; //总图层
@property (nonatomic,assign) CGPoint TouchPointInSelf; //手指按下的时候在Button内部 的位置 用于Button设为跟随手指的时候
@property (nonatomic,assign) BOOL hasPreformedBeginAnimation; //判断是否在进行动画 防止动画未表演完就触发下一个动作 造成错位
@end
@implementation JZParallaxButton
//省略 @synthesize ...
- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
WithLayerArray:(NSMutableArray *)ArrayOfLayer
WithRoundCornerEnabled:(BOOL)isRoundCorner
WithCornerRadiusifEnabled:(CGFloat)Radius
WithRotationFrames:(int)Frames
WithRotationInterval:(CGFloat)Interval
{
//省略之前的代码....
LayerArray = [[NSMutableArray alloc] initWithCapacity:[ArrayOfLayer count]];
BoundsView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
BoundsView.layer.masksToBounds = YES;
BoundsView.layer.shouldRasterize = TRUE;
BoundsView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
if (self.RoundCornerEnabled)
{
BoundsView.layer.cornerRadius = self.RoundCornerRadius;
}
[self addSubview:BoundsView];
for (int i = 0; i < [ArrayOfLayer count]; i++)
{
UIImageView *LayerImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
UIImage *LayerImage = [ArrayOfLayer objectAtIndex:i];
[LayerImageView setImage:LayerImage];
LayerImageView.layer.shouldRasterize = TRUE;
LayerImageView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
//从下往上添加
[BoundsView addSubview:LayerImageView];
[LayerArray addObject:LayerImageView];
//如果把所有分图层都加完了
if (i == [ArrayOfLayer count] - 1)
{
//在最上层添加高光分图层
SpotLightView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width,self.frame.size.height)];
NSString *bundlePath = [[NSBundle bundleForClass:[JZParallaxButton class]]
pathForResource:@"JZParallaxButton" ofType:@"bundle"];
NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
SpotLightView.image = [UIImage imageNamed:@"Spotlight" inBundle:bundle compatibleWithTraitCollection:nil];
SpotLightView.contentMode = UIViewContentModeScaleAspectFit;
SpotLightView.layer.masksToBounds = YES;
[BoundsView addSubview:SpotLightView];
SpotLightView.layer.zPosition = zPositionMax;
[self bringSubviewToFront:SpotLightView];
SpotLightView.alpha = 0.0;
[LayerArray addObject:SpotLightView];
}
}
- (void)selfLongPressed:(UILongPressGestureRecognizer *)sender
{
CGPoint PressedPoint = [sender locationInView:self];
//NSLog(@"%F , %F",PressedPoint.x,PressedPoint.y);
self.TouchPointInSelf = PressedPoint;
if(sender.state == UIGestureRecognizerStateBegan)
{
//NSLog(@"Long Press");
self.hasPreformedBeginAnimation = NO;
switch (self.RotateMethod)
{
case AutoRotate:
{
//长按后 如果在进行视差效果就结束 如果现在是普通状态就开启视差效果
if (isParallax)
{
[self EndParallax];
}
else
{
[self BeginParallax];
}
}
break;
case WithFinger:
{
//手动动画结束视差效果并不依靠长按 而是通过判断手指是否留在屏幕上
if (!isParallax)
{
[self BeginParallax];
}
}
break;
}
}
else if (sender.state == UIGestureRecognizerStateEnded)
{
//如果长按结束但仍有动画在进行 就等待 LongPressInterval 再执行TouchUp方法 否则立即执行TouchUp
if (self.hasPreformedBeginAnimation == NO)
{
[self performSelector:@selector(TouchUp) withObject:self afterDelay:LongPressInterval ];
}
else
{
[self TouchUp];
}
}
}
@end
这里的 shouldRasterize
:
@property BOOL shouldRasterize
Description A Boolean that indicates whether the layer is rendered as a bitmap before compositing. Animatable
在某些时候 例如导入的图片分辨率较大 此时对UIViewImage进行CA动画 会出现锯齿 这个时候 可以通过设置 shouldRasterize
来解决
BoundsView.layer.shouldRasterize = YES;
BoundsView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
当然设置 allowsEdgeAntialiasing
也是一种方法 这里我们对比下 可以注意看右边墙壁上的树影 通过 shouldRasterize
设置后 基本上影子不会出现较大的变动:
3.透视效果需要实现的一个方法
//JZParallaxButton.m
//CATransform3DMake 默认给出的CATransform3D是没有透视效果的 需要手动加入这一段 完成从 orthographic 到 perspective 的改变
//这个方法的解析可以看 http://wonderffee.github.io/blog/2013/10/19/an-analysis-for-transform-samples-of-calayer/
CATransform3D CATransform3DMakePerspective(CGPoint center, float disZ)
{
CATransform3D transToCenter = CATransform3DMakeTranslation(-center.x, -center.y, 0);
CATransform3D transBack = CATransform3DMakeTranslation(center.x, center.y, 0);
CATransform3D scale = CATransform3DIdentity;
scale.m34 = -1.0f/disZ;
return CATransform3DConcat(CATransform3DConcat(transToCenter, scale), transBack);
}
CATransform3D CATransform3DPerspect(CATransform3D t, CGPoint center, float disZ)
{
return CATransform3DConcat(t, CATransform3DMakePerspective(center, disZ));
}
4.实现自动动画
这部分主要内容就是 在长按后 按钮会先进入某个特定的角度位置 然后进行自转
这里为了简单 使用了NSTimer进行每一帧的计数 但需要注意的是 NSTimer的精度不足以完成真正流畅的动画
自动动画里 总图层和分图层的移动 旋转都和两个参数有关:一是当前的计数位置(即) 而是图层在总按钮里的层级位置(即LayerArray里的i) 通过这两个参数进行计算CATransform3D
// JZParallaxButton.m
#define OutTranslationParameter (float)([LayerArray count] + i)/(float)([LayerArray count] * 2)
#define OutScaleParameter ScaleBase+ScaleAddition/5*((float)i/(float)([LayerArray count]))
@implementation JZParallaxButton
- (void)BeginAutoRotation
{
__weak JZParallaxButton *weakSelf = self;
//需要到达动画的起始位置
CGFloat PIE = 0;
CGFloat Degress = M_PI*2*PIE;
//NSlog(@"Degress : %f PIE",PIE);
CGFloat Sin = sin(Degress)/4;
CGFloat Cos = cos(Degress)/4;
int i =0;
//计算初始位置的旋转 移动 和缩放
CATransform3D NewRotate,NewTranslation,NewScale;
NewRotate = CATransform3DConcat(CATransform3DMakeRotation(-Sin*RotateParameter, 0, 1, 0), CATransform3DMakeRotation(Cos*RotateParameter, 1, 0, 0));
NewTranslation = CATransform3DMakeTranslation(Sin*BoundsVieTranslation*OutTranslationParameter, Cos*BoundsVieTranslation*OutTranslationParameter, 0);
NewScale = CATransform3DMakeScale(OutScaleParameter, OutScaleParameter, 1);
//使用CATransform3DConcat将三个CATransform3D结合成一个CATransform3D
CATransform3D TwoTransform = CATransform3DConcat(NewRotate,NewTranslation);
CATransform3D AllTransform = CATransform3DConcat(TwoTransform,NewScale);
//对BoundsLayer 即总图层进行动画
CABasicAnimation *BoundsLayerCABasicAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
BoundsLayerCABasicAnimation.duration = 0.4f;
BoundsLayerCABasicAnimation.autoreverses = NO;
BoundsLayerCABasicAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax)];
BoundsLayerCABasicAnimation.fromValue = [NSValue valueWithCATransform3D:BoundsView.layer.transform];
BoundsLayerCABasicAnimation.fillMode = kCAFillModeBoth;
BoundsLayerCABasicAnimation.removedOnCompletion = YES;
[BoundsView.layer addAnimation:BoundsLayerCABasicAnimation forKey:@"BoundsLayerCABasicAnimation"];
BoundsView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
//对LayerArray内的UIImageView 即分图层进行动画
for (int i = 0 ; i < [LayerArray count]; i++)
{
//对于不同的前后位置 需要移动的半径不一样
float InTranslationParameter = [self InTranslationParameterWithLayerArray:LayerArray WithIndex:i];
float InScaleParameter = [self InScaleParameterWithLayerArray:LayerArray WithIndex:i];
UIImageView *LayerImageView = [LayerArray objectAtIndex:i];
CATransform3D NewTranslation ;
CATransform3D NewScale = CATransform3DMakeScale(InScaleParameter, InScaleParameter, 1);
if (i == [LayerArray count] - 1) //是高光所在的View
{
NewTranslation = CATransform3DMakeTranslation(Sin*LayerVieTranslation*InTranslationParameter*SpotlightOutRange, Cos*LayerVieTranslation*InTranslationParameter*SpotlightOutRange, 0);
}
else //分图层其他的View 注意可以看到这里的 Translation 和高光是相反的
{
NewTranslation = CATransform3DMakeTranslation(-Sin*LayerVieTranslation*InTranslationParameter, -Cos*LayerVieTranslation*InTranslationParameter, 0);
}
CATransform3D NewAllTransform = CATransform3DConcat(NewTranslation,NewScale);
CABasicAnimation *LayerImageViewCABasicAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
LayerImageViewCABasicAnimation.duration = 0.4f;
LayerImageViewCABasicAnimation.autoreverses = NO;
LayerImageViewCABasicAnimation.toValue = [NSValue valueWithCATransform3D:NewAllTransform];
LayerImageViewCABasicAnimation.fromValue = [NSValue valueWithCATransform3D:LayerImageView.layer.transform];
LayerImageViewCABasicAnimation.fillMode = kCAFillModeBoth;
LayerImageViewCABasicAnimation.removedOnCompletion = YES;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:LayerImageViewCABasicAnimation,nil];
animGroup.duration = 0.4f;
animGroup.removedOnCompletion = YES;
animGroup.autoreverses = NO;
animGroup.fillMode = kCAFillModeRemoved;
[CATransaction begin];
LayerImageView.layer.transform = CATransform3DPerspect(NewAllTransform, CGPointMake(0, 0), zPositionMax);
[CATransaction setCompletionBlock:^
{
if (i == [LayerArray count] - 1)
{
//简单的周期循环
RotationNowStep = 0;
RotationTimer = [NSTimer scheduledTimerWithTimeInterval:RotationInterval/RotationAllSteps target:weakSelf selector:@selector(RotationCreator) userInfo:nil repeats:YES];
weakSelf.hasPreformedBeginAnimation = YES;
}
}];
[LayerImageView.layer addAnimation:animGroup forKey:@"LayerImageViewParallaxInitAnimation"];
[CATransaction commit];
}
}
//计时器每次触发要执行的方法
- (void)RotationCreator
{
__weak JZParallaxButton *weakSelf = self;
//NSlog(@"RotationNowStep : %d of %d",RotationNowStep,RotationAllSteps);
if (RotationNowStep == RotationAllSteps)
{
//一周完成 计数器置1
RotationNowStep = 1;
}
else
{
RotationNowStep ++ ;
}
CGFloat PIE = (float)RotationNowStep/(float)RotationAllSteps;
CGFloat Degress = M_PI*2*PIE;
//NSlog(@"Degress : %f PIE",PIE);
CGFloat Sin = sin(Degress)/4;
CGFloat Cos = cos(Degress)/4;
int i = 0;
CATransform3D NewRotate,NewTranslation,NewScale;
NewRotate = CATransform3DConcat(CATransform3DMakeRotation(-Sin*RotateParameter, 0, 1, 0), CATransform3DMakeRotation(Cos*RotateParameter, 1, 0, 0));
NewTranslation = CATransform3DMakeTranslation(Sin*BoundsVieTranslation*OutTranslationParameter, Cos*BoundsVieTranslation*OutTranslationParameter, 0);
NewScale = CATransform3DMakeScale(OutScaleParameter, OutScaleParameter, 1);
CATransform3D TwoTransform = CATransform3DConcat(NewRotate,NewTranslation);
CATransform3D AllTransform = CATransform3DConcat(TwoTransform,NewScale);
BoundsView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
for (int i = 0 ; i < [LayerArray count]; i++)
{
float InScaleParameter = [self InScaleParameterWithLayerArray:LayerArray WithIndex:i];
float InTranslationParameter = [self InTranslationParameterWithLayerArray:LayerArray WithIndex:i];
if (i == [LayerArray count] - 1) //is spotlight
{
UIImageView *LayerImageView = [weakSelf.LayerArray objectAtIndex:i];
CATransform3D Translation = CATransform3DMakeTranslation(Sin*LayerVieTranslation*InTranslationParameter*SpotlightOutRange, Cos*LayerVieTranslation*InTranslationParameter*SpotlightOutRange,0);
CATransform3D Scale = CATransform3DMakeScale(InScaleParameter, InScaleParameter, 1);
CATransform3D AllTransform = CATransform3DConcat(Translation,Scale);
LayerImageView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
}
else //is Parallax layer
{
UIImageView *LayerImageView = [weakSelf.LayerArray objectAtIndex:i];
CATransform3D Translation = CATransform3DMakeTranslation(-Sin*LayerVieTranslation*InTranslationParameter, -Cos*LayerVieTranslation*InTranslationParameter, 0);
CATransform3D Scale = CATransform3DMakeScale(InScaleParameter, InScaleParameter, 1);
CATransform3D AllTransform = CATransform3DConcat(Translation,Scale);
LayerImageView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
}
}
}
- (float)InTranslationParameterWithLayerArray:(NSMutableArray *)Array
WithIndex:(int)i
{
switch (ParallaxMethod)
{
//移动半径和图层层级成线性关系
case Linear:
return (float)(i)/(float)([Array count]);
break;
//移动半径和图层层级成二次关系
case EaseIn:
return powf((float)(i)/(float)([Array count]), 0.5f);
break;
//移动半径和图层层级成根号关系
case EaseOut:
return powf((float)(i)/(float)([Array count]), 2.0f);
break;
default:
return (float)(i)/(float)([Array count]);
break;
}
}
- (float)InScaleParameterWithLayerArray:(NSMutableArray *)Array
WithIndex:(int)i
{
switch (ParallaxMethod)
{
//缩放与图层层级的不同关系
case Linear:
return 1+ScaleAddition/10*((float)i/(float)([LayerArray count]));
break;
case EaseIn:
return 1+ScaleAddition/10*powf(((float)i/(float)([LayerArray count])), 0.5f);
break;
case EaseOut:
return 1+ScaleAddition/10*powf(((float)i/(float)([LayerArray count])), 2.0f);
break;
default:
return 1+ScaleAddition/10*((float)i/(float)([LayerArray count]));
break;
}
}
@end
4.实现手动动画
//手动动画和自动动画的区别是 移动的角度不再跟进计数器计算 而是直接读取手指的CGPoint
__weak JZParallaxButton *weakSelf = self;
CGFloat XOffest;
if (TouchPointInSelf.x < 0)
{
XOffest = - weakSelf.frame.size.width / 2;
}else if (TouchPointInSelf.x > weakSelf.frame.size.width)
{
XOffest = weakSelf.frame.size.width / 2;
}else
{
XOffest = TouchPointInSelf.x - weakSelf.frame.size.width / 2;
}
CGFloat YOffest;
if (TouchPointInSelf.y < 0)
{
YOffest = - weakSelf.frame.size.height / 2;
}else if (TouchPointInSelf.y > weakSelf.frame.size.height)
{
YOffest = weakSelf.frame.size.height / 2;
}else
{
YOffest = TouchPointInSelf.y - weakSelf.frame.size.height / 2;
}
//NSLog(@"XOffest : %f , YOffest : %f",XOffest,YOffest);
CGFloat XDegress = XOffest / weakSelf.frame.size.width / 2;
CGFloat YDegress = YOffest / weakSelf.frame.size.height / 2;
//NSLog(@"XDegress : %f , YDegress : %f",XDegress,YDegress);
效果
此时还有很多方法没有实现(比如动画结束后需要变回原有的非三维效果) 不过大体上已经可以看到效果了 你也可以直接将完成版的视差按钮下载下来
Have Fun :-)
其他资源
如果你对真正三维状态下的按钮还是不太理解 可以点击下面的播放按钮自由查看这个三维模型 点击播放按钮后 通过三维场景右下角的左右切换查看按钮的不同旋转状态 或者点击数字1-5来快速跳转
以外 这篇文章里所有文件都是提供公共下载的
配图所用Sketch文件:下载链接
三维模型文件(进入点击Download):下载链接