如果鼠标移动太快,SwiftUI onHover 不会注册鼠标离开元素
Posted
技术标签:
【中文标题】如果鼠标移动太快,SwiftUI onHover 不会注册鼠标离开元素【英文标题】:SwiftUI onHover doesn't register mouse leaving the element if mouse moves too fast 【发布时间】:2021-01-22 07:58:45 【问题描述】:我在 SwiftUI 中制作了一些自定义滑块视图,它们根据悬停状态更改外观,但如果鼠标移出太快(这实际上是移动光标的非常合理的速度),它会一直保持悬停状态,直到您重新悬停并缓慢地重新离开组件。
有解决办法吗? 悬停代码非常标准:
struct RulerSlider: View
@State var hovering = false
var body: some View
GeometryReader geometry in
ZStack
// Ruler lines
if hovering
Ruler()
.onHover hover in
withAnimation(.easeOut(duration: 0.1))
self.hovering = hover
问题如下:
重现错误的示例代码: https://gist.github.com/rdev/ea0c53448e12835b29faa11fec8e0388
【问题讨论】:
尝试无动画,尝试使行(在父视图中)唯一。如果没有帮助,请准备独立的最小可重现示例进行调试。 我尝试在没有动画的情况下进行,结果相同。如果鼠标移出视图的速度不够慢,则似乎不会触发“mouseleave”事件(或与之等效的任何事件) 添加了一个带有可重现代码的要点链接。虽然在精简示例中不会发生太多,但仍然会发生。 我在我的应用程序中也看到了这一点。即使背景试图在鼠标退出时为零。 【参考方案1】:我今天用一个空的 NSView 上的跟踪区域解决了这个问题。这是在一个半复杂且快速刷新的网格视图中测试的,该视图之前具有您所描绘的相同行为。大约 75 个视图在 GIF capture in this gist 中应用了此修饰符,大多数彼此之间的边框为零。
呼叫站点的糖
import SwiftUI
extension View
func whenHovered(_ mouseIsInside: @escaping (Bool) -> Void) -> some View
modifier(MouseInsideModifier(mouseIsInside))
用空的跟踪视图表示
struct MouseInsideModifier: ViewModifier
let mouseIsInside: (Bool) -> Void
init(_ mouseIsInside: @escaping (Bool) -> Void)
self.mouseIsInside = mouseIsInside
func body(content: Content) -> some View
content.background(
GeometryReader proxy in
Representable(mouseIsInside: mouseIsInside,
frame: proxy.frame(in: .global))
)
private struct Representable: NSViewRepresentable
let mouseIsInside: (Bool) -> Void
let frame: NSRect
func makeCoordinator() -> Coordinator
let coordinator = Coordinator()
coordinator.mouseIsInside = mouseIsInside
return coordinator
class Coordinator: NSResponder
var mouseIsInside: ((Bool) -> Void)?
override func mouseEntered(with event: NSEvent)
mouseIsInside?(true)
override func mouseExited(with event: NSEvent)
mouseIsInside?(false)
func makeNSView(context: Context) -> NSView
let view = NSView(frame: frame)
let options: NSTrackingArea.Options = [
.mouseEnteredAndExited,
.inVisibleRect,
.activeInKeyWindow
]
let trackingArea = NSTrackingArea(rect: frame,
options: options,
owner: context.coordinator,
userInfo: nil)
view.addTrackingArea(trackingArea)
return view
func updateNSView(_ nsView: NSView, context: Context)
static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator)
nsView.trackingAreas.forEach nsView.removeTrackingArea($0)
【讨论】:
@Ryan - 你有没有可能这么聪明! :-) 非常感谢。为什么 onHover() 上的 Apples 修改后只使用这个?? @DuncanGroenewald 哈哈我不知道我是不是......但如果是这样,请雇用我作为初级开发人员! @Ryan - 实际上存在一个问题,因为它阻止了其他手势的发生,例如 .onTapGesture() 必须在 .whenHovered() 修饰符之外。所以你需要把它放在任何需要响应手势事件的控件下面。例如 HStack Toggle($isOn, label:... ..overlay(...).whenHovered .. 表示 Toggle() 不会得到任何鼠标事件。 @DuncanGroenewald 你能用代码打开一个新问题吗?在我自己的项目中将 .whenHovered 应用于 Toggle 外部或将手势应用于 Toggle 标签时,我没有观察到干扰。如果没有 .whenHovered,在 Toggle 之外应用轻击手势将不起作用。 太棒了!完美运行【参考方案2】:onHover
如果您为每个单独的子视图应用它,则它是滞后的。如果您在父容器视图中应用它,它会按预期工作。这是一个例子:
struct ContainerView: View
var elements = (0..<10).map "\($0)"
@State private var lastHoveredId = ""
var body: some View
ScrollView
ForEach(elements, id: \.self) element in
ChildView(lastHoveredId: $lastHoveredId, id: element)
.onHover isHovered in
if isHovered
lastHoveredId = element
else if lastHoveredId == element
lastHoveredId = ""
struct ChildView: View
@Binding var lastHoveredId: String
var id: String
@State private var isHovered = false
var body: some View
Text(id)
.frame(width: 100, height: 30)
.background(isHovered ? Color.primary.opacity(0.2) : .clear)
.animation(.easeIn(duration: 0.2))
.onChange(of: lastHoveredId)
isHovered = $0 == id
【讨论】:
【参考方案3】:您可以通过在容器视图中将onHover
实际应用到您的RulerSliders
来轻松修复此问题,而无需离开 SwiftUI 环境(与公认的答案不同)。换句话说,跟踪当前悬停在List
中的元素(如果有),而不是列出子元素。
public struct RulersList
@State private var hoveredRulerId: RulerModel.ID? = nil
public let rulers: [RulerModel]
public var body: some View
List
ForEach(rulers) ruler in
RulerSlider(ruler, ruler.id == hoveredRulerId)
.onHover(perform: isHovering in
self.hoveredRulerId = isHovering ? ruler.id : nil
)
public struct RulerSlider: View
public let ruler: RulerModel
public var isHovered
public var body: some View
Stuff()
.background(isHovered ? Color.accentColor.opacity(0.10) : Color.clear)
// DON'T DO onHover HERE, IT HAS SEVERE PERFORMANCE PENALTY
请注意ForEach
中的RulerSlider(ruler, ruler.id == hoveredRulerId)
,这是您通知子视图是否被悬停的方式。
【讨论】:
非常有趣!整洁的。你知道为什么会这样吗?很高兴知道有这个选项,虽然这太糟糕了,原生方式需要父母知道和管理孩子的详细信息。 我猜是性能?如果您为列表中的每个子视图应用onHover
修饰符,则必须为每个子视图分别计算跟踪光标焦点和更新绑定值,每个子视图都需要单独的原子事务。但是,如果您将容器本身设置为跟踪元素,那么您只需检查光标的 CGPoint 是否在任何子视图的框架内,然后更新同一事务中的所有依赖绑定。但我无权访问 SwiftUI 源,所以我不确定。
有道理。使用数百个 AppKit NSResponder 比 child-modifier 响应更快,而且成本也不高。实现有些奇怪。以上是关于如果鼠标移动太快,SwiftUI onHover 不会注册鼠标离开元素的主要内容,如果未能解决你的问题,请参考以下文章
在图例圆环图的 onHover 期间更快地加载 Tooltip
急求解决方法 鼠标移动太快时 onmouseout事件无法触发