惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
S
Security @ Cisco Blogs
S
Secure Thoughts
Attack and Defense Labs
Attack and Defense Labs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Hacker News: Front Page
博客园 - 叶小钗
H
Heimdal Security Blog
Microsoft Security Blog
Microsoft Security Blog
Forbes - Security
Forbes - Security
AI
AI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
罗磊的独立博客
Application and Cybersecurity Blog
Application and Cybersecurity Blog
爱范儿
爱范儿
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
TaoSecurity Blog
TaoSecurity Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
D
DataBreaches.Net
Recent Announcements
Recent Announcements
Schneier on Security
Schneier on Security
C
Cisco Blogs
美团技术团队
D
Docker
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
月光博客
月光博客
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
H
Hackread – Cybersecurity News, Data Breaches, AI and More
A
Arctic Wolf
B
Blog RSS Feed
Cisco Talos Blog
Cisco Talos Blog
C
Cybersecurity and Infrastructure Security Agency CISA
V
Vulnerabilities – Threatpost
V2EX - 技术
V2EX - 技术
Y
Y Combinator Blog
N
News and Events Feed by Topic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
W
WeLiveSecurity
Security Archives - TechRepublic
Security Archives - TechRepublic
G
GRAHAM CLULEY
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派 万字剖析:千问App深度体验报告(2026) - 少数派 在2026年,如何真正防止别人抄袭你的作品 - 少数派 怎么用 50 块搭个 AI 语音助手?我踩了 3 天坑 - 少数派 YeeroAI:让 AI 对话真正成为知识管理的一部分 - 少数派 爬泰山 - 少数派 「旅图显影」 App 更新:这次,我们补上了一点「手感」 - 少数派 假期出门太折磨?我的 23 条经验帮你规划惬意旅行 - 少数派 工作流会变吗 - 少数派 Claude Opus 4.6 怎么用最省钱?我测了 5 种方案 - 少数派 GPT Image 2 让图文并茂不再稀罕 - 少数派 用户侧出发——什么是AI,我要不要学习? - 少数派 找片、转存、整理、播放一条龙!让你的付费网盘值回票价 - 少数派 欢迎试用!日课一问2.0插件 - 少数派 自己做的MDeditor,原本想购买 Typora 试了两次支付不成功,干脆自己做一个 - 少数派 vibe coding了一个 3MB 的小工具,让 ~/Downloads 彻底告别混乱 - 少数派 因为受不了 Mac 的风扇策略,我做了一个风扇控制工具 - 少数派 别只怪模型 - 少数派 Warp 终端的 AI 功能怎么用?我测了一周的体验 - 少数派 AI 写代码老是出 bug?这 5 个配置我后悔没早知道 - 少数派 「新玩意」苹果出相机可能就这样:Sigma BF + 45mm F2.8 DG Contemporary - 少数派 一个面向2030年的AI操作系统是什么样子的:浅谈cola这款有灵魂的Agent - 少数派 别只看写代码 - 少数派 每天解决10个问题,还是一口气攻坚解决400个? - 少数派 AI 交易机器人怎么搭?我用 Claude 跑了一周实盘 - 少数派 Maptoposter Online:把你爱的城市画成艺术海报 - 少数派 Function Calling 怎么用?我测了 3 个模型发现差距真大 - 少数派 Legend Talk:我做了个 AI 圆桌,让 160 位思想家围着你的问题转 - 少数派 如何找到自己的蓝方?在小县城寻找压力测试 - 少数派 语音输入与软件接口|2026年聊AI时,我们都聊些什么(上) - 少数派 混动已经卖爆,纯电又来补刀——钛7闪充版简直“不讲武德” - 少数派 本月玩什么|朋友收藏、识质存在、沙罗周期 - 少数派 为什么要每天坚持输出? - 少数派 Claude API 挂了好几个小时,你的项目有备用方案吗? - 少数派 Function Calling 没你想的复杂——我用它做了个有点用的工具 - 少数派 登录系统立即播放视频或者图片音乐的软件 - 少数派 我为什么创建 FlipHTML5 下载工具 - 少数派 残局没电?多品牌外设电量统一管理软件EasyBluetooth已支持RTSS游戏内显示以及AIDA64 - 少数派 前往通义路的路 - 少数派 太好看了,媲美Sun的个人导航页,NAS部署星云门户 - 少数派 乌黑嘴唇“一键检测”上线了 - 少数派 派早报:Claude AI 接入多个创意软件生态、FILCO 生产方接手品牌等 - 少数派 【更新】BearCLI、Claude 连接器与 MCP 服务器 - 少数派 记了上千条流水,还是看不懂财务?我做了一个让 AI 读懂账本的工作台 - 少数派 MINI R56 升级原厂 Sport 模式 - 少数派 新玩意 | 一棵柠檬树(仿真版) - 少数派 Momenta的“物理AI”野望,需迈过“含摩量”这道关 - 少数派 网页直接投屏控制手机!NAS一键部署PandaScrcpy,流畅丝滑可远程。 - 少数派 众测|邀你一同探索随身 AI 硬件入口 YoooClaw C·ONE - 少数派 2050大会:分享时间是真诚 参会记 - 少数派 iPad 赋能电影创作:国内首部宣纸手绘长片《燃比娃》的幕后故事 - 少数派 AI的审美:我用 8 个大模型给 100 张旅行照片打分 - 少数派 普通人如何破圈?去参加一个本地协会 - 少数派 把极空间的图标全换了,主题DIY全攻略打造你的专属NAS桌面 - 少数派 电子便签墙,帮你实现便签自由 - 少数派 我如何用三个 CLI 工具取代文档创建需求 - 少数派 原来真的有人可以玩一辈子 - 少数派 社区速递 139 | 派友热议三月买了啥、复古单反尼康 Df 体验 - 少数派 06 作品的赏析与评价 - 少数派 TDS REVIEW|索尼 WF-1000XM6 降噪真无线耳机体验 - 少数派 35.98万起售的第二代腾势D9,我看重的不是堆料,而是不凑合 - 少数派 鼠须管 Squirrel 皮肤配置指北 - 少数派 从watch ultra2换到redmi watch6 - 少数派 派早报:阿里巴巴发布视频生成模型 HappyHorse 1.0 等 - 少数派 别迷信1M - 少数派 家人们天塌了!网盘“大封杀”,多个渠道多条路,NAS部署PanHub - 少数派 AI与人勾心斗角!NAS一键部署AI狼人杀,假日休闲必备。 - 少数派 电商必备!Comfyui工作流批量生图插件,一次生成12张!支持Nano banana pro模型 - 少数派 Comfyui工作流配置Gpt-image-2模型教程,0.03/张 - 少数派 OpenClaw第三方APi怎么配置?可使用Gpt-image-2模型 - 少数派 会员社区话题精选 Ep. 103 - 少数派
丝滑的 iOS 进度条解锁交互到底是怎么制作的? - 少数派
2024-03-26 · via 少数派

原文首发于:丝滑的 iOS 进度条解锁交互到底是怎么制作的?

这篇是拆解吉光卡片 App 底部彩蛋入口的动画实现。将这个入口发到社区后,很多小伙伴表示对实现原理比较感兴趣,于是便有了这篇文章,我将从思路和原理入手,带大家抽丝剥茧地带大家了解这个动画是如何实现的。新手可以通过这个文章,了解实现的原理,老手可以看代码来更深入了解实现的细节。

而立之年的首页横幅

我将尽可能详细地拆解 吉光卡片小彩蛋的实现原理,确保大家可以理解并掌握其中的所有步骤。动画效果使用 SwiftUI 来实现,所以你可以看到其强大的功能和灵活性。我希望你们会喜欢这个教程,并从中学到新的技巧和知识。我会贴上必要的部分代码块,有兴趣可以自行查看或忽略。

渐变光

在这个动画中,极其重要的点在于对渐变光的视觉把控。关键的细节在于如何通过模拟物理世界点光源的扩散与衰减效应模拟出真实世界的感觉。另外就是在光的边缘,如果通过遮罩层对光进行裁切,保证光不会被发散出去。

渐变光进度条加载展示

物理

通过叠加多层的渐进阴影,可以得到一种更柔和,更接近物理效果的散射光。这种效果就像是光从光源发出后,边缘会逐渐变得柔和,而远离光源的光则会逐渐散开,给人一种非常自然和真实的感觉。

当时在家里拍视频的场景

一般来说,我们可以通过叠加三层或更多层的渐进阴影来达到这种效果。渐变层的透明度、扩散大小,阴影数值没有太多的规律可言,关键还是尽可能还原物理世界的规律,适当的调整各层的扩散大小和透明度数值,通常,扩散小的层次透明度数值大,扩散大的层次透明度数值小,通过这样的非线性叠加,可以得到非常理想的效果。

当时在家里拍视频的场景
.shadow(color: .accent.opacity(0.3), radius: 80)
.shadow(color: .accent.opacity(0.5), radius: 60)
.shadow(color: .accent.opacity(0.6), radius: 20)
.shadow(color: .accent.opacity(0.7), radius: 8)

遮罩

在 SwiftUI 中,如果有一些多余的部分需要去除,我们可以采用一个非常简单的方法,就是通过加入一个矩形的遮罩来达到我们的目的。这个矩形遮罩的作用就是将那些我们不需要的、多余的光线部分进行裁剪,从而使得我们的设计更加精准和符合我们的期望。这种方法不仅简单,而且效果非常好,可以帮助我们在设计中达到更好的效果。

当时在家里拍视频的场景
.contentShape(Rectangle())

进度条的实现

百分比

在 SwiftUI 中,GeometryReader 它提供了几何坐标读取的功能,用来读取该空间中的大小和位置信息,这是一个非常有用的工具。利用这个工具,我们可以在最外层添加一个撑开的宽度修饰器,这样我们就可以获得一个动态宽度的进度条。具体来说,这个进度条的宽度会根据我们设置的参数动态地进行调整,非常灵活且易于使用。

当时在家里拍视频的场景

宽度变化

此时这里的 progress 是一个百分比,它可以表示进度条的完成程度。例如,如果我们设置每隔 0.01 秒增加 0.01,也就是 1% 的 progress,那么要跑完整条进度条就需要 0.01 x 100 = 1秒。这样一来,我们就可以根据需要设置进度条的速度,让它以我们希望的速度进行加载。但是这样我们可能会得到一条线性增长的进度条,这可能并不是我们想要的效果。

@State private var progress: CGFloat = 0
@State private var timer: Timer?
@State private var isProgressCompleted: Bool = false

var body: some View {
    GeometryReader { geometry in
        RoundedRectangle(cornerRadius: 12, style: .continuous)
            .frame(width: isProgressCompleted ? geometry.size.width : geometry.size.width * progress)
    }
    .frame(maxWidth: .infinity)
}

蓄力的实现

这个方法实现了三个功能,这三个功能都是在用户使用过程中非常重要的:

松手的时候会归位。这是一个贴近物理的反馈,因为它可以保证在用户松手后,进度条会自动归位,不会对其他操作产生影响。

渐进式的宽度增量。通过这个功能,我们可以看到进度条的宽度是逐渐增加的,而不是突然增加,这可以带来更好的用户体验。

具有增量的震动提示。这个功能可以在进度条增加的同时,给用户提供震动提示,增加用户的使用感觉。

加速度

那么,怎么去获取到一条有加速度的更接近物理蓄力感觉的进度条呢?我们采取了一个简单的方法,就是设置了一个常量增量 acceleration 为 0.00007。这样,每次增加的量就不同,且呈指数型增加。所以在完成第一步的时候,progress 就变成了 0.007% + 0.07% = 0.077%,所以可以看到宽度的变化最开始是缓慢的,随着不断的相加,这个进度条就只需要 1.72秒 就可以从 0% 到 100%。

当时在家里拍视频的场景

进度条只要不满足到 100% 的条件,isProgressCompleted 这个条件就是 false,也就会执行重置计时器,这就是 resetProgress() 这个方法所要做的事情,让进度条回归到 0。假如进度条达到了 100%,那么 isProgressCompleted 就会是 true,那么循环就结束,用户可以看到进度条的完整过程。

震动

震动提示是通过 UIKit 里面来实现的,有不同的挡位的震动可以选择,这里选择了一个比较轻的挡位 soft。这里的方法其实跟宽度的加速度增量类似,也是一个递增的方法且跟随进度条的宽度变化而变化的。当进度超过 0.1%,会产生一次震动,根据上面第一个 10 毫秒才变化了 0.07% 的进度,可以发现是一个先慢后快的进度反馈,跑完震动条大概会震动 75 次,这样的设计可以增加用户的使用感觉,提升用户体验。

这里有个值得分享的,震动的频次不能过高,我一开始设置的是只要的宽度发生变化,就震动一次,首先是会震动到你手麻,其次是发现震动反馈有些许卡顿。所以才重新设置了震动阈值,来“降低”震动的频次。

当时在家里拍视频的场景
    func startProgress() {
        // 取消之前的计时器
        timer?.invalidate()
        isProgressCompleted = false // 重置进度完成状态
        var increment = 0.0007 // 初始进度增量
        let acceleration = 0.00007 // 进度增量的加速度

        let feedbackGenerator = UIImpactFeedbackGenerator(style: .soft)
        feedbackGenerator.prepare()

        // 定义震动的阈值,例如每增加1%时触发一次
        let vibrationThreshold: CGFloat = 0.01

        // 记录上次震动的进度
        var lastVibrationProgress: CGFloat = 0

        // 创建一个新的计时器
        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [self] timer in
            // 更新进度条的进度
            self.progress += increment

            // 增加下一次的进度增量,模拟加速效果
            increment += acceleration

            // 当进度超过上次震动进度+阈值时,触发震动
            if self.progress >= lastVibrationProgress + vibrationThreshold {
                feedbackGenerator.impactOccurred()
                lastVibrationProgress = self.progress
            }

            // 确保进度不会超过1
            if self.progress >= 1 {
                // 当进度条完成时,保持进度状态,不重置
                timer.invalidate()
                self.progress = 1
                self.isProgressCompleted = true // 标记进度完成
            }
        }
    }

    func resetProgress() {
        // 取消计时器并重置进度条
        timer?.invalidate()
        progress = 0
        isProgressCompleted = false // 重置进度完成状态
    }

长按手势

序列化手势/手势链

开始和重置的两个方法是要搭配手势来实现的,这里通过 sequenced 来完成前后两件事情,一件是长按开始,另外一个未达条件,去归零进度条。如果只是往上叠手势,这些手势其实是异步的且权重是一样的。而手势其实跟图层是一样的,是可以设置优先级和前后顺序的。下面的例子是一个同步的例子。

当时在家里拍视频的场景
   LongPressGesture(minimumDuration: 0.5)
            .onEnded { _ in
                withAnimation {
                    if !isProgressCompleted { // 如果进度未完成,则开始
                        startProgress()
                    }
                }
            }
            .sequenced(before: DragGesture(minimumDistance: 0))
            .onEnded { _ in
                withAnimation {
                    if !isProgressCompleted { // 如果进度未完成,则重置
                        resetProgress()
                    } else {
                  //
                    }
                }
            }

矩形缩放

神奇移动/Magic move标注

矩形的缩放是通过一个强大的方法 matchedGeometryEffect 来实现的。这个修饰器的使用实际上是非常简单直接的,它与AE(After Effects)里面的动画标记效果有些类似。在使用这个修饰器时,你只需要定义动画的初始帧和结束帧,然后它会自动处理其他的帧,也就是中间的过渡帧。这种方式可以让动画过程看起来非常流畅自然。

为了能够准确地识别这两个帧,SwiftUI 提供了一种属性包装器,叫做Namespace。Namespace的主要作用就是给初始帧和结束帧进行唯一标识,保证动画的正确性。除此之外,我们还可以利用ifElse 的语句来实现元素的切换。比如,我们可以让一个元素在某个时刻消失,然后另外一个元素出现,而在这个过程中,所有的动画变化都会被自动补齐。这就是说,你不需要手动去处理每一帧的变化,所有的过程都是自动的,极大地方便了开发。

当时在家里拍视频的场景
@State private var scaleIntoOneCard = false // 缩成一张卡片
  @Namespace private var shapeTransition // 几何动画切换
  
   if !scaleIntoOneCard {
         progressbar()
           .matchedGeometryEffect(id: "card", in: shapeTransition)
   } else{
				RoundedRectangle(cornerRadius: 4)
				   .matchedGeometryEffect(id: "card", in: shapeTransition)
   }

一张变六张

在上面的描述中,我们提到了一张卡片,这张卡片已经没有用处了,所以我们直接让它消失。但是,这个过程对用户来说是不可见的。实际上,在卡片消失的0.5秒前,我们已经提前偷偷地把后面的六张卡片用ZStack堆叠起来了。

这种堆叠方式其实就是我们熟悉的初始帧和结束帧的逻辑,其中一个是前后堆叠的ZStack,另一个是水平堆叠的HStack。所有的变化动画都是由matchedGeometryEffect来完成的。每一张卡片的边际依次从1到6,这样就能形成一个良好的视觉效果。在这个过程中,我们需要注意的一点是,id前后一定要一一对应,否则会造成混乱。这是一个非常重要的细节,可能会导致整个动画效果出现问题。

当时在家里拍视频的场景
	 ZStack {
            ForEach(0 ..< 6) { index in
                card()
                    .matchedGeometryEffect(id: "card\(index)", in: shapeTransition) // 动画标记
            }
        }
     HStack {
            ForEach(0 ..< 6) { index in
                card()
                    .matchedGeometryEffect(id: "card\(index)", in: shapeTransition)
            }
        }

所以总结来说就是,长按拉满进度条后,立刻变成一张卡片,然后单张卡片消失,接着一张卡片变成六张卡片,六张卡片消失,出现密码输入框。这个动画的过渡效果其实可以类比 keynote 里面的 magic move 的效果。

输入框

等宽数字

类似验证码的输入框,是可以通过六个 Textfield 来实现的,然后再通过 Focusfield 来实现焦点的游走,但是我懒。后面的输入框背景是假的,我用的是一个完整的 textfield。

通过控制字体的字体间距来实现,这个时候一定要设置等宽的数字,保证每个间距都是一样的。

当时在家里拍视频的场景
 .kerning(20)
 .font(.system(size: 13).weight(.bold).monospaced())

晃动

这里有个密码错误的晃动提示,我也采用了一个偷懒的方法,通常来说一个递减 x 轴的 offset 来控制一个左右晃动的效果。而我是在这个输入框通过一个弹性动画来实现的,也就是当输入框刚要向 x轴的偏移 4 个单位的时候,就被强行停止了。

停止的动画是有弹性的,所以还需要多几帧才可以归位到 0。这样就讨巧地实现了一个 shake 的效果。

当时在家里拍视频的场景
 .offset(x: start ? 4 : 0)
 
 //
 
 start = true
 withAnimation(Animation.spring(response: 0.2, dampingFraction: 0.2, blendDuration: 0)) {
 start = false
 }

消失弥散过渡效果

输入框成功的消失动画,使用了一个自定义的复合过渡效果,是带透明度和大小的变化。这也是苹果喜欢用的一个过渡,比如说 ipad 上的 spotlight 的消失效果。

其实很好理解,结束帧是一个放大和模糊的状态,这里可以控制 radius 和 scale 来控制整个效果,而这个强度的调整其实跟即将消失的物体的大小也是息息相关的。

当时在家里拍视频的场景
private struct BlurModifier: ViewModifier {
    public let isIdentity: Bool
    public var intensity: CGFloat

    public func body(content: Content) -> some View {
        content
            .blur(radius: isIdentity ? intensity : 0)
            .opacity(isIdentity ? 0 : 1)
    }
}

public extension AnyTransition {
    static var blur: AnyTransition {
        .blur()
    }

    static var blurWithoutScale: AnyTransition {
        .modifier(
            active: BlurModifier(isIdentity: true, intensity: 5),
            identity: BlurModifier(isIdentity: false, intensity: 5)
        )
    }

    static func blur(
        intensity: CGFloat = 5,
        scale: CGFloat = 0.9,
        scaleAnimation animation: Animation = .spring()
    ) -> AnyTransition {
        .scale(scale: scale)
            .animation(animation)
            .combined(
                with: .modifier(
                    active: BlurModifier(isIdentity: true, intensity: intensity),
                    identity: BlurModifier(isIdentity: false, intensity: intensity)
                )
            )
    }
}

动画控制

DispatchQueue.main.asyncAfter 是一个常用的方法,用于在主线程上延迟执行代码。这个方法非常适用于需要稍后执行操作,但不阻塞当前线程的情况。这对于改善用户界面的响应性或等待某些条件成熟后再执行操作特别有用。我还喜欢嵌套来使用,比如说在执行了某个指令,完成后0.5秒再执行另外一个指令。这样连加减法的延迟都不用计算了。

当时在家里拍视频的场景
  DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                withAnimation {
                                    showSixCards = true
                                }
                                // 再一秒之后显示输入密码
                                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                    withAnimation {
                                        showPasswordInput = true
                                    }
                                }
                            }

完结散花~很高兴能跟大家分享一些设计的小细节,这是一篇设计和技术五五分成的文章,其实可以发现,懂视觉的人不懂技术,经常受限于实现。换句话说,技术限制了你对于设计的想象,而技术有能力实现,但是又缺乏对设计颗粒度的认知,很多开发经常说:“没必要,又看不出来”。他们之间还是隔了一条很大的缝隙,割裂,马里亚纳海沟!社会发展的形态的分工就似乎只让我们“专注”在一个事情,而很多事情的完成本质上一个连贯的多学科复合的事情,至少我是这么理解的。

文章写得比较仓促,尽可能通过图解的方式带大家了解实现的原理,如果对文章有任何困惑或不理解的地方,欢迎反馈。

吉光卡片 App 应用地址: