您可以通过创建自己的UIWindow 子类,然后创建该子类的实例来做到这一点。你需要对窗口做三件事:
将其windowLevel 设置为一个非常高的数字,例如CGFloat.max。它被限制在(从 iOS 9.2 开始)为 10000000,但您最好将其设置为可能的最高值。
将窗口的背景颜色设置为 nil 以使其透明。
覆盖窗口的pointInside(_:withEvent:) 方法以仅对按钮中的点返回true。这将使窗口只接受点击按钮的触摸,所有其他触摸都将传递给其他窗口。
然后为窗口创建一个根视图控制器。创建按钮并将其添加到视图层次结构中,并将其告知窗口,以便窗口可以在pointInside(_:withEvent:) 中使用它。
还有最后一件事要做。事实证明,屏幕键盘也使用最高的窗口级别,并且由于它可能在您的窗口之后出现在屏幕上,因此它将位于您的窗口顶部。您可以通过观察 UIKeyboardDidShowNotification 并在发生这种情况时重置窗口的 windowLevel 来解决此问题(因为这样做是为了将您的窗口置于同一级别的所有窗口之上)。
这是一个演示。我将从将在窗口中使用的视图控制器开始。
import UIKit
class FloatingButtonController: UIViewController {
这是按钮实例变量。创建FloatingButtonController 的任何人都可以访问该按钮以向其添加目标/操作。我稍后会证明这一点。
private(set) var button: UIButton!
你必须“实现”这个初始化器,但我不会在故事板中使用这个类。
required init?(coder aDecoder: NSCoder) {
fatalError()
}
这是真正的初始化程序。
init() {
super.init(nibName: nil, bundle: nil)
window.windowLevel = CGFloat.max
window.hidden = false
window.rootViewController = self
设置window.hidden = false 将其显示在屏幕上(当当前CATransaction 提交时)。我还需要注意键盘是否出现:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
}
我需要一个对我的窗口的引用,这将是一个自定义类的实例:
private let window = FloatingButtonWindow()
我将在代码中创建我的视图层次结构以保持这个答案自包含:
override func loadView() {
let view = UIView()
let button = UIButton(type: .Custom)
button.setTitle("Floating", forState: .Normal)
button.setTitleColor(UIColor.greenColor(), forState: .Normal)
button.backgroundColor = UIColor.whiteColor()
button.layer.shadowColor = UIColor.blackColor().CGColor
button.layer.shadowRadius = 3
button.layer.shadowOpacity = 0.8
button.layer.shadowOffset = CGSize.zero
button.sizeToFit()
button.frame = CGRect(origin: CGPointMake(10, 10), size: button.bounds.size)
button.autoresizingMask = []
view.addSubview(button)
self.view = view
self.button = button
window.button = button
那里没有什么特别的。我只是在创建我的根视图并在其中放置一个按钮。
为了允许用户拖动按钮,我将在按钮上添加一个平移手势识别器:
let panner = UIPanGestureRecognizer(target: self, action: "panDidFire:")
button.addGestureRecognizer(panner)
}
窗口将在第一次出现时布置其子视图,并在调整大小时(特别是因为界面旋转),所以我想在这些时候重新定位按钮:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
snapButtonToSocket()
}
(稍后将详细了解snapButtonToSocket。)
为了处理按钮的拖动,我以标准方式使用平移手势识别器:
func panDidFire(panner: UIPanGestureRecognizer) {
let offset = panner.translationInView(view)
panner.setTranslation(CGPoint.zero, inView: view)
var center = button.center
center.x += offset.x
center.y += offset.y
button.center = center
您要求“捕捉”,所以如果平移结束或取消,我会将按钮捕捉到我称之为“套接字”的固定数量的位置之一:
if panner.state == .Ended || panner.state == .Cancelled {
UIView.animateWithDuration(0.3) {
self.snapButtonToSocket()
}
}
}
我通过重置window.windowLevel来处理键盘通知:
func keyboardDidShow(note: NSNotification) {
window.windowLevel = 0
window.windowLevel = CGFloat.max
}
要将按钮捕捉到插槽,我会找到离按钮位置最近的插槽并将按钮移动到那里。请注意,这不一定是您想要的界面旋转,但我会为读者留下一个更完美的解决方案作为练习。无论如何,它会在旋转后将按钮保留在屏幕上。
private func snapButtonToSocket() {
var bestSocket = CGPoint.zero
var distanceToBestSocket = CGFloat.infinity
let center = button.center
for socket in sockets {
let distance = hypot(center.x - socket.x, center.y - socket.y)
if distance < distanceToBestSocket {
distanceToBestSocket = distance
bestSocket = socket
}
}
button.center = bestSocket
}
我在屏幕的每个角落都放了一个插座,中间放了一个用于演示目的:
private var sockets: [CGPoint] {
let buttonSize = button.bounds.size
let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
let sockets: [CGPoint] = [
CGPointMake(rect.minX, rect.minY),
CGPointMake(rect.minX, rect.maxY),
CGPointMake(rect.maxX, rect.minY),
CGPointMake(rect.maxX, rect.maxY),
CGPointMake(rect.midX, rect.midY)
]
return sockets
}
}
最后,自定义UIWindow 子类:
private class FloatingButtonWindow: UIWindow {
var button: UIButton?
init() {
super.init(frame: UIScreen.mainScreen().bounds)
backgroundColor = nil
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
正如我所提到的,我需要覆盖 pointInside(_:withEvent:) 以便窗口忽略按钮外部的触摸:
private override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
guard let button = button else { return false }
let buttonPoint = convertPoint(point, toView: button)
return button.pointInside(buttonPoint, withEvent: event)
}
}
现在你如何使用这个东西?我下载了Apple's AdaptivePhotos sample project 并将我的FloatingButtonController.swift 文件添加到AdaptiveCode 目标。我给AppDelegate添加了一个属性:
var floatingButtonController: FloatingButtonController?
然后我在application(_:didFinishLaunchingWithOptions:)的末尾添加代码来创建FloatingButtonController:
floatingButtonController = FloatingButtonController()
floatingButtonController?.button.addTarget(self, action: "floatingButtonWasTapped", forControlEvents: .TouchUpInside)
这些行就在函数末尾的return true 之前。我还需要写按钮的动作方法:
func floatingButtonWasTapped() {
let alert = UIAlertController(title: "Warning", message: "Don't do that!", preferredStyle: .Alert)
let action = UIAlertAction(title: "Sorry…", style: .Default, handler: nil)
alert.addAction(action)
window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
这就是我所要做的。但为了演示目的,我还做了一件事:在 AboutViewController 中,我将 label 更改为 UITextView,这样我就有办法调出键盘了。
按钮如下所示:
这是点击按钮的效果。请注意,按钮浮动在警报上方:
当我调出键盘时会发生以下情况:
它是否处理旋转?你打赌:
好吧,旋转处理并不完美,因为旋转后哪个套接字最接近按钮可能与旋转前的“逻辑”套接字不同。您可以通过跟踪按钮上次捕捉到哪个插槽并专门处理旋转(通过检测大小变化)来解决此问题。
为了您的方便,我将整个FloatingViewController.swift 放在this gist 中。