当前位置: 首页 > news >正文

自定义转场动画 - 从悬浮按钮丝滑扩散到发布页面

一. 引言

很多 App 都有一个显眼的悬浮按钮。

比如微博的「+」发布、B 站的「上传」、知乎的「提问」……

它们的共同点是:

当用户点击时,新的页面会以一种“自然生长”的方式展开。

今天我们就实现一个类似的效果:

点击悬浮按钮,页面从按钮中心扩散成全屏的发布界面。整个动效不靠第三方库,完全使用系统的转场机制配合核心动画来完成。

二. 核心思路

我们知道,在 iOS 里每个页面切换,默认动画都是由系统提供的(比如 push 的右滑、present 的上滑)。

但如果想要做自定义动画,比如“从按钮中扩散出来”,那就得使用一个更底层的机制:

UIViewControllerTransitioningDelegate 它允许我们接管页面切换时的动画过程。

整个转场动画的本质是:

  1. 用户点击按钮
  2. 系统调用我们的转场代理
  3. 动画类(Animator)执行自定义动画逻辑
  4. 动画完成后,系统恢复控制

而我们的自定义动画,就是通过一个圆形遮罩(CAShapeLayer),然后通过动画让圆形遮罩不断扩大,让下一个页面从按钮中心“生长”出来。

三. 实现步骤

整个过程我们还是分成三个部分吧:

  1. ViewController:一级页面,悬浮按钮会在这个页面显示,点击后进行页面切换。
  2. PublishAnimator:过渡动画核心,真正的“魔法”发生在这里,负责结果系统的过渡动画。
  3. PublishViewController:发布页面,点击发布按钮后切换到该页面。

3.1 主页面(ViewController)

主页面要做的事情只有三件:

  1. 显示悬浮按钮
  2. 点击按钮时 present 新页面
  3. 让系统在切换时使用我们自己的动画类

该类的所有代码如下:

class ViewController: UIViewController, UIViewControllerTransitioningDelegate {let floatButton = UIButton(type: .system)override func viewDidLoad() {super.viewDidLoad()view.backgroundColor = .white// 悬浮按钮floatButton.frame = CGRect(x: view.bounds.width - 80, y: view.bounds.height - 150, width: 60, height: 60)floatButton.layer.cornerRadius = 30floatButton.backgroundColor = .systemBluefloatButton.setTitle("+", for: .normal)floatButton.setTitleColor(.white, for: .normal)floatButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)view.addSubview(floatButton)}@objc func buttonTapped() {let publishVC = PublishViewController()publishVC.modalPresentationStyle = .fullScreen// 告诉系统:使用自定义转场动画publishVC.transitioningDelegate = self  present(publishVC, animated: true, completion: nil)}// MARK: - UIViewControllerTransitioningDelegate// 当页面「出现」时调用func animationController(forPresented presented: UIViewController,presenting: UIViewController,source: UIViewController) -> UIViewControllerAnimatedTransitioning? {let animator = PublishAnimator()animator.originFrame = floatButton.frameanimator.isPresenting = truereturn animator}// 当页面「关闭」时调用func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {let animator = PublishAnimator()animator.originFrame = floatButton.frameanimator.isPresenting = falsereturn animator}
}

 解释一下这个机制:

  • transitioningDelegate 告诉系统:“我要自己控制 present/dismiss 的动画”
  • 每当 present 或 dismiss 时,系统会来问我们:“请给我一个 Animator,我该怎么动画?”
  • 我们就返回 PublishAnimator 实例,里面写的是什么动画,系统就执行什么。

3.2 动画核心(PublishAnimator)

真正的“魔法”发生在这里。

我们要实现的动画逻辑是:用按钮的 frame 作为圆心,创建一个遮罩圆,从小到大扩散覆盖整个屏幕。

代码如下:

class PublishAnimator: NSObject, UIViewControllerAnimatedTransitioning {let duration: TimeInterval = 0.35var originFrame: CGRect = .zerovar isPresenting = truefunc transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {return duration}func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {guardlet toVC = transitionContext.viewController(forKey: .to),let fromVC = transitionContext.viewController(forKey: .from)else { return }let container = transitionContext.containerViewif isPresenting {// ① 展开动画container.addSubview(toVC.view)// 动画起点:按钮区域的圆let startPath = UIBezierPath(ovalIn: originFrame)// 动画终点:足够覆盖整个屏幕的超大圆let extreme = sqrt(pow(container.bounds.width, 2) + pow(container.bounds.height, 2))let endPath = UIBezierPath(ovalIn: originFrame.insetBy(dx: -extreme, dy: -extreme))// 创建遮罩层let maskLayer = CAShapeLayer()maskLayer.path = endPath.cgPathtoVC.view.layer.mask = maskLayer// 路径动画(从小圆扩展为大圆)let animation = CABasicAnimation(keyPath: "path")animation.fromValue = startPath.cgPathanimation.toValue = endPath.cgPathanimation.duration = durationanimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)animation.delegate = AnimationDelegate {toVC.view.layer.mask = niltransitionContext.completeTransition(true)}maskLayer.add(animation, forKey: "path")} else {// ② 收回动画(反向)container.insertSubview(toVC.view, belowSubview: fromVC.view)let extreme = sqrt(pow(container.bounds.width, 2) + pow(container.bounds.height, 2))let startPath = UIBezierPath(ovalIn: originFrame.insetBy(dx: -extreme, dy: -extreme))let endPath = UIBezierPath(ovalIn: originFrame)let maskLayer = CAShapeLayer()maskLayer.path = endPath.cgPathfromVC.view.layer.mask = maskLayerlet animation = CABasicAnimation(keyPath: "path")animation.fromValue = startPath.cgPathanimation.toValue = endPath.cgPathanimation.duration = durationanimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)animation.delegate = AnimationDelegate {fromVC.view.layer.mask = niltransitionContext.completeTransition(true)}maskLayer.add(animation, forKey: "path")}}
}

动画要点拆解:

主要是通过 UIBezierPath(ovalIn:) 创建一个圆形路径,起点是按钮的 frame,使用CAShapeLayer来当作遮罩(mask)层,让视图按照圆形区域显示,然后通过核心动画 CABasicAnimation(keyPath: "path") 来改变mask的形状(从小圆到大圆)。

最后 transitionContext.completeTransition(true) 通知系统动画结束。

这样我们就实现了一个从按钮中心“扩散开”的动画。收回时,只要反向执行即可。

3.3 发布页面(PublishViewController)

PublishViewController 本身没做动画,它只是普通的页面布局,带有一个返回按钮(dismiss 动画就是上面反向的动画)。

import UIKitclass PublishViewController: UIViewController {override func viewDidLoad() {super.viewDidLoad()view.backgroundColor = .systemYellow// 顶部导航栏let navBarHeight: CGFloat = 80let navBar = UIView(frame: CGRect(x: 0, y: 12.0, width: view.bounds.width, height: navBarHeight))view.addSubview(navBar)// 返回按钮let backButton = UIButton(type: .system)backButton.frame = CGRect(x: 10, y: 40, width: 60, height: 30)backButton.setImage(UIImage(systemName: "chevron.backward"), for: .normal)backButton.tintColor = .blackbackButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)navBar.addSubview(backButton)....}
}

四. 结语

整个动画的核心,其实就两步:

  1. 接管转场过程 —— 通过 UIViewControllerTransitioningDelegate,让系统在 present 和 dismiss 时使用我们自定义的动画控制器。
  2. 构建圆形扩散动画 —— 使用 CAShapeLayer 作为遮罩,通过 path 动画让圆从按钮中心扩散覆盖全屏,再在返回时反向执行。

实现过程简单清晰,但效果极具空间感。

用户能一眼看出:这是从按钮“进入”的页面,也能从同一点“返回”,交互逻辑自然闭环。

在实际项目中,你可以:

  • 把圆形改成矩形、心形、logo形(只要换路径);
  • 在扩散时叠加模糊或缩放动画,让动效更有层次;
  • 使用弹性曲线(Spring 动画)增强空间张力,让展开更生动。

动画不是为了炫技,而是为了让过渡更「有意义」。它让用户在切换中保持空间感与方向感。

如果你在项目中也有类似的转场需求,

或想探讨不同的动效表现方式,欢迎在评论区或私信交流,一起让 App 的过渡更具“生命力”。

http://www.dtcms.com/a/509157.html

相关文章:

  • 刚做的网站为什么搜索不到网站建设明确细节
  • asp医院网站源码市场调研报告怎么写
  • 深入浅出SystemC TLM — 以PCIe为例介绍虚拟原型的作用
  • 网站配色分析上海知名的网站建设公司
  • HarmonyOS线程模型与性能优化实战
  • 苏州市规划建设局网站阳江房产网58同城
  • ubuntu系统中对于硬盘占用的分析
  • 推荐专业做网站公司wordpress在后台去掉链接
  • easyui做门户网站百度seo快速见效方法
  • html5手机资讯网站模板网络营销解决方案
  • 做商城网站的项目背景网店营业执照
  • stanley工具网站开发什么叫动漫设计与制作
  • Datawhale25年10月组队学习:math for AI+Task3线性代数(下)
  • 2014网站建设如何做一个论坛网站
  • 网站建设重点是什么it服务商
  • 【连接器专题】案例:在充电线端应用PTC时为什么内模要用PP类材料
  • 安徽建设相关网站seo经理
  • 【随笔】2026年陕西会举办哪几场马拉松
  • 推广网站seo设计网站公司 都赞湖南岚鸿案例10
  • 网站建设书 模板下载爱用系统的设计理念
  • 会qt怎么做网站衡水网站建设浩森宇特
  • 信阳制作网站ihanships免费模板素材网站
  • 江浦做网站百度seo新规则
  • 网站推广自己可以做吗如何建一个营销网站
  • 淘宝实时优惠券网站怎么做的网站情况建设说明
  • 电子科技网站模板买完域名接下来怎么弄
  • C语言动态数组
  • Linux小课堂: 系统监控与进程管理之深入解析 w、ps 与 top 命令
  • 一文解析软件项目管理:从核心概念到实战要点
  • 选择合适的电机试验平台的要点