在拖放期间在 NSTableView 中打开一个间隙

Posted

技术标签:

【中文标题】在拖放期间在 NSTableView 中打开一个间隙【英文标题】:Opening a gap in NSTableView during drag and drop 【发布时间】:2012-04-25 22:14:10 【问题描述】:

我有一个简单的、单列、基于视图的 NSTableView,其中包含可以拖动以重新排序的项目。在拖放过程中,我想让它在鼠标下方的位置打开要放置的项目的间隙。当您拖动以重新排序曲目时,GarageBand 会执行类似的操作(此处为视频:http://www.screencast.com/t/OmUVHcCNSl)。据我所知,在 NSTableView 中没有内置支持。

有没有其他人尝试将此行为添加到 NSTableView 并找到一个好的解决方案?我已经想到并尝试了几种方法,但没有取得多大成功。我的第一个想法是通过在我的数据源的-tableView:validateDrop:... 方法中发送-noteHeightOfRowsWithIndexesChanged:,然后在-tableView:heightOfRow: 中返回两倍的正常高度,在拖动过程中将鼠标下的行的高度加倍。不幸的是,据我所知,NSTableView 在拖放过程中不会更新其布局,因此尽管调用了noteHeightOfRowsWithIndexesChanged:,但实际上并没有更新行高。

请注意,我使用的是基于视图的 NSTableView,但我的行并不复杂,如果这样做有助于实现这一点,我无法移动到基于单元格的表视图。我知道在拖动完成后为放置的项目设置动画间隙的简单内置功能。我正在寻找一种在拖动过程中打开间隙的方法。此外,这是针对要在 Mac App Store 中销售的应用程序,因此不得使用私有 API。

编辑:我刚刚向 Apple 提交了一项增强请求,请求内置支持此行为:http://openradar.appspot.com/12662624。 如果你也想看的话,请假。 更新:我要求的增强功能已在 OS X 10.9 Mavericks 中实现,现在可以使用 NSTableView API 实现此行为。见NSTableViewDraggingDestinationFeedbackStyleGap。

【问题讨论】:

我也想知道怎么做! 我不确定这是否可行,但是在移动时插入一个空白行怎么样? @Inafziger 我也想到了这一点,但我认为“不幸的是,我能说的最好的情况是,NSTableView 在拖放过程中不会更新其布局”使得它不起作用。 是的,问题是 NSTableView 在拖动处于活动状态时会停止更新布局。也许这种行为可以通过子类化来克服,但我不知道如何。 你可以拖动项目并且让拖动操作不属于 NSTableView 的一部分吗? 【参考方案1】:

我觉得这样做很奇怪,但是这里的队列中有一个非常彻底的答案,似乎已被其作者删除。在其中,他们提供了the correct links to a working solution,我觉得需要将其作为答案提供给其他人,如果他们愿意,也包括他们。

NSTableView 的文档中,针对行动画效果隐藏了以下注意事项:

行动画效果

指定 tableview 将使用淡入淡出来移除行或列的可选常量。该效果可以与任何NSTableViewAnimationOptions 常量组合。

enum 
    NSTableViewAnimationEffectFade = 0x1,
    NSTableViewAnimationEffectGap = 0x2,
;

常量:

...

NSTableViewAnimationEffectGap

为新插入的行创建一个间隙。这对于动画到新打开的间隙的拖放动画很有用,应该在委托方法tableView:acceptDrop:row:dropOperation:中使用。

通过the example code from Apple,我发现了这个:

- (void)_performInsertWithDragInfo:(id <NSDraggingInfo>)info parentNode:(NSTreeNode *)parentNode childIndex:(NSInteger)childIndex 
    // NSOutlineView's root is nil
    id outlineParentItem = parentNode == _rootTreeNode ? nil : parentNode;
    NSMutableArray *childNodeArray = [parentNode mutableChildNodes];
    NSInteger outlineColumnIndex = [[_outlineView tableColumns] indexOfObject:[_outlineView outlineTableColumn]];

    // Enumerate all items dropped on us and create new model objects for them    
    NSArray *classes = [NSArray arrayWithObject:[SimpleNodeData class]];
    __block NSInteger insertionIndex = childIndex;
    [info enumerateDraggingItemsWithOptions:0 forView:_outlineView classes:classes searchOptions:nil usingBlock:^(NSDraggingItem *draggingItem, NSInteger index, BOOL *stop) 
        SimpleNodeData *newNodeData = (SimpleNodeData *)draggingItem.item;
        // Wrap the model object in a tree node
        NSTreeNode *treeNode = [NSTreeNode treeNodeWithRepresentedObject:newNodeData];
        // Add it to the model
        [childNodeArray insertObject:treeNode atIndex:insertionIndex];
        [_outlineView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:insertionIndex] inParent:outlineParentItem withAnimation:NSTableViewAnimationEffectGap];
        // Update the final frame of the dragging item
        NSInteger row = [_outlineView rowForItem:treeNode];
        draggingItem.draggingFrame = [_outlineView frameOfCellAtColumn:outlineColumnIndex row:row];

        // Insert all children one after another
        insertionIndex++;
    ];


我不确定它是否真的这么简单,但如果它不能满足您的需求,至少值得检查和彻底驳斥。

编辑:请参阅此答案的 cmets,了解正确解决方案所遵循的步骤。 OP 已发布a more complete answer,任何寻求相同问题解决方案的人都应该参考。

【讨论】:

感谢戈麦斯先生的回答。我对回答者删除此答案的原因的猜测是,我在问题中提到我知道这一点并且这不是我想要的。这是为了在项目被放下之后设置一个间隙,而不是在拖动期间为(移动)间隙设置动画。如果我不知道如何做我真正想做的事,我实际上已经在使用这个解决方案作为权宜之计/妥协。 @AndrewMadsen 感谢您的友好回复。我原以为是这种情况,但我想确保任何直接寻求这个问题的人都能澄清这一点。我将寻找更好的选择来更优雅地解决这个问题,现在,将此作为路标留给其他潜在的回答者跟随。毕竟:明确地知道你已经尝试和反驳了什么,应该会带来更好的答案。 :) @AndrewMadsen 由于到目前为止没有人回答您的问题,因此我继续深入研究,看看我是否可以自己回答。我在this documentation node 中发现,您可以在目标中实现draggingUpdated: 以获取拖动操作发送方,并从那里获取当前指针位置。 @AndrewMadsen 这里的想法是然后使用这个位置来执行你的动画效果,捕获draggingEnded:draggingExited: 来执行任何必要的清理。这应该允许您在拖动操作期间执行您寻求的操作。请让我知道这是否可行,以便我可以相应地更新我的答案,如果可行,请提供一些示例代码。 :) 我也很高兴,谢谢!为了将来其他人的利益,我将添加我自己的答案,其中包含我的实现细节,包括示例代码。我已授予您的答案赏金,但应稍后对其进行编辑,以澄清(当前/原始)答案并未提供问题的解决方案。【参考方案2】:

注意:这个问题和答案描述的行为现在可以在 OS X 10.9 Mavericks 及更高版本上使用 NSTableView 中的内置 API 获得。见NSTableViewDraggingDestinationFeedbackStyleGap。

如果在面向 OS X 10.8 或更早版本的应用中需要此行为,此答案可能仍然有用。 原答案如下:

我现在已经实现了。我的基本方法如下所示:

@interface ORSGapOpeningTableView : NSTableView

@property (nonatomic) NSInteger dropTargetRow;
@property (nonatomic) CGFloat heightOfDraggedRows;

@end

@implementation ORSGapOpeningTableView

#pragma mark - Dragging

- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender

    NSInteger oldDropTargetRow = self.dropTargetRow;
    NSDragOperation result = [super draggingUpdated:sender];
    CGFloat imageHeight = [[sender draggedImage] size].height;
    self.heightOfDraggedRows = imageHeight;

    NSMutableIndexSet *changedRows = [NSMutableIndexSet indexSet];
    if (oldDropTargetRow > 0) [changedRows addIndex:oldDropTargetRow-1];
    if (self.dropTargetRow > 0) [changedRows addIndex:self.dropTargetRow-1];
    [self noteHeightOfRowsWithIndexesChanged:changedRows];

    return result;


- (void)draggingExited:(id<NSDraggingInfo>)sender

    self.dropTargetRow = -1;
    [self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];

    [super draggingExited:sender];


- (void)draggingEnded:(id<NSDraggingInfo>)sender

    self.dropTargetRow = -1;
    self.heightOfDraggedRows = 0.0;
    self.draggedRows = nil;
    [self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];


- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender

    self.dropTargetRow = -1;
    self.heightOfDraggedRows = 0.0;
    self.draggedRows = nil;
    [self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];

    return [super performDragOperation:sender];

// 在我的委托和数据源中:

- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id<NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation

    if (dropOperation == NSTableViewDropOn) 
    
        dropOperation = NSTableViewDropAbove;
        [self.tableView setDropRow:++row dropOperation:dropOperation];
    

    NSDragOperation result = [self.realDataSource tableView:tableView validateDrop:info proposedRow:row proposedDropOperation:dropOperation];
    if (result != NSDragOperationNone) 
    
        self.tableView.dropTargetRow = row;
     
    else 
    
        self.tableView.dropTargetRow = -1; // Don't open a gap
    
    return result;


- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row

    CGFloat result = [tableView rowHeight];

    if (row == self.tableView.dropTargetRow - 1 && row > -1)
    
        result += self.tableView.heightOfDraggedRows;
    

    return result;

请注意,这是简化的代码,而不是从我的程序中逐字复制/粘贴。实际上,我最终使这一切都包含在使用代理委托和数据源对象的 NSTableView 子类中,因此上述数据源/委托方法中的代码实际上是在代理对真实委托和数据源的调用的拦截中。这样,真正的数据源和委托不必做任何特殊的事情来获得间隙打开行为。此外,表格视图动画有时会有点不稳定,这不适用于第一行上方的拖动(没有打开间隙,因为没有行可以变得更高)。总而言之,尽管还有进一步改进的空间,但这种方法的效果相当不错。

我仍然想尝试类似的方法,但插入一个空白行(如 Caleb 建议的那样)而不是更改行高。

【讨论】:

嘿 Andrew,你有什么办法可以在 github 上抛出一个简单的示例项目来展示这个? ORSGapOpeningTableView.m 中有“代理”数据源和委托对象。这些用于拦截 NSTableView 发送到其自己的数据源和委托的消息,以便无论(外部)数据源/委托如何都可以修改结果。 self.realDataSource 是真实的(即外部设置的)数据源,而不是代理。【参考方案3】:

从 Mac OS X 10.9 (Mavericks) 开始,在 NSTableView 中实现拖放动画的解决方案要简单得多:

[aTableView setDraggingDestinationFeedbackStyle:NSTableViewDraggingDestinationFeedbackStyleGap];

表格视图将在拖动一行时自动插入带有动画的间隙,这比旧的蓝线插入点方法要好得多。

【讨论】:

【参考方案4】:

完成您所要求的一种方法是在建议的放置点(即,在最近的两行之间)插入一个空行。听起来您一直在考虑使用NSTableViewAnimationEffectGap,正如您所注意到的,它实际上是用于在-tableView:acceptDrop:row:dropOperation: 中接受拖放时为插入设置动画。

由于您希望在用户释放鼠标按钮以实际进行放置之前打开间隙,因此您可以使用表格的-draggingUpdate: 方法中的-insertRowsAtIndexes:withAnimation: 插入一个空白行,然后同时使用-removeRowsAtIndexes:withAnimation: 删除您之前为此拖动插入的任何空白行。酌情使用NSTableViewAnimationSlideUpNSTableViewAnimationSlideDown 作为这些操作的动画。

【讨论】:

迦勒,非常感谢您的回答。我实际上并没有关注 NSTableViewAnimationEffectGap,我只提到我知道它并且它没有做我想要的(请参阅我对 MrGomez 回答的第一条评论)。基于 MrGomez cmets,我尝试了一种类似于您描述的方法,它会起作用。所有这一切的关键是在 NSTableView 子类中覆盖 -draggingUpdated:。如果你尝试在数据源的-tableView:validateDrop:... 方法(被-[NSTableView draggingUpdated:] 间接调用)中做类似的事情,它不会起作用。

以上是关于在拖放期间在 NSTableView 中打开一个间隙的主要内容,如果未能解决你的问题,请参考以下文章

在拖放项目期间滚动项目

Chrome F8/热键调试器在拖放操作期间中断

在拖放期间重绘

UITableView - 如何在拖放期间更改单元格的背景颜色?

在 NSTableView 中实现拖放

如何在 OSX 上拖放期间检测 META 按键