iOS HUD (Heads Up Display) 旋转框

Posted 北冥鱼_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS HUD (Heads Up Display) 旋转框相关的知识,希望对你有一定的参考价值。

一、隐藏软键盘

在 viewDidLoad() 添加 gestureRecognizer

// Hide keyboard
let gestureRecognizer = UITapGestureRecognizer(
  target: self, 
  action: #selector(hideKeyboard))
gestureRecognizer.cancelsTouchesInView = false
tableView.addGestureRecognizer(gestureRecognizer)
@objc func hideKeyboard(
  _ gestureRecognizer: UIGestureRecognizer
) {
  let point = gestureRecognizer.location(in: tableView)
  let indexPath = tableView.indexPathForRow(at: point)

  if indexPath != nil && indexPath!.section == 0 && 
  indexPath!.row == 0 {
    return
  }
  descriptionTextView.resignFirstResponder()
}

以上代码确保只有点击 section 0, row 0 之外部分才会隐藏软键盘。

The HUD (Heads Up Display)

HUD,是 Heads-Up Display 的缩写。HUD 通常用于像下载文件或执行其它长期任务时显示进度条。

HUD 是 UIView 的子类,我们可以在其它视图之上添加 HUD。实际上,labels 是添加在 cells 顶部的 view,cells 是被添加到 table view 顶部的 view,而 table view 又是被添加在 navigation controller 的顶部的内容视图。

创建 HUD view

HudView.swift:

import UIKit

class HudView: UIView {
  var text = ""

  class func hud(
    inView view: UIView, 
    animated: Bool
  ) -> HudView {
    let hudView = HudView(frame: view.bounds)
    hudView.isOpaque = false

    view.addSubview(hudView)
    view.isUserInteractionEnabled = false

    hudView.backgroundColor = UIColor(
      red: 1, 
      green: 0, 
      blue: 0, 
      alpha: 0.5)
    return hudView
  }

   override func draw(_ rect: CGRect) {
      let boxWidth: CGFloat = 96
      let boxHeight: CGFloat = 96

      let boxRect = CGRect(
        x: round((bounds.size.width - boxWidth) / 2),
        y: round((bounds.size.height - boxHeight) / 2),
        width: boxWidth,
        height: boxHeight)

      let roundedRect = UIBezierPath(
        roundedRect: boxRect, 
        cornerRadius: 10)
      UIColor(white: 0.3, alpha: 0.8).setFill()
      roundedRect.fill()

      // Draw checkmark
      if let image = UIImage(named: "Checkmark") {
           let imagePoint = CGPoint(
              x: center.x - round(image.size.width / 2),
              y: center.y - round(image.size.height / 2) - boxHeight / 8)
              image.draw(at: imagePoint)
        }

       // Draw the text
       let attribs = [ 
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),
            NSAttributedString.Key.foregroundColor: UIColor.white
       ]

       let textSize = text.size(withAttributes: attribs)

       let textPoint = CGPoint(
             x: center.x - round(textSize.width / 2),
             y: center.y - round(textSize.height / 2) + boxHeight / 4)                                      
             text.draw(at: textPoint, withAttributes: attribs)
      }
}

方法 hud(inView, animated) 被称为便利构造函数。它创建并返回一个新的 HudView 实例。

使用示例:

let hudView = HudView.hud(inView: parentView, animated: true)

构造函数通常是一个类方法,即不用实例化对象就可以调用的方法,声明它,以 class func 开头,而不是 func 。

使用 HUD view
@IBAction func done() {
  guard let mainView = navigationController?.parent?.view 
  else { return }
  let hudView = HudView.hud(inView: mainView, animated: true)
  hudView.text = "Tagged"
}

guard let

是一种语法糖,我们可以这样使用它:

var mainView: UIView
if let view = navigationController?.parent?.view {
  mainView = view
} else {
  return
}

使用 guard let 这种方式,可以精简代码。

使用导航控制器父级中的视图 - 导航控制器的父级是 tab bar controller。这样可以保证 HUD 覆盖了 navigation controller 和 tab bar controlle 的查看区域。

每当 UIKit 希望视图重绘时,都会调用 draw() 方法。

ios 中的所有内容都是事件驱动的。除非 UIKit 要求视图自行绘制,否则视图不会在屏幕上绘制任何内容。这意味着我们永远不要自己调用 draw() 。

如果要重绘视图,则应向 UIKit 发送 setNeedsDisplay() 消息。准备好执行绘图时,UIKit 然后将触发 draw() 。

let boxRect = CGRect(
  x: round((bounds.size.width - boxWidth) / 2),
  y: round((bounds.size.height - boxHeight) / 2),
  width: boxWidth,
  height: boxHeight)

round() 确保矩形的长宽不为小数,因为这会使图像看起来模糊。

let roundedRect = UIBezierPath(roundedRect: boxRect, cornerRadius: 10)
UIColor(white: 0.3, alpha: 0.8).setFill()
roundedRect.fill()

用 UIBezierPath 绘制带有圆角的矩形,非常方便。

运行 app,结果如下所示:

添加动画

在 HudView.swift 添加如下代码:

 // MARK: - Helper methods
func show(animated: Bool) {
  if animated {
    // 1
    alpha = 0
    transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
    // 2
    UIView.animate(withDuration: 0.3) {
      // 3
      self.alpha = 1
      self.transform = CGAffineTransform.identity
    }
  }
}

在动画开始之前设置视图的初始状态。这里,将alpha 设置为0,使视图完全透明。将 scale factor 设置为 1.3 ,这样,视图最初会放大到比正常情况下大 1.3 倍的大小。

调用 UIView.animate(withDuration:animations:) 方法设置动画。将方法传递给闭包,在闭包中执行动画。闭包是一段内联代码,不会立即执行。UIKit 将在闭包内部设置动画终止时的状态。

在闭包内部,设置动画完成后的视图状态。将 alpha 设置为 1,让 HudView 完全不透明。我们还可以将变换设置为“identity”变换,将比例恢复为正常。由于此代码使用闭包,因此需要使用 self 来引用 HudView 实例及其属性。这就是关闭的规则。

将 hud(inView:animated:) 方法更改为在返回之前立即调用 show(animated:):

class func hud(inView view: UIView, animated: Bool) -> HudView {
  . . .
  hudView.show(animated: animated)    // Add this
  return hudView
}
优化动画

iOS 有一种称为“spring”动画的东西,它可以上下反弹,并且在视觉上比普通的旧版动画更有趣。使用起来非常简单。

修改UIView.animate(withDuration:animations:) 代码:

UIView.animate(
  withDuration: 0.3, 
  delay: 0, 
  usingSpringWithDamping: 0.7, 
  initialSpringVelocity: 0.5,
  options: [], 
  animations: {
    self.alpha = 1
    self.transform = CGAffineTransform.identity
  }, completion: nil) 

Swift 5.3 引入了多个尾随闭包(这里如animations 和 completion)。但是,由于completion中没有代码,因此无法在此处使用 non-trailing closure 语法 - 除非我们将空闭包传递到 completion 闭包。

运行该 app 并观看它的反弹动画。效果更好一些!

处理 navigation

GCD 是一个非常方便但有些底层的库,可以用于处理异步任务。让 app 在执行一些代码之前等待几秒钟,是异步任务的一个完美示例。

将以下代码添加到 done() 方法底部:

let delayInSeconds = 0.6
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) {
  self.navigationController?.popViewController(animated: true)
}

DispatchQueue.main.asyncAfter() 函数将闭包作为其最终参数。在该闭包中,我们告诉导航控制器返回到导航堆栈中的上一个视图控制器。

DispatchQueue.main.asyncAfter() 在 .now() + delayInSeconds 时间之后执行操作。

我们发现返回上一页面后,HUD 并没有消失,这是不好的体验。

我们进行如下优化,将以下方法添加到 HudView.swift:

func hide() {
  superview?.isUserInteractionEnabled = true
  removeFromSuperview()
}

返回上一页面之前,调用此新方法隐藏 HUD。

在 LocationDetailsViewController.swift 中修改 done() 方法:

DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) {
  hudView.hide()   // Add this line
  self.navigationController?.popViewController(animated: true)
}
代码精简优化

我们可以把方法抽取到一个工具类,Functions.swift:

import Foundation

func afterDelay(_ seconds: Double, run: @escaping () -> Void) {
  DispatchQueue.main.asyncAfter(
    deadline: .now() + seconds, 
    execute: run)
}

这是一个自由函数,而不是对象内部的方法。因此,可以在代码中的任何位置使用它。

仔细看 afterDelay() 的第二个参数,一个名为 run 的参数,它的类型是 () -> Void。在 Swift 中表示带参数且无返回值的闭包。

闭包的类型通常如下所示:

(parameter list) -> return type

在这种情况下,参数列表和返回值均为空, () 和 Void。这也可以写为 Void -> Void 甚至 () -> (),但是推荐这样写 () -> Void,因为它看起来更像一个函数声明。

对于不立即执行的 closures,@escaping 注释是必需的。 这样一来,Swift 便知道应该持有该 closures 一段时间。

回到 LocationDetailsViewController.swift,按如下所示修改done() :

@IBAction func done() {
  ...
  hudView.text = "Tagged"
  afterDelay(0.6) {
    hudView.hide()
    self.navigationController?.popViewController(animated: true)
  }
}

通过将令人讨厌的 GCD 内容移至一个新函数 afterDelay() 中,我们抽象了代码,使跟踪变得更加容易。

编写好的程序就是寻找正确的抽象(abstractions)。

注意:由于引用导航控制器 (navigation controller) 的代码位于闭包中,因此需要使用 self。在闭包内部,我们始终需要显式使用self。但是,这里并不需要在引用 hudView 的行上也加上 self,这是因为 hudView 是一个局部变量,它仅在 done() 方法内存在。

源码地址:https://github.com/MFiOSDemos/HudDemos.git

以上是关于iOS HUD (Heads Up Display) 旋转框的主要内容,如果未能解决你的问题,请参考以下文章

Android 5.0以上heads up通知

Heads Up通知和徽章在某些Android设备中没有显示?

如何添加HUD? (iOS)

在 iOS 8 中隐藏音量 HUD

[HTML5] Level up -- Display

在 Unity3D 游戏中自定义 iOS 音量 HUD