You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现类原生通知的TableCellView悬停清除按钮持久显示效果

解决方案:表格单元格悬停与清除按钮交互优化

问题分析

当前实现仅通过单元格的mouseEntered/mouseExited控制清除按钮的显示,但清除按钮是独立的浮动视图,不属于单元格层级。当鼠标离开单元格边界时,会直接触发单元格的mouseExited事件隐藏按钮,完全忽略了鼠标停留在按钮上的场景,不符合需求。

修改思路

要实现鼠标在清除按钮上(即使超出单元格范围)时保持单元格悬停状态,需要同时监听单元格和清除按钮的鼠标事件,通过状态计数来判断是否需要维持悬停:

  • 给清除按钮添加鼠标进入/离开的监听
  • 维护一个悬停计数:进入单元格或按钮时计数+1,离开时-1,仅当计数归0时才隐藏按钮并取消单元格悬停状态

具体代码修改

1. 新增状态跟踪变量

在类中添加两个变量,用于跟踪当前悬停行和悬停计数:

// 记录当前悬停的行索引
private var hoveredRowIndex: Int = -1
// 悬停计数:单元格进入+1,按钮进入+1;任一离开则-1
private var hoverCount: Int = 0

2. 给清除按钮添加鼠标事件监听

修改floatingView的初始化代码,添加跟踪矩形以捕获鼠标事件:

var floatingView: NSView = {
    let view = NSView(frame: NSRect(x: 0, y: 0, width: 16, height: 16))
    let shadow = NSShadow()
    shadow.shadowColor = .black.withAlphaComponent(0.3)
    shadow.shadowBlurRadius = 1.5
    shadow.shadowOffset = NSMakeSize(0, -0.5)
    view.shadow = shadow
    
    // 添加跟踪矩形,启用鼠标事件监听
    view.wantsLayer = true
    view.addTrackingRect(view.bounds, owner: self, userData: nil, assumeInside: false)
    
    return view
}()

3. 重写鼠标事件处理逻辑

同时处理单元格和清除按钮的鼠标进入/离开事件:

// 处理单元格的鼠标进入
public func mouseEntered(in rowView: NSTableRowView) {
    let rowIndex = tableView.row(for: rowView)
    if hoveredRowIndex == -1 || hoveredRowIndex == rowIndex {
        hoverCount += 1
        hoveredRowIndex = rowIndex
        showCellFrame(rowView: rowView)
    }
}

// 处理单元格的鼠标离开
public func mouseExited(with event: NSEvent, in rowView: NSTableRowView) {
    let rowIndex = tableView.row(for: rowView)
    if hoveredRowIndex == rowIndex {
        hoverCount -= 1
        updateHoverState()
    }
}

// 处理清除按钮的鼠标进入
override public func mouseEntered(with event: NSEvent) {
    let point = floatingView.convert(event.locationInWindow, from: nil)
    if floatingView.bounds.contains(point) && hoveredRowIndex != -1 {
        hoverCount += 1
    }
}

// 处理清除按钮的鼠标离开
override public func mouseExited(with event: NSEvent) {
    let point = floatingView.convert(event.locationInWindow, from: nil)
    if floatingView.bounds.contains(point) && hoveredRowIndex != -1 {
        hoverCount -= 1
        updateHoverState()
    }
}

// 根据计数更新悬停状态
private func updateHoverState() {
    if hoverCount <= 0 {
        floatingView.isHidden = true
        // 此处添加取消单元格悬停样式的代码(比如还原背景色等)
        hoveredRowIndex = -1
        hoverCount = 0 // 重置计数,避免出现负数
    }
}

4. 调整原有显示逻辑

修正showCellFrame中的行索引引用错误:

func showCellFrame(rowView: NSTableRowView) {
    let rowIndex = tableView.row(for: rowView)
    let cellFrame = tableView.frameOfCell(atColumn: 0, row: rowIndex)
    let convertedFrame = tableView.convert(cellFrame, to: self)
    
    if !isRowPartiallyVisible(rowIndex) || convertedFrame.minY < 72, 
       (convertedFrame.minY + convertedFrame.height) < (scrollView.contentView.frameHeight + 10) {
        showFloatingView(xValue: convertedFrame.minX + convertedFrame.width, 
                         yValue: (convertedFrame.minY + convertedFrame.height) - 8)
    }
}

func showFloatingView(xValue : CGFloat, yValue: CGFloat) {
    floatingView.frame.origin = CGPoint(x: xValue, y: yValue)
    floatingView.isHidden = false
}

关键说明

  • 通过hoverCount统一管理鼠标在单元格或按钮上的状态,只有当两者都离开时才结束悬停
  • 给清除按钮添加trackingRect是捕获其鼠标事件的核心,确保按钮能独立响应鼠标进出
  • 严格匹配hoveredRowIndex,避免不同行的悬停状态互相干扰

内容的提问来源于stack exchange,提问作者RRSK01_11

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 07:20:04