原文:Custom UIViewController Transitions: Getting Started
作者:Richard Critz
译者:kmyhy

更新说明: 本教程由 Richard Critz 更新至 iOS11 和 Swift 4。原文作者是 József Vesza。

iOS 内置了一些好看的 View Controller 转换动画——push、pop、cover vertically——这些都是现成的,但创建自己的动画岂不更有趣呢?自定义 UIViewController 转换能大大地提高用户体验,并让你的 app 明显超出其它 app 一大截。如果你曾经因为这个过程太难而不愿意自定义转换动画,你会发现其实它并没有你想象中的那么难。

在本教程中,我们将为一个简单的猜谜游戏添加自定义 UIViewController 转换动画。在最后,你将学习到:

  • Transitioning API 的结构
  • 如何用自定义转换动画呈现、解散 View Controller
  • 如何构建交互式转换

注意:本教程中演示的转换动画使用的是 UIView 动画,你需要对此有所了解。如果你需要帮助,请参考我们的 iOS Animation 以便快速进入我们的主题。

开始

下载开始项目。Build & run,你会看到:

这个 app 用一个 page view controller 来展现几个不同的卡片。每张卡片显示一段关于宠物的描述,当你点击一张卡片显示它所描述的是什么样的宠物。

你的任务是猜猜这是什么宠物?猫、狗、还是鱼?试完一下 app,看看你猜得准不准?

导航逻辑是写好的,但 app 给人的感觉平淡无奇。我们将通过自定义转换动画来为它增添一些色彩。

介绍 Tansitioning API

Transitioning API 是一个协议集。它允许你为你的 app 选择最合适的一种实现方式:使用负责管理转换动画的现成对象或者创建专门的对象。这一节结束,你将了解每个协议的作用及其相互间的关联。下图显示了 API 的组成部分:

组成部分

尽管图很复杂,但一旦你理解如何将各部分组装起来之后就会变得很简单了。

Transitioning Delegate

每个 view controller 都有一个 transitioningDelegate 属性,这个对象实现了 UIViewControllerTransitioningDelegate 协议。

当你呈现或解散一个 view controller 时,UIKit 会询问 transitioning delegate 对象要使用哪一个 animation controller。要将默认的动画替换成你自己的动画,你必须实现一个 transitioning delegate 并通过它返回一个特定的动画控制器。

Animation Controller

transitioning delegate 对象所返回的 animation controller 对象则实现了 UIViewControllerAnimatedTransitioning 协议。它负责实现转换动画的“重体力活”。

Transitioning Context

Transitioning contenxt 对象实现了 UIViewControllerContextTransitioning 协议并负责转换过程中的一个重要角色:它封装了和动画相关的视图和视图控制器的信息。

如你在上图中所见,你不需要自己实现这个协议。UIKit 会为你创建和配置 transitioning context 并在动画发生时传递给你的 animation controller。

转换动画的工作流程

在呈现动画中包括:

  1. 触发动画,无论是以编码方式还是 segue 方式。
  2. UIKit 会询问 “to” view controlle(即将呈现的 view controller)它的 transitioning delegate 是谁。如果没有提供,UIKit 会使用标准的、内置的转换动画。
  3. 然后 UIKit 会通过 animationController(forPresented:presenting:source:) 方法向 transitioning delegate 对象索要一个 animation controller。如果返回 nil,转换过程将使用默认的动画。
  4. UIKit 会创建 transitioning context。
  5. UIKit 会通过 transitionDuration(using:) 方法向 animation controller 询问动画需要的时长。
  6. UIKit 调用 animation controller 的 animateTransition(using:) 方法,以执行转换动画。
  7. 最后,animation controller 调用 transitioning context 上的 completeTransition(_:) 方法,通知动画已经完成。

解散过程与此类似。只不过,UIKit 是向 “from” view controller(即将被解散的控制器)索要 transitioning delegate 对象。而 transitioning delegate 对象是通过 animationController(forDismissed:) 方法返回 animation controller。

创建自定义呈现动画

到了真枪实干的时候了!我们的目的是实现这个动画:

  • 当用户点击卡片,它会翻过第二个视图,第二个视图缩小到卡片的尺寸。
  • 翻转动作完成后,第二个视图放大到整屏。

创建 Animator

开始来创建 animation controller。

File\New\File…,选择 iOS\Source\Cocoa Touch Class 然后点 Next。文件命名为 FlipPresentAnimationController,继承 NSObject ,余元选 Swift。点 Next,勾上 Group to Animation Controllers。点击 Create。

Animation controllers 必须实现 UIViewControllerAnimatedTransitioning 协议。打开 FlipPresentAnimationController.swift 并适当修改类声明。

class FlipPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {}

Xcode 会报错,说 FlipPresentAnimationController 未实现 UIViewControllerAnimatedTransitioning 协议,点击 Fix to 添加对应的空方法。

我们在动画一开始会用到所点击的卡片的 frame。在类的实现中,添加一个属性来保存这个信息。

private let originFrame: CGRectinit(originFrame: CGRect) {self.originFrame = originFrame
}

然后,你需要在刚才新增的两个空方法中编写代码。将 transitionDuration(using:) 修改为:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {return 2.0
}

正如方法名所暗示的,这个方法用于返回动画时长。将其设置为 2 秒足以让你有足够的时间看到这个动画。

在 animateTransition(using:) 方法中添加:

// 1
guard let fromVC = transitionContext.viewController(forKey: .from),let toVC = transitionContext.viewController(forKey: .to),let snapshot = toVC.view.snapshotView(afterScreenUpdates: true)else {return
}// 2
let containerView = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: toVC)// 3
snapshot.frame = originFrame
snapshot.layer.cornerRadius = CardViewController.cardCornerRadius
snapshot.layer.masksToBounds = true

这段代码中做了这些事情:

  • 获取两个 View controller 的引用:将要被替换的 view controller 和将被呈现的 view controller。然后对动画结束时的屏幕内容进行截图。
  • UIKit 将整个转封装到一个容器 view 中,以便简化对视图树和动画的管理。获得对容器视图的引用,然后计算新视图的最终框架 frame 有多大。
  • 配置屏幕截图的 frame 让它和 from 视图的 frame 一致并盖住卡片。

继续在 animateTransition(using:) 方法中添加:

// 1
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
toVC.view.isHidden = true// 2
AnimationHelper.perspectiveTransform(for: containerView)
snapshot.layer.transform = AnimationHelper.yRotation(.pi / 2)
// 3
let duration = transitionDuration(using: transitionContext)

container view 在刚刚被 UIKit 创建时,它只包含了 from 视图。你必须将动画中涉及到的其它视图添加进去。记住 addSubview(_:) 方法会将新的视图添加到视图树的最上面,因此视图树的顺序就是你添加它们的顺序。

  1. 将新的 to 视图添加到视图树然后隐藏它。将截图放到它的前面。
  2. 设置动画的开始状态,将截图的 y 轴旋转 90°,这会导致它以侧向的姿态面对观察者,也就是在动画的一开始它不可见。
  3. 获得动画时长。

注意:AnimatorHelper 是一个工具类,用于给视图添加透视和旋转变形。你可以看看它的实现。如果你想了解 perspectiveTransform 方法的原理,请在完成教程后为这个方法添加注释。

现在前期工作完成了,来执行动画吧!添加这个方法最后的代码:

// 1
UIView.animateKeyframes(withDuration: duration,delay: 0,options: .calculationModeCubic,animations: {// 2UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1/3) {fromVC.view.layer.transform = AnimationHelper.yRotation(-.pi / 2)}// 3UIView.addKeyframe(withRelativeStartTime: 1/3, relativeDuration: 1/3) {snapshot.layer.transform = AnimationHelper.yRotation(0.0)}// 4UIView.addKeyframe(withRelativeStartTime: 2/3, relativeDuration: 1/3) {snapshot.frame = finalFramesnapshot.layer.cornerRadius = 0}
},// 5completion: { _ intoVC.view.isHidden = falsesnapshot.removeFromSuperview()fromVC.view.layer.transform = CATransform3DIdentitytransitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})

这是详细解释:

  1. 我们使用了标准的 keyframe 动画。动画时长必须必须和转换时长完全一致。
  2. 一开始沿 y 轴旋转 from 视图 90°,隐藏它。
  3. 显示截图,将它从侧向状态旋转回。
  4. 设置截图的框架大小填充全屏。
  5. 截图现在已经完全和 to 视图一致了,因此可以安全第显示真正的 to 视图了。从视图树中删除截图,因为它不再需要。然后将 from 视图恢复原有状态;否则当转换动画结束它会被隐藏。。调用 completeTransition(_:) 告诉 UIKit 动画已经完成。这将确保最终状态是一致的并从容器视图中移除 from 视图。

你的 animation controller 已经准备好了!

使用 animator

UIKit 需要一个 transitioning delegate 对象为它提供 animation controller。因此,你必须用某个对象来实现 UIViewControllerTransitioningDelegate 协议。在本例中,我们用 CardViewController 来充当这个 transitioning delegate>

打开 CardViewController.swift 在文件最后声明一个扩展。

extension CardViewController: UIViewControllerTransitioningDelegate {func animationController(forPresented presented: UIViewController,presenting: UIViewController,source: UIViewController)-> UIViewControllerAnimatedTransitioning? {return FlipPresentAnimationController(originFrame: cardView.frame)}
}

我们在这里返回一个自己定义的 animation controller 对象,用当前卡片的 frame 进行初始化。

最后是将 CardViewController 设置为 transitioning delegate。View Controller 有一个 transitioningDelegate 属性,UIKit 会通过它来判断是否要使用自定义的转换动画。

在 prepare(for:sender:) 方法中的对 card 赋值之后添加:

destinationViewController.transitioningDelegate = self

注意,是对被呈现的(presented) view controller 索要 transitioning delegate,而不是对触发呈现动作的(presenting) view controller 进行索要。

Build & run。点击一张卡片,你会看到:

这就是你的第一个自定义转换动画!

好棒!

解散视图控制器

你完成了一个漂亮的呈现动画,但这只完成了一半的工作。你的解散过程仍然是默认的。让我们来搞定它!

打开 File\New\File…,选择 iOS\Source\Cocoa Touch Class,然后点击 Next。文件名为 FlipDismissAnimationController,让它继承 NSObject 并指定语言为 Swift。点击 Next 并将文件夹指定到 Animation Controllers。点击 Create。
将类定义修改成:

class FlipDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {private let destinationFrame: CGRectinit(destinationFrame: CGRect) {self.destinationFrame = destinationFrame}func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {return 0.6}func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}
}

这个 animation controller 的工作是对呈现动画进行逆向操作,这样 UI 上给人的感觉是对称的。要做到这一点,你需要:

  • 将正在显示的视图缩小到卡片大小;这个值保存在 destinationFrame 中。
  • 翻转视图,显示原来的卡片。

在 animateTransition(using:) 方法中添加代码。

// 1
guard let fromVC = transitionContext.viewController(forKey: .from),let toVC = transitionContext.viewController(forKey: .to),let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false)else {return
}snapshot.layer.cornerRadius = CardViewController.cardCornerRadius
snapshot.layer.masksToBounds = true// 2
let containerView = transitionContext.containerView
containerView.insertSubview(toVC.view, at: 0)
containerView.addSubview(snapshot)
fromVC.view.isHidden = true// 3
AnimationHelper.perspectiveTransform(for: containerView)
toVC.view.layer.transform = AnimationHelper.yRotation(-.pi / 2)
let duration = transitionDuration(using: transitionContext)

看起来眼熟啊。不同之处在于:

  1. 这次,你要操作的是 from 视图,因此你对它进行了截图。
  2. 再次强调图层顺序的重要性。从后到前,它们应当是:to 视图、from 视图、截屏视图。当然,在本例中这个顺序貌似也不重要,但在某些时候却很重要,尤其是动画可以被取消的情况下。
  3. 将 to 视图旋转到侧立状态,这样在旋转截屏视图时,它不会被马上看到。

然后开始真正的动画。在 animateTransition(using:) 继续编写代码。

UIView.animateKeyframes(withDuration: duration,delay: 0,options: .calculationModeCubic,animations: {// 1UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1/3) {snapshot.frame = self.destinationFrame}UIView.addKeyframe(withRelativeStartTime: 1/3, relativeDuration: 1/3) {snapshot.layer.transform = AnimationHelper.yRotation(.pi / 2)}UIView.addKeyframe(withRelativeStartTime: 2/3, relativeDuration: 1/3) {toVC.view.layer.transform = AnimationHelper.yRotation(0.0)}
},// 2completion: { _ infromVC.view.isHidden = falsesnapshot.removeFromSuperview()if transitionContext.transitionWasCancelled {toVC.view.removeFromSuperview()}transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})

这实际上是呈现动画的逆过程。

  1. 首先,缩小截屏视图,让它旋转 90° ,让它不可见。然后,将 to 视图从侧立状态旋转回 0°,以便显示它。
  2. 清除你对视图树所做的修改,移除截屏视图,恢复 from 视图的状态。如果转换动画被取消——对于本例而言这不支持,但对于后期来说这是可能的——有一点非常重要,就是在你通知动画完成之前,将你添加到视图中的东西删除。

最后,还要在宠物图片解散时让 transitioning delegate 返回这个 animation controller。

打开 CardViewController.swift 在 UIViewControllerTransitioningDelegate 扩展中添加下列方法。

func animationController(forDismissed dismissed: UIViewController)-> UIViewControllerAnimatedTransitioning? {guard let _ = dismissed as? RevealViewController else {return nil}return FlipDismissAnimationController(destinationFrame: cardView.frame)
}

确保被解散的 View controller 我们所期望的类型,然后创建 animation controller,提供一个正确的卡片显示时的 frame。

现在不需要将呈现动画的时长设置得那么慢了。打开 FlipPresentAnimationController.swift 将 duration 从 2.0 修改成 0.6,这样它就和你的解散动画相一致了。

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {return 0.6
}

Build & run。测试一下 app,欣赏一下新的转换动画。

添加交互

你的自定义动画看起来不错。但是,你还可以更进一步,在解散动画中添加与用户交互的能力。iOS 的设置 app 是一个很好的交互式转换动画的例子:

在这一节中,我们的任务是通过从左轻扫的手势返回到卡片背面朝上的状态。转换动画的进度将跟随用户的手指而定。

交互式转换动画的工作机制

一个交互式控制器能够响应触摸事件或者程序输入,它能够加快、减慢甚至反向动画过程。为了使用交互式转换动画,transitioning delegate 必须提供一个交互式控制器。这是另外一种实现了 UIViewControllerInteractiveTransitioning 协议的对象。

你已经创建了一个转换动画。交互式控制器会根据手势的响应来管理动画,而不仅仅是播放一个视频。苹果提供了一个预置的 UIPercentDrivenInteractiveTransition 类,它就是一个交互式控制器的具体实现。你可以用这个类来创建自己的交互式转换动画。

点击 File\New\File…,选择 iOS\Source\Cocoa Touch Class,然后点击 Next。命名文件为 SwipeInteractionController,让它继承 UIPercentDrivenInteractiveTransition ,语言选择 Swift。点击 Next,将文件夹指定为 Interaction Controllers。点击 Create。

在类中编写代码:

var interactionInProgress = falseprivate var shouldCompleteTransition = false
private weak var viewController: UIViewController!init(viewController: UIViewController) {super.init()self.viewController = viewControllerprepareGestureRecognizer(in: viewController.view)
}

这些定义非常易懂。

  • interactionInProgress,正如名称所暗示的,用于表示一个交互是否已经发生。
  • shouldCompleteTransition 用于在内部控制这个动画。后面你会看到。
  • viewController 引用了这个交互式控制器所属的 view controller。

然后是创建手势识别器。

private func prepareGestureRecognizer(in view: UIView) {let gesture = UIScreenEdgePanGestureRecognizer(target: self,action: #selector(handleGesture(_:)))gesture.edges = .leftview.addGestureRecognizer(gesture)
}

这个手势识别器在用户从屏幕左边沿轻扫时触发,将它添加到视图中。

最后是 handleGesture(_:) 方法。在类中添加:

@objc func handleGesture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {// 1let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)var progress = (translation.x / 200)progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))switch gestureRecognizer.state {// 2case .began:interactionInProgress = trueviewController.dismiss(animated: true, completion: nil)// 3case .changed:shouldCompleteTransition = progress > 0.5update(progress)// 4case .cancelled:interactionInProgress = falsecancel()// 5case .ended:interactionInProgress = falseif shouldCompleteTransition {finish()} else {cancel()}default:break}
}

这是具体解释:

  1. 声明一个局部变量来跟踪轻扫的进度。首先获得视图中的 translation 并计算出进度。轻扫超过 200 个像素,我们就可以认为整个动画可以算作是完成了。
  2. 当手势开始,设置 interactionInProgress 为 ture,然后触发 view controller 的解散。
  3. 当手势还在移动中,我们不断调用 update(_:) 方法。这是 UIPercentDrivenInteractiveTransition 中的一个方法,它会根据你传入的百分数播放动画。
  4. 如果手势被取消,更新 interactionInProgress 并回滚动画。
  5. 当手势结束,根据当前动画的进度来决定是要 cancel() 还是要 finish() 动画。

现在,你必须来真正创建你的 SwipeInteractionController。打开 RevealViewController.swift 添加下列属性。

var swipeInteractionController: SwipeInteractionController?

然后,在 viewDidLoad() 方法最后添加:

swipeInteractionController = SwipeInteractionController(viewController: self)

当宠物卡片的照片显示时,会创建一个 interaction controller 并赋给这个属性。

打开 FlipDismissAnimationController.swift 在 destinationFrame 后添加属性。

let interactionController: SwipeInteractionController?

将 init(destinationFrame:) 修改成:

init(destinationFrame: CGRect, interactionController: SwipeInteractionController?) {self.destinationFrame = destinationFrameself.interactionController = interactionController
}

这个 animation controller 必须获得一个 interaction controller 的引用,这样它们两才能成为一对好基友。

打开 CardViewController.swift 将animationController(forDismissed:) 修改为:

func animationController(forDismissed dismissed: UIViewController)-> UIViewControllerAnimatedTransitioning? {guard let revealVC = dismissed as? RevealViewController else {return nil}return FlipDismissAnimationController(destinationFrame: cardView.frame,interactionController: revealVC.swipeInteractionController)
}

这里将 FlipDismissAnimationController 的创建改成和新的初始化方法相一致。

最后,UIKit 是通过调用 transitioning delegate 对象的interactionControllerForDismissal(using:) 方法来索要 interaction controller 的。在 UIViewConrollerTransitioningDelegate 扩展的最后添加zhege 方法:

func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning)-> UIViewControllerInteractiveTransitioning? {guard let animator = animator as? FlipDismissAnimationController,let interactionController = animator.interactionController,interactionController.interactionInProgresselse {return nil}return interactionController
}

这首先会检查 animation controller 是否是一个 FlipDismissAnimationController。如果是,获得一个对 interaction controller 的引用,并检查是否处于和用户交互的过程中。如果这些条件任何一个不满足,返回 nil,这样动画将以非交互的方式进行。否则,将 interaction controller 返回给 UIKit,以便它能够执行这种转换。

Build & run。点击一张卡片,然后从屏幕左边沿开始滑动,看看最终效果。

恭喜你!你创建了一个有趣和迷人的交互式转换动画!

接下来做什么?

你可以从这里下载已经完成的项目。

要学习更多动画,请阅读《iOS Animations by Tutorials》第17张“呈现控制器和方向动画” 。

本教程主要介绍了模式呈现和解散动画。有一点需要注意,自定义 UIViewController 转换动画也能用在 container view controller 上:

  • 当使用导航控制器时,负责提供 animation controller 的是它的 delegate,即一个实现了 UINavigationControllerDelegate 的对象。这个委托必须用 navigationController(_:animationControllerFor:from:to:) 方法来提供 animation controller。
  • Tab bar controller 需要用实现 UITabBarControllerDelegate 协议的对象来返回 animation controller,使用的是 tabBarController(_:animationControllerForTransitionFrom:to:) 方法。

希望你喜欢本教程。如果有任何问题和建议,请在论坛中留言。

自定义 UIViewController 转换动画: 开始相关推荐

  1. 如何创建 Ping app 中的 UIViewController 转换动画?

    原文:How To Make A UIViewController Transition Animation Like in the Ping App 作者:Luke Parham 译者:kmyhy ...

  2. TransformAnimation - 一个超简单的导航转换动画

    TransformAnimation 实现了一个导航转换动画,用于替换系统导航控制器默认的 push 动画: 当你点击第一个 view controller 的 Butto,转换动画开始播放. 这个动 ...

  3. ios 自定义加载动画效果

    在开发过程中,可能会遇到各种不同的场景需要等待加载成功后才能显示数据.以下是自定义的一个动画加载view效果.      在UIViewController的中加载等到效果,如下 - (void)vi ...

  4. iOS自定义转场动画实战讲解

    转场动画这事,说简单也简单,可以通过presentViewController:animated:completion:和dismissViewControllerAnimated:completio ...

  5. 【Swift学习笔记-《PRODUCT》读书记录-实现自定义转场动画】

    iOS默认的push动画是把即将展示的控制器从右边推过来.有时我们想实现类似PPT中的一些动画,这时候就需要自定义转场动画了.如下图我们想实现一个淡出并且放大的过场动画,在退出时是一个淡出缩小的动画. ...

  6. Android官方开发文档Training系列课程中文版:动画视图之创建自定义转场动画

    原文地址:http://android.xsoftlab.net/training/transitions/custom-transitions.html 自定义转场可以创建自定义动画.比如,可以定义 ...

  7. iOS 自定义转场动画浅谈

    代码地址如下: http://www.demodashi.com/demo/11612.html 路漫漫其修远兮,吾将上下而求索 前记 想研究自定义转场动画很久了,时间就像海绵,挤一挤还是有的,花了差 ...

  8. Swift——自定义转场动画(一)

    弹窗转场/过度动画(Popover效果) 避免浪费大家时间,快速查看运行效果可以直接拉到最后看 [五.完整代码] 部分,如果要看递推逻辑,可以从前往后看. 一.基本设置 弹出一个控制器:系统提供了以下 ...

  9. iOS 自定义转场动画篇

    前言: 自定义转场动画其实并不难, 关键在于能够明白思路, 也就是操作步骤. 本篇博客主要以present转场动画为例, 进行分析, 操作, 如有错误欢迎简信与我交流. 不进行修改的话, presen ...

最新文章

  1. __CLASS__ get_class() get_called_class()区别
  2. Tkinter模块常用参数(python3)
  3. 作业21-加载静态文件,父模板的继承和扩展
  4. 中如何调取api_API(接口)是什么
  5. 微信公众平台消息接口开发(34)桃花运测试
  6. vue 扫码页面限制区域_Vue.js 单页面多路由区域操作的实例详解
  7. 旋转矩阵公式生成器_坐标变换(8)—复特征值与旋转
  8. Redis持久化机制(RDB VS AOF)
  9. c语言 统计数量用count_c语言中统计重复数字次数 c语言问题 统计不同数字的个数...
  10. UVA10494 If We Were a Child Again【大数除法】
  11. linux 分析系统配置,在Linux系统上部署AWStats日志分析系统
  12. Eclipse下载与安装及汉化(详解版)
  13. 21_08_17王道计算机考研 数据结构(二)
  14. STM32中断编程步骤
  15. 深海泰坦x86_八代标压,深海泰坦X8Ti深度评测
  16. 【PDF合并】滴滴出行电子发票及行程报销单【一页打印】
  17. 网页游戏老手村《梦幻西游网页版》项目开发经验分享
  18. linux查询进程号是否存在启动脚本,Shell实现判断进程是否存在并重新启动脚本分享...
  19. 攻防世界——pwn_warmup
  20. 简明扼要的概述微服务设计原则,深入开发微服务,就从今天开始

热门文章

  1. Oracle数据库信息分类汇总计数
  2. 鸿蒙操作系统开源是什么意思鸿,鸿蒙操作系统开源,你会支持吗?
  3. 163邮箱的格式什么样的?常见的电子邮箱品牌有哪些?
  4. cron项目启动执行一次_集团要闻丨陕钢集团召开品牌战略咨询项目启动会暨第一次培训会...
  5. JSuite 最新版下载试用2021版本
  6. OPKG命令执行过程分析
  7. Android动画-实现雪花飞舞动画效果
  8. 计算机专业英语论文摘要合辑【2】
  9. Vue app安卓端移动端实现微信支付和支付宝支付
  10. uniapp接入谷歌导航功能