Drag & Drop(拖拽)提供了在应用与OS X系统,不同应用之间,应用内部多种场景下 资源,文件,数据可视化交换的极致的一种用户体验。
我们把可以拖拽的视图(view)或窗口(window)称为 拖放源(Drag Sources),接收拖放的视图或窗口称为 拖放目标(Drag Destination)。
拖放开始时会出现代表拖放源Drag Sources的图标顺着鼠标轨迹运动,直到拖放目标Drag Destination 接收了这个拖放请求,就完成了一次成功的拖放。如果拖放目标不能响应这个拖放请求,代表拖放源Drag Sources的图标会以动画形式弹回到拖放源以前的位置。
拖放源和拖放目标之间数据交换是通过使用系统的剪贴板(NSPasteboard)保存数据来完成的。
整个拖放过程中,涉及到拖放源和拖放目标之间一系列的交互处理流程。
拖放开始
用户鼠标点击NSView/NSWindow触发MouseDown事件,调用beginDraggingSessionWithItems方法开始建立一个拖放的session,开始启动拖放过程。
beginDraggingSessionWithItems需要3个参数,按顺序依次为拖放数据items,鼠标的NSEvent,拖放源代理。
-
(void)mouseDown:(NSEvent *)theEvent
{NSMutableArray *draggingItems = [NSMutableArray array];
NSPasteboardItem *pasteboardItem = [NSPasteboardItem new];
DragImageItem *dataProvider = [[DragImageItem alloc]init];
NSData *data = [self.image TIFFRepresentation];
dataProvider.data = data;
[pasteboardItem setDataProvider:dataProvider forTypes:@[kPasteboardTypeName]];
NSDraggingItem *draggingItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pasteboardItem];
[draggingItems addObject:draggingItem];
[self beginDraggingSessionWithItems:draggingItems event:theEvent source:self.dragSourceDelegate];
}
拖放数据定义
拖放源跟拖放目标方约定数据的type类型,拖放源的数据按type类型存储,拖放接收方注册根据类型来获取数据。
NSPasteboardItem
使用NSPasteboardItem定义拖放携带的基本数据信息。NSPasteboardItem提供了3种基本的定义数据的方法和一个使用代理提供数据的方式。
1.基本的数据定义方法
可以定义NSData,NSString,id类型的数据
- (BOOL)setData:(NSData *)data forType:(NSString *)type;
- (BOOL)setString:(NSString *)string forType:(NSString *)type;
- (BOOL)setPropertyList:(id)propertyList forType:(NSString *)type;
2.使用代理方式提供数据
- (BOOL)setDataProvider:(id )dataProvider forTypes:(NSArray *)types;
实现代理类中定义获取数据的协议方法,实际上实现部分仍然是调用NSPasteboardItem的基本方法把数据存储起来。
-
(void)pasteboard:(nullable NSPasteboard *)pasteboard item:(NSPasteboardItem *)item provideDataForType:(NSString *)type;
{[item setData:self.data forType:type];
}
NSDraggingItem
使用NSDraggingItem包装NSPasteboardItem
NSDraggingItem *draggingItem = [[NSDraggingItem alloc] initWithPasteboardWriter:item];
拖放可视化定义
定义拖放过程中的跟随鼠标移动的图像。
draggingFrame中定义了拖放图像的位置,当有多个拖放对象一起拖放时,每个拖放图像的位置是不一样的。
imageComponentsProvider block块中定义了NSDraggingImageComponent对象, 可以在drawingHandler使用Cocoa绘图方法绘制出代表拖放的图像。block最后返回数据对象,方便表示多个不同的拖放源。
NSDraggingItem *draggingItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pasteboardItem];
draggingItem.draggingFrame = NSMakeRect(0, 0, 16, 16);
draggingItem.imageComponentsProvider = {
NSDraggingImageComponent *component = [NSDraggingImageComponent draggingImageComponentWithKey:NSDraggingImageComponentIconKey];
component.frame = NSMakeRect(0, 0, 16, 16);
component.contents = [NSImage imageWithSize:NSMakeSize(16, 16) flipped:NO drawingHandler:NSRect rect {
NSImage *image = ....
[image drawInRect:rect];
return YES;
}];
return @[component];
};
拖放源协议NSDraggingSource
返回允许的拖放操作,代理必须实现的方法
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context;
可选3个方法分别是拖放开始,拖放移动,拖放结束时的位置。
@optional
- (void)draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint;
- (void)draggingSession:(NSDraggingSession *)session movedToPoint:(NSPoint)screenPoint;
- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation;
拖放内容
接收拖放
拖放接收方即拖放目标对象实现拖放目标方协议,通过协议的一系列方法依次调用,最终完成跟拖放源的数据交换,结束拖放。
注册接受的拖放类型
NSView 或 NSWindow 提供了注册拖放类型的方法registerForDraggedTypes
[self.window registerForDraggedTypes:[NSArray arrayWithObjects:
NSColorPboardType,
NSFilenamesPboardType,
NSURLPboardType,
NSFilesPromisePboardType
nil]];
可以使用Cocoa系统中预定义的类型,也可以自定义一种类型。
拖放目标方协议NSDraggingDestination
NSView 或 NSWindow 已经继承了NSDraggingDestination协议,需要拖放目标类来实现协议方法。
@protocol NSDraggingDestination
@optional
//拖放进入目标区
- (NSDragOperation)draggingEntered:(id )sender;
//拖放进入目标区移动
- (NSDragOperation)draggingUpdated:(id )sender;
//拖放退出目标区,拖放的图像会弹回到拖放源
- (void)draggingExited:(id )sender;
//拖放预处理,一般是根据拖放类型type,判断是否接受拖放。
- (BOOL)prepareForDragOperation:(id )sender;
//允许接收拖放,开始接收处理拖放数据
- (BOOL)performDragOperation:(id )sender;
//拖放完成结束
- (void)concludeDragOperation:(id )sender;
。。。
@end
拖放接收方处理过程
拖放源的代表图像进入拖放目标区,触发draggingEntered方法。draggingEntered中必须返回一种有效的拖放操作,如果返回NSDragOperationNone,接收拖放,拖放的图像会弹回到拖放源。
鼠标在拖放目标区移动,触发draggingUpdated。
鼠标退出目标区,调用draggingExited方法,拖放的图像会弹回到拖放源。
拖放源代表图像完全进入拖放目标区,释放鼠标按键。触发prepareForDragOperation方法,1. 如果返回NO,拖放的图像会弹回到拖放源,接收拖放。2. 如果返回YES,调用performDragOperation处理拖放,获取拖放数据,进行相关操作。最后调用concludeDragOperation完成一次成功的拖放。
拖放编程实践
前面的章节完整的阐述了拖放处理的全流程,实际应用中很多场景,不需要进行拖放源部分的编程,只需要进行拖放目标方的编程处理。
文件拖放处理
从OSX 系统Finder目录中拖放一个文件到应用。
自定义应用内部接收拖放的view视图类FileDragView,注册拖放类型,实现目标拖放协议NSDraggingDestination。
注册拖放类型
- (void)awakeFromNib {
[super awakeFromNib];
[self registerForDraggedTypes:@[NSFilenamesPboardType]];
}
拖放文件进入拖放区,返回拖放操作类型
- (NSDragOperation)draggingEntered:(id )sender {
NSLog(@"drag operation entered");
NSDragOperation sourceDragMask = [sender draggingSourceOperationMask];
NSPasteboard *pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
if (sourceDragMask & NSDragOperationLink) {
return NSDragOperationLink;
} else if (sourceDragMask & NSDragOperationCopy) {
return NSDragOperationCopy;
}
}
return NSDragOperationNone;
}
执行拖放处理,获取文件路径。可以通过代理或通知形式发送消息通知Controller去处理。
- (BOOL)performDragOperation:(id )sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
NSLog(@"drop now");
if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
NSInteger numberOfFiles = [files count];
if(numberOfFiles>0)
{
NSString *filePath = [files objectAtIndex:0];
//if(self.delegate){
// [self.delegate didFinishDragWithFile:filePath];
//}
return YES;
}
}
else{
NSLog(@"pboard types(%@) not register!",[pboard types]);
}
return YES;
}
NSTableView的内部数据拖放处理
自定义NSTableView类DragTableView,在awakeFromNib方法中注册拖放类型为kDragTableViewTypeName.
实现NSTableViewDataSource协议,实现下面3个方法
1.数据复制到NSPasteboard
这里是将拖放时选择的的行rowIndexes 按kDragTableViewTypeName类型注册存储到系统剪贴板
- (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet )rowIndexes toPasteboard:(NSPasteboard)pboard {
// Copy the row numbers to the pasteboard.
NSData *zNSIndexSetData = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
[pboard declareTypes:[NSArray arrayWithObject:kDragTableViewTypeName] owner:self];
[pboard setData:zNSIndexSetData forType:kDragTableViewTypeName];
return YES;
}
- validate,返回允许的拖放操作类型
-
(NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id )info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)op {
//Add code here to validate the drop
//NSLog(@validate Drop
);
return NSDragOperationEvery;
}
3.拖放数据处理。
从剪贴板获取到rowIndexes,进行相关处理。
- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id )info
row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation {
NSPasteboard* pboard = [info draggingPasteboard];
NSData* rowData = [pboard dataForType:kDragTableViewTypeName];
NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
NSInteger dragRow = [rowIndexes firstIndex];
return YES;
}
NSOutlineView的数据拖放处理
- 将拖放的节点数据 存储到剪切板
-
(BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:items];
[pboard declareTypes:[NSArray arrayWithObject:kDragOutlineViewTypeName] owner:self];
[pboard setData:data forType:kDragOutlineViewTypeName];
return YES;
}
2.validate,返回允许的拖放操作类型
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index {
// Add code here to validate the drop
NSLog(@"validate Drop");
return NSDragOperationEvery;
}
3.从剪贴板获取到拖放的数据items,进行相关处理。
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id)info item:(id)item childIndex:(NSInteger)index {
NSPasteboard* pboard = [info draggingPasteboard];
NSData* data = [pboard dataForType:kDragOutlineViewTypeName];
NSArray* items = [NSKeyedUnarchiver unarchiveObjectWithData:data];
....
}