diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 87a18ee..7e4dd31 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -79,6 +79,8 @@ 049FB20B2EC1C13800FAB05D /* KBSkinBottomActionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB20A2EC1C13800FAB05D /* KBSkinBottomActionView.m */; }; 049FB20E2EC1CD2800FAB05D /* KBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB20D2EC1CD2800FAB05D /* KBAlert.m */; }; 049FB2112EC1F72F00FAB05D /* KBMyListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2102EC1F72F00FAB05D /* KBMyListCell.m */; }; + 049FB2172EC20A6600FAB05D /* BMLongPressDragCellCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2132EC20A6600FAB05D /* BMLongPressDragCellCollectionView.m */; }; + 049FB21A2EC20A9E00FAB05D /* KBMyKeyBoardVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2192EC20A9E00FAB05D /* KBMyKeyBoardVC.m */; }; 04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97082EB31B14007BD342 /* KBHUD.m */; }; 04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */; }; 04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */; }; @@ -133,6 +135,7 @@ A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */; }; A1B2E1022EBC7AAA00000001 /* HomeHotCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0042EBC7AAA00000001 /* HomeHotCell.m */; }; ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; }; + 049FB31D2EC21BCD00FAB05D /* KBMyKeyboardCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -275,6 +278,14 @@ 049FB20D2EC1CD2800FAB05D /* KBAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAlert.m; sourceTree = ""; }; 049FB20F2EC1F72F00FAB05D /* KBMyListCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyListCell.h; sourceTree = ""; }; 049FB2102EC1F72F00FAB05D /* KBMyListCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyListCell.m; sourceTree = ""; }; + 049FB2122EC20A6600FAB05D /* BMLongPressDragCellCollectionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BMLongPressDragCellCollectionView.h; sourceTree = ""; }; + 049FB2132EC20A6600FAB05D /* BMLongPressDragCellCollectionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BMLongPressDragCellCollectionView.m; sourceTree = ""; }; + 049FB2142EC20A6600FAB05D /* BMLongPressDragCellCollectionViewDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BMLongPressDragCellCollectionViewDataSource.h; sourceTree = ""; }; + 049FB2152EC20A6600FAB05D /* BMLongPressDragCellCollectionViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BMLongPressDragCellCollectionViewDelegate.h; sourceTree = ""; }; + 049FB2182EC20A9E00FAB05D /* KBMyKeyBoardVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyKeyBoardVC.h; sourceTree = ""; }; + 049FB2192EC20A9E00FAB05D /* KBMyKeyBoardVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyKeyBoardVC.m; sourceTree = ""; }; + 049FB31B2EC21BCD00FAB05D /* KBMyKeyboardCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyKeyboardCell.h; sourceTree = ""; }; + 049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyKeyboardCell.m; sourceTree = ""; }; 04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = ""; }; 04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = ""; }; 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = ""; }; @@ -619,6 +630,7 @@ 048908D32EBF618E00FABA60 /* Vender */ = { isa = PBXGroup; children = ( + 049FB2162EC20A6600FAB05D /* BMLongPressDragCellCollectionView */, 048908D92EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout */, ); path = Vender; @@ -633,6 +645,17 @@ path = UICollectionViewLeftAlignedLayout; sourceTree = ""; }; + 049FB2162EC20A6600FAB05D /* BMLongPressDragCellCollectionView */ = { + isa = PBXGroup; + children = ( + 049FB2122EC20A6600FAB05D /* BMLongPressDragCellCollectionView.h */, + 049FB2132EC20A6600FAB05D /* BMLongPressDragCellCollectionView.m */, + 049FB2142EC20A6600FAB05D /* BMLongPressDragCellCollectionViewDataSource.h */, + 049FB2152EC20A6600FAB05D /* BMLongPressDragCellCollectionViewDelegate.h */, + ); + path = BMLongPressDragCellCollectionView; + sourceTree = ""; + }; 04A9FE122EB4D0D20020DB6D /* Manager */ = { isa = PBXGroup; children = ( @@ -831,6 +854,8 @@ 04FC95BC2EB1E3B1007BD342 /* V */ = { isa = PBXGroup; children = ( + 049FB31B2EC21BCD00FAB05D /* KBMyKeyboardCell.h */, + 049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */, 048908E12EBF760000FABA60 /* MySkinCell.h */, 048908E22EBF760000FABA60 /* MySkinCell.m */, 048908E42EBF841B00FABA60 /* KBSkinDetailTagCell.h */, @@ -856,6 +881,8 @@ children = ( 04FC95D02EB1E7AE007BD342 /* MyVC.h */, 04FC95D12EB1E7AE007BD342 /* MyVC.m */, + 049FB2182EC20A9E00FAB05D /* KBMyKeyBoardVC.h */, + 049FB2192EC20A9E00FAB05D /* KBMyKeyBoardVC.m */, 048908DE2EBF73DC00FABA60 /* MySkinVC.h */, 048908DF2EBF73DC00FABA60 /* MySkinVC.m */, 048908E12EBF821700FABA60 /* KBSkinDetailVC.h */, @@ -1355,6 +1382,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 049FB31D2EC21BCD00FAB05D /* KBMyKeyboardCell.m in Sources */, 048909F62EC0AAAA00FABA60 /* KBCategoryTitleCell.m in Sources */, 048909F72EC0AAAA00FABA60 /* KBCategoryTitleView.m in Sources */, 04890A042EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m in Sources */, @@ -1377,6 +1405,7 @@ 048908EF2EBF861800FABA60 /* KBSkinSectionTitleCell.m in Sources */, 048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */, 0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */, + 049FB21A2EC20A9E00FAB05D /* KBMyKeyBoardVC.m in Sources */, 048908D22EBF611D00FABA60 /* KBHistoryMoreCell.m in Sources */, 04FC95D82EB1EA16007BD342 /* BaseCell.m in Sources */, 0477BDF72EBC63A80055D639 /* KBTestVC.m in Sources */, @@ -1392,6 +1421,7 @@ 048908E02EBF73DC00FABA60 /* MySkinVC.m in Sources */, 048908F22EC047FD00FABA60 /* KBShopHeadView.m in Sources */, 048908EC2EBF849300FABA60 /* KBSkinTagsContainerCell.m in Sources */, + 049FB2172EC20A6600FAB05D /* BMLongPressDragCellCollectionView.m in Sources */, 0477BDF02EBB76E30055D639 /* HomeSheetVC.m in Sources */, 048908E62EBF841B00FABA60 /* KBSkinDetailTagCell.m in Sources */, 04FC97002EB30A00007BD342 /* KBGuideTopCell.m in Sources */, diff --git a/keyBoard/Class/Me/V/KBMyKeyboardCell.h b/keyBoard/Class/Me/V/KBMyKeyboardCell.h new file mode 100644 index 0000000..27e1ba1 --- /dev/null +++ b/keyBoard/Class/Me/V/KBMyKeyboardCell.h @@ -0,0 +1,24 @@ +// +// KBMyKeyboardCell.h +// keyBoard +// +// 自定义键盘截图页的标签 Cell(带表情、文案、右上角删除圆点)。 +// 使用 Masonry 约束,支持根据文案动态计算宽度。 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBMyKeyboardCell : UICollectionViewCell + +/// 配置展示内容 +- (void)configEmoji:(NSString *)emoji title:(NSString *)title; + +/// 根据文案计算自适应尺寸(高度固定,宽度随文本变化) ++ (CGSize)sizeForEmoji:(NSString *)emoji title:(NSString *)title; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Me/V/KBMyKeyboardCell.m b/keyBoard/Class/Me/V/KBMyKeyboardCell.m new file mode 100644 index 0000000..af886f4 --- /dev/null +++ b/keyBoard/Class/Me/V/KBMyKeyboardCell.m @@ -0,0 +1,161 @@ +// +// KBMyKeyboardCell.m +// keyBoard +// +// 截图页标签样式 cell,实现: +// - 左侧 Emoji,中间标题; +// - 右上角小圆点(减号),用于删除编辑态展示; +// - 白底圆角,阴影/描边轻微以贴近设计; +// - 暴露 +sizeForEmoji:title: 以便外部根据文本动态决定 item 宽度。 +// + +#import "KBMyKeyboardCell.h" + +@interface KBMyKeyboardCell () +@property (nonatomic, strong) UIView *coverView; // 左侧表情 + +@property (nonatomic, strong) UILabel *emojiLabel; // 左侧表情 +@property (nonatomic, strong) UILabel *titleLabel; // 标题 +@property (nonatomic, strong) UIView *minusBadge; // 右上角减号圆点 +@property (nonatomic, strong) UILabel *minusLabel; // 减号 +@end + +@implementation KBMyKeyboardCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.contentView.backgroundColor = [UIColor clearColor]; + + // 轻微阴影,让卡片更立体 + self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.08].CGColor; + self.layer.shadowOpacity = 1.0; + self.layer.shadowOffset = CGSizeMake(0, 2); + self.layer.shadowRadius = 6; + + [self.contentView addSubview:self.coverView]; + + [self.coverView addSubview:self.emojiLabel]; + [self.coverView addSubview:self.titleLabel]; + [self.contentView addSubview:self.minusBadge]; + [self.minusBadge addSubview:self.minusLabel]; + + [self.coverView mas_makeConstraints:^(MASConstraintMaker *make) { + make.bottom.left.equalTo(self.contentView).offset(0); + make.height.mas_equalTo(36); + }]; + + // Masonry 布局:左右 12 内边距,高度固定由外部控制 + [self.emojiLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.coverView).offset(12); + make.centerY.equalTo(self.coverView); + }]; + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.emojiLabel.mas_right).offset(8); + make.centerY.equalTo(self.coverView); + make.right.lessThanOrEqualTo(self.coverView).offset(-12); + }]; + + // 右上角删除圆点(展示用),默认显示;如需编辑态控制,可对其 hidden 做处理 + [self.minusBadge mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.height.mas_equalTo(11); + make.top.equalTo(self.contentView).offset(0); + make.right.equalTo(self.contentView).offset(0); + }]; + [self.minusLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.minusBadge); + }]; + } + return self; +} + +- (void)prepareForReuse { + [super prepareForReuse]; + self.emojiLabel.text = @""; + self.titleLabel.text = @""; +} + +- (void)configEmoji:(NSString *)emoji title:(NSString *)title { + // 配置 UI 文案(为空补空串,避免计算崩溃) + self.emojiLabel.text = emoji ?: @""; + self.titleLabel.text = title ?: @""; +} + ++ (CGSize)sizeForEmoji:(NSString *)emoji title:(NSString *)title { + // 固定高度 44,宽度 = 左右内边距(12+12) + Emoji 宽(按 20 号字估算) + 间距(8) + 文案宽 + UIFont *emojiFont = [UIFont systemFontOfSize:20]; + UIFont *titleFont = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + + CGFloat emojiW = 0; + if (emoji.length > 0) { + CGSize s = [emoji sizeWithAttributes:@{NSFontAttributeName:emojiFont}]; + emojiW = ceil(s.width); + } else { + emojiW = 0; // 没有表情则不占宽 + } + + CGFloat textW = 0; + if (title.length > 0) { + CGSize s = [title sizeWithAttributes:@{NSFontAttributeName:titleFont}]; + textW = ceil(s.width); + } + + CGFloat width = 12 + emojiW + (emojiW > 0 ? 8 : 0) + textW + 12; + CGFloat minW = 84; // 最小宽保证视觉 + CGFloat maxW = UIScreen.mainScreen.bounds.size.width - 16 * 2; // 不超过屏幕可视宽 + width = MAX(minW, MIN(width, maxW)); + return CGSizeMake(width, 41.0); +} + +#pragma mark - Lazy + +- (UILabel *)emojiLabel { + if (!_emojiLabel) { + _emojiLabel = [UILabel new]; + _emojiLabel.font = [UIFont systemFontOfSize:20]; + _emojiLabel.text = @"😀"; // 默认 + } + return _emojiLabel; +} + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [UILabel new]; + _titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + _titleLabel.textColor = [UIColor colorWithHex:0x1B1F1A]; + } + return _titleLabel; +} + +- (UIView *)minusBadge { + if (!_minusBadge) { + _minusBadge = [UIView new]; + _minusBadge.backgroundColor = [UIColor colorWithHex:KBColorValue]; + _minusBadge.layer.cornerRadius = 5.5; + _minusBadge.layer.masksToBounds = YES; + } + return _minusBadge; +} + +- (UILabel *)minusLabel { + if (!_minusLabel) { + _minusLabel = [UILabel new]; + _minusLabel.text = @"-"; + _minusLabel.textColor = [UIColor whiteColor]; + _minusLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightBold]; + _minusLabel.textAlignment = NSTextAlignmentCenter; + } + return _minusLabel; +} + +- (UIView *)coverView{ + if (!_coverView) { + _coverView = [[UIView alloc] init]; + _coverView.backgroundColor = [UIColor whiteColor]; + _coverView.layer.cornerRadius = 4; + _coverView.layer.masksToBounds = true; + } + return _coverView; +} + +@end + diff --git a/keyBoard/Class/Me/VC/KBMyKeyBoardVC.h b/keyBoard/Class/Me/VC/KBMyKeyBoardVC.h new file mode 100644 index 0000000..2a13cec --- /dev/null +++ b/keyBoard/Class/Me/VC/KBMyKeyBoardVC.h @@ -0,0 +1,16 @@ +// +// KBMyKeyBoardVC.h +// keyBoard +// +// Created by Mac on 2025/11/10. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBMyKeyBoardVC : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Me/VC/KBMyKeyBoardVC.m b/keyBoard/Class/Me/VC/KBMyKeyBoardVC.m new file mode 100644 index 0000000..7b8b04c --- /dev/null +++ b/keyBoard/Class/Me/VC/KBMyKeyBoardVC.m @@ -0,0 +1,240 @@ +// +// KBMyKeyBoardVC.m +// keyBoard +// +// Created by Mac on 2025/11/10. +// + +#import "KBMyKeyBoardVC.h" +#import "BMLongPressDragCellCollectionView.h" +#import "UICollectionViewLeftAlignedLayout.h" +#import "KBMyKeyboardCell.h" + +/// 复用标识 +static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId"; + +/// 截图页 - 自定义键盘管理 +/// 要点: +/// 1)使用 BMLongPressDragCellCollectionView 支持长按拖拽排序; +/// 2)cell 宽度根据文案自适应; +/// 3)全部使用 Masonry 进行布局,并采用懒加载创建控件; +@interface KBMyKeyBoardVC () + +// UI +@property (nonatomic, strong) UIView *sheetView; // 底部白色容器(圆角) +@property (nonatomic, strong) BMLongPressDragCellCollectionView *collectionView; // 可拖拽的列表 +@property (nonatomic, strong) UIButton *saveButton; // 保存按钮 + +// 数据源(必须是二维数组,库内部会在拖动时直接调整顺序) +@property (nonatomic, strong) NSMutableArray *> *dataSourceArray; // {emoji,title} + +@end + +@interface KBMyKeyBoardVC () + +@end + +@implementation KBMyKeyBoardVC + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = [UIColor colorWithHex:0xF6F8F9]; + self.navigationController.title = @"My KeyBoard"; + // 布局视图 + [self.view addSubview:self.sheetView]; + [self.sheetView addSubview:self.collectionView]; + [self.view addSubview:self.saveButton]; + + + [self.sheetView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self.view); + make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 60); + make.bottom.equalTo(self.view).offset(16); + }]; + + [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.top.equalTo(self.sheetView); + make.bottom.equalTo(self.saveButton.mas_top).offset(-15); + }]; + + [self.saveButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, 24, 0, 24)); + make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12); + make.height.mas_equalTo(50); + }]; + + // 初始数据 + [self buildDefaultData]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + // 隐藏系统导航栏 +// [self.navigationController setNavigationBarHidden:YES animated:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; +// if (self.isMovingFromParentViewController || self.isBeingDismissed) { +// [self.navigationController setNavigationBarHidden:NO animated:animated]; +// } +} + + + +#pragma mark - Data + +/// 构造示例数据(二维数组,仅 1 个 section) +- (void)buildDefaultData { + NSArray *arr = @[ + @{@"emoji":@"😊", @"title":@"Humor"}, + @{@"emoji":@"😄", @"title":@"Jokes"}, + @{@"emoji":@"🥰", @"title":@"Love"}, + @{@"emoji":@"🤔", @"title":@"Thinking"}, + @{@"emoji":@"🔥", @"title":@"Hot"}, + @{@"emoji":@"🎉", @"title":@"Celebrate"}, + @{@"emoji":@"🧠", @"title":@"Brainstorm"}, + @{@"emoji":@"🐱", @"title":@"Cats"}, + @{@"emoji":@"😂", @"title":@"LOL"}, + @{@"emoji":@"📸", @"title":@"Photography"}, + @{@"emoji":@"🌟", @"title":@"Star"}, + @{@"emoji":@"🍀", @"title":@"Lucky"}, + @{@"emoji":@"📚", @"title":@"Knowledge"}, + @{@"emoji":@"🎵", @"title":@"Music"}, + @{@"emoji":@"🚀", @"title":@"Launch"}, + @{@"emoji":@"😊", @"title":@"Humor"}, + @{@"emoji":@"😄", @"title":@"Jokes"}, + @{@"emoji":@"🥰", @"title":@"Love"}, + @{@"emoji":@"🤔", @"title":@"Thinking"}, + @{@"emoji":@"🔥", @"title":@"Hot"}, + @{@"emoji":@"🎉", @"title":@"Celebrate"}, + @{@"emoji":@"🧠", @"title":@"Brainstorm"}, + @{@"emoji":@"🐱", @"title":@"Cats"}, + @{@"emoji":@"😂", @"title":@"LOL"}, + @{@"emoji":@"📸", @"title":@"Photography"}, + @{@"emoji":@"🌟", @"title":@"Star"}, + @{@"emoji":@"🍀", @"title":@"Lucky"}, + @{@"emoji":@"📚", @"title":@"Knowledge"}, + @{@"emoji":@"🎵", @"title":@"Music"}, + @{@"emoji":@"🚀", @"title":@"Launch"}, + @{@"emoji":@"😊", @"title":@"Humor"}, + @{@"emoji":@"😄", @"title":@"Jokes"}, + @{@"emoji":@"🥰", @"title":@"Love"}, + @{@"emoji":@"🤔", @"title":@"Thinking"}, + @{@"emoji":@"🔥", @"title":@"Hot"}, + @{@"emoji":@"🎉", @"title":@"Celebrate"}, + @{@"emoji":@"🧠", @"title":@"Brainstorm"}, + @{@"emoji":@"🐱", @"title":@"Cats"}, + @{@"emoji":@"😂", @"title":@"LOL"}, + @{@"emoji":@"📸", @"title":@"Photography"}, + @{@"emoji":@"🌟", @"title":@"Star"}, + @{@"emoji":@"🍀", @"title":@"Lucky"}, + @{@"emoji":@"📚", @"title":@"Knowledge"}, + @{@"emoji":@"🎵", @"title":@"Music"}, + @{@"emoji":@"🚀", @"title":@"Launch"}, + ]; + self.dataSourceArray = [@[[arr mutableCopy]] mutableCopy]; + [self.collectionView reloadData]; +} + +#pragma mark - BMLongPressDragCellCollectionViewDataSource + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return self.dataSourceArray.count; } + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.dataSourceArray[section].count; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + KBMyKeyboardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBMyKeyboardCellId forIndexPath:indexPath]; + NSDictionary *d = self.dataSourceArray[indexPath.section][indexPath.item]; + [cell configEmoji:d[@"emoji"] title:d[@"title"]]; + return cell; +} + +// 拖拽库要求实现:返回当前“二维数组”数据源 +- (NSArray *> *)dataSourceWithDragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView { + return self.dataSourceArray; +} + +// 拖拽后回调:保存最新数据 +- (void)dragCellCollectionView:(BMLongPressDragCellCollectionView *)dragCellCollectionView newDataArrayAfterMove:(nullable NSArray *> *)newDataArray { + self.dataSourceArray = [newDataArray mutableCopy]; +} + +#pragma mark - BMLongPressDragCellCollectionViewDelegate (布局) + +// 根据文案长度动态返回 item 尺寸 +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + NSDictionary *d = self.dataSourceArray[indexPath.section][indexPath.item]; + return [KBMyKeyboardCell sizeForEmoji:d[@"emoji"] title:d[@"title"]]; +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { + return UIEdgeInsetsMake(12, 12, 12, 12); +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 10; } +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return 10; } + +#pragma mark - Actions + +- (void)onSave { + // 这里只做示意:保存当前顺序 + NSLog(@"保存顺序: %@", self.dataSourceArray); + [KBHUD showInfo:@"已保存"]; +} + +#pragma mark - Lazy UI + +//- (UILabel *)titleLabel { +// if (!_titleLabel) { +// _titleLabel = [UILabel new]; +// _titleLabel.text = @"My Keyboard"; // 顶部标题 +// _titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; +// _titleLabel.textColor = [UIColor colorWithHex:0x1B1F1A]; +// } +// return _titleLabel; +//} + +- (UIView *)sheetView { + if (!_sheetView) { + _sheetView = [UIView new]; + _sheetView.backgroundColor = [UIColor whiteColor]; + _sheetView.layer.cornerRadius = 32.0; + _sheetView.layer.masksToBounds = YES; + } + return _sheetView; +} + +- (BMLongPressDragCellCollectionView *)collectionView { + if (!_collectionView) { + UICollectionViewLeftAlignedLayout *layout = [UICollectionViewLeftAlignedLayout new]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; +// layout.sectionInset = UIEdgeInsetsMake(16, 16, 16, 16); + _collectionView = [[BMLongPressDragCellCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + _collectionView.backgroundColor = [UIColor clearColor]; + _collectionView.delegate = self; // 注意:代理为 BMLongPressDragCellCollectionViewDelegate + _collectionView.dataSource = self; // 注意:数据源为 BMLongPressDragCellCollectionViewDataSource + _collectionView.alwaysBounceVertical = YES; + _collectionView.showsVerticalScrollIndicator = NO; + [_collectionView registerClass:KBMyKeyboardCell.class forCellWithReuseIdentifier:kKBMyKeyboardCellId]; + } + return _collectionView; +} + +- (UIButton *)saveButton { + if (!_saveButton) { + _saveButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_saveButton setTitle:@"Save" forState:UIControlStateNormal]; + _saveButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; + [_saveButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + _saveButton.backgroundColor = [UIColor colorWithHex:KBColorValue]; + _saveButton.layer.cornerRadius = 25; + _saveButton.layer.masksToBounds = YES; + [_saveButton addTarget:self action:@selector(onSave) forControlEvents:UIControlEventTouchUpInside]; + } + return _saveButton; +} + +@end diff --git a/keyBoard/Class/Me/VC/MyVC.m b/keyBoard/Class/Me/VC/MyVC.m index c044a34..af641a5 100644 --- a/keyBoard/Class/Me/VC/MyVC.m +++ b/keyBoard/Class/Me/VC/MyVC.m @@ -10,6 +10,7 @@ #import "KBSkinDetailVC.h" #import "KBMyHeaderView.h" // 顶部视图独立封装 #import "KBMyListCell.h" +#import "KBMyKeyBoardVC.h" @interface MyVC () @property (nonatomic, strong) BaseTableView *tableView; // 列表 @@ -100,6 +101,8 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; + KBMyKeyBoardVC *vc = [[KBMyKeyBoardVC alloc] init]; + [self.navigationController pushViewController:vc animated:true]; } #pragma mark - Lazy diff --git a/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionView.h b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionView.h new file mode 100644 index 0000000..55a766b --- /dev/null +++ b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionView.h @@ -0,0 +1,71 @@ +// MIT License +// +// Copyright (c) 2019 https://liangdahong.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//////////////////// +/// v3.1.4 +//////////////////// + +#import +#import "BMLongPressDragCellCollectionViewDelegate.h" +#import "BMLongPressDragCellCollectionViewDataSource.h" + +/* + - (NSArray *> *)dataSourceWithDragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView { + return self.dataSourceArray; + // 此协议方法里返回数据源 + } + + - (void)dragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView newDataArrayAfterMove:(nullable NSArray *> *)newDataArray { + self.dataSourceArray = [newDataArray mutableCopy]; + // 此协议方法里保存数据源 + } + */ +@interface BMLongPressDragCellCollectionView : UICollectionView + +@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) id dataSource; + +/// 长按触发拖拽所需时间,默认是 0.5 秒。 +@property (nonatomic, assign) NSTimeInterval minimumPressDuration; + +/// 是否可以拖拽 默认为 YES。 +@property (nonatomic, assign, getter=isCanDrag) IBInspectable BOOL canDrag; + +/// 长按拖拽时拖拽中的的 Cell 缩放比例,默认是 1.2 倍。 +@property (nonatomic, assign) IBInspectable CGFloat dragZoomScale; + +/// 拖拽的 Cell 在拖拽移动时的透明度 默认是: 1.0 不透明。 +@property (nonatomic, assign) IBInspectable CGFloat dragCellAlpha; + +/// 拖拽到 UICollectionView 边缘时 UICollectionView 的滚动速度 +/// 默认为: 8.0f ,建议设置 5.0f 到 15.0f 之间。 +@property (nonatomic, assign) IBInspectable CGFloat dragSpeed; + +/// 拖拽 View 的背景颜色,默认和被拖拽的 Cell 一样。 +@property (nonatomic, strong) IBInspectable UIColor *dragSnapedViewBackgroundColor; + +/// 移动到指定位置 +/// @param indexPath 移动到的位置 +/// 内部只会处理当前正在拖拽的情况,会把拖拽的 Cell 移动到指定位置,建议在停止手势时或者认为适当的时候使用。 +- (void)dragMoveItemToIndexPath:(NSIndexPath *)indexPath; + +@end diff --git a/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionView.m b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionView.m new file mode 100644 index 0000000..dbf6471 --- /dev/null +++ b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionView.m @@ -0,0 +1,729 @@ +// MIT License +// +// Copyright (c) 2019 https://liangdahong.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "BMLongPressDragCellCollectionView.h" + +typedef NS_ENUM(NSUInteger, BMLongPressDragCellCollectionViewScrollDirection) { + BMLongPressDragCellCollectionViewScrollDirectionNone = 0, + BMLongPressDragCellCollectionViewScrollDirectionLeft = 1, + BMLongPressDragCellCollectionViewScrollDirectionRight = 2, + BMLongPressDragCellCollectionViewScrollDirectionUp = 3, + BMLongPressDragCellCollectionViewScrollDirectionDown = 4, +}; + +@interface BMLongPressDragCellCollectionView () + +/// 长按手势 +@property (nonatomic, strong, nullable) UILongPressGestureRecognizer *longGesture; +/// 截图快照 +@property (nonatomic, strong, nullable) UIView *snapedView; +/// 定时器 +@property (nonatomic, strong, nullable) CADisplayLink *edgeTimer; +/// 旧的IndexPath +@property (nonatomic, strong, nullable) NSIndexPath *oldIndexPath; +/// 当前路径 +@property (nonatomic, strong, nullable) NSIndexPath *currentIndexPath; + +@property (nonatomic, assign) BOOL isEndDrag; ///< 是否已经停止拖动 +@property (nonatomic, assign) BOOL banReload; ///< 禁止刷新 +@property (nonatomic, assign) CGPoint oldPoint; ///< 旧的位置 +@property (nonatomic, assign) CGPoint lastPoint; ///< 最后的触摸点 + +@end + +@implementation BMLongPressDragCellCollectionView + +@dynamic delegate, dataSource; + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { + if (self = [super initWithFrame:frame collectionViewLayout:layout]) { + [self initConfiguration]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self initConfiguration]; + } + return self; +} + +- (void)dealloc { + [self _stopEdgeTimer]; +} + +#pragma mark - getters setters + +- (void)setCanDrag:(BOOL)canDrag { + _canDrag = canDrag; + self.longGesture.enabled = _canDrag; +} + +- (void)setMinimumPressDuration:(NSTimeInterval)minimumPressDuration { + _minimumPressDuration = minimumPressDuration; + self.longGesture.minimumPressDuration = minimumPressDuration; +} + +- (void)setDragZoomScale:(CGFloat)dragZoomScale { + if (dragZoomScale < 0.1) { + dragZoomScale = 1.2; + } + _dragZoomScale = dragZoomScale; +} + +- (void)setDragSpeed:(CGFloat)dragSpeed { + if (dragSpeed < 0.5) { + dragSpeed = 8.0; + } + _dragSpeed = dragSpeed; +} + +- (void)setDragSnapedViewBackgroundColor:(UIColor *)dragSnapedViewBackgroundColor { + _dragSnapedViewBackgroundColor = dragSnapedViewBackgroundColor; + _snapedView.backgroundColor = dragSnapedViewBackgroundColor; +} + +- (UILongPressGestureRecognizer *)longGesture { + if (!_longGesture) { + _longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlelongGesture:)]; + _longGesture.minimumPressDuration = _minimumPressDuration; + } + return _longGesture; +} + +// iOS 10 新特性 对UICollectionView做了优化,但是这里如果使用了会导致bug,重写此方法是为了让其无法修改 +// https://developer.apple.com/documentation/uikit/uicollectionview/1771771-prefetchingenabled +// https://sxgfxm.github.io/blog/2016/10/18/uicollectionview-ios10-new-features +- (void)setPrefetchingEnabled:(BOOL)prefetchingEnabled { + [super setPrefetchingEnabled:NO]; +} + +#pragma mark - 私有方法 + +- (void)initConfiguration { + _canDrag = YES; + _minimumPressDuration = 0.5; + _dragZoomScale = 1.2; + _dragCellAlpha = 1.0; + _dragSpeed = 8.0; + [self addGestureRecognizer:self.longGesture]; + // iOS 10 新特性 对 UICollectionView 做了优化,但是这里如果使用了会导致bug + // https://developer.apple.com/documentation/uikit/uicollectionview/1771771-prefetchingenabled + // https://sxgfxm.github.io/blog/2016/10/18/uicollectionview-ios10-new-features + if (@available(iOS 10.0, *)) { + self.prefetchingEnabled = NO; + } +} + +- (void)reloadData { + if (_banReload) { + return; + } + [super reloadData]; +} + +- (void)insertSections:(NSIndexSet *)sections { + if (_banReload) { + return; + } + [super insertSections:sections]; +} + +- (void)deleteSections:(NSIndexSet *)sections { + if (_banReload) { + return; + } + [super deleteSections:sections]; +} + +- (void)reloadSections:(NSIndexSet *)sections { + if (_banReload) { + return; + } + [super reloadSections:sections]; +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { + if (_banReload) { + return; + } + [super moveSection:section toSection:newSection]; +} + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { + if (_banReload) { + return; + } + [super insertItemsAtIndexPaths:indexPaths]; +} + +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { + if (_banReload) { + return; + } + [super deleteItemsAtIndexPaths:indexPaths]; +} + +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths { + if (_banReload) { + return; + } + [super reloadItemsAtIndexPaths:indexPaths]; +} + +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { + if (_banReload) { + return; + } + [super moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; +} + +- (__kindof UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath { + UICollectionViewCell *cell = [super dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; + // 处理复用时的问题,为了保证显示的正确性 + if (_isEndDrag) { + cell.hidden = NO; + } else { + cell.hidden = (self.oldIndexPath + && self.oldIndexPath.item == indexPath.item + && self.oldIndexPath.section == indexPath.section); + } + return cell; +} + +- (nullable NSIndexPath *)_getChangedNullIndexPath { + __block NSIndexPath *index = nil; + CGPoint point = [self.longGesture locationInView:self]; + NSInteger number = self.numberOfSections; + while (number--) { + NSInteger row = [self.dataSource collectionView:self numberOfItemsInSection:number]; + if (row == 0) { + // 如果这组是空的 + CGRect headerFrame = [self layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:number]].frame; + CGRect footerFrame = [self layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:number]].frame; + CGRect frame = CGRectZero; + if (((UICollectionViewFlowLayout *)self.collectionViewLayout).scrollDirection == UICollectionViewScrollDirectionHorizontal) { + // 水平方向 + frame = CGRectMake(CGRectGetMinX(headerFrame), + CGRectGetMinY(headerFrame), + CGRectGetMaxX(footerFrame) - CGRectGetMidX(headerFrame), + CGRectGetWidth(footerFrame)); + + if (frame.size.width < 10.0) { + // 如果这组的宽度小于 10.0,就设置一个默认值 10.0,主要是为了判断触摸点缩放在这组内。 + frame.size.width = 10.0; + frame.size.height = CGRectGetHeight(self.frame); + frame.origin.x -= 5.0; + } + } else { + // 垂直方向 + frame = CGRectMake(CGRectGetMinX(headerFrame), + CGRectGetMinY(headerFrame), + CGRectGetWidth(footerFrame), + CGRectGetMaxY(footerFrame) - CGRectGetMidY(headerFrame)); + + if (frame.size.height < 10.0) { + // 如果这组的高度小于 10.0,就设置一个默认值 10.0,主要是为了判断触摸点缩放在这组内。 + frame.size.height = 10.0; + frame.size.width = CGRectGetWidth(self.frame); + frame.origin.y -= 5.0; + } + } + + if (CGRectContainsPoint(frame, point)) { + // 触摸点在空的组内 + return [NSIndexPath indexPathForItem:0 inSection:number]; + } + } + } + return index; +} + +- (nullable NSIndexPath *)_getChangedIndexPath { + __block NSIndexPath *index = nil; + CGPoint point = [self.longGesture locationInView:self]; + // 遍历拖拽的 Cell 的中心点在哪一个 Cell 里 + [[self visibleCells] enumerateObjectsUsingBlock:^(__kindof UICollectionViewCell * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (CGRectContainsPoint(obj.frame, point)) { + index = [self indexPathForCell:obj]; + *stop = YES; + } + }]; + // 找到而且不是当前的 Cell 就返回此 index + if (index) { + if ((index.section == self.oldIndexPath.section) && (index.item == self.oldIndexPath.item)) { + return nil; + } + return index; + } + + // 获取最应该交换的 Cell + __block CGFloat width = MAXFLOAT; + __weak typeof(self) weakSelf = self; + [[self visibleCells] enumerateObjectsUsingBlock:^(__kindof UICollectionViewCell * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (CGRectContainsPoint(obj.frame, point)) { + index = [self indexPathForCell:obj]; + *stop = YES; + } + __strong typeof(weakSelf) self = weakSelf; + CGPoint p1 = self.snapedView.center; + CGPoint p2 = obj.center; + // 计算距离 + CGFloat distance = sqrt(pow((p1.x - p2.x), 2) + pow((p1.y - p2.y), 2)); + if (distance < width) { + width = distance; + index = [self indexPathForCell:obj]; + } + }]; + if (!index) { + return nil; + } + if ((index.section == self.oldIndexPath.section) && (index.item == self.oldIndexPath.item)) { + // 最近的就是隐藏的 Cell 时, return nil + return nil; + } + return index; +} + +// 处理临界点问题 +- (BMLongPressDragCellCollectionViewScrollDirection)_setScrollDirection { + if (((self.bounds.size.height + self.contentOffset.y - _snapedView.center.y) < (_snapedView.bounds.size.height / 2)) + && ((self.bounds.size.height + self.contentOffset.y) < self.contentSize.height)) { + return BMLongPressDragCellCollectionViewScrollDirectionDown; + + } else if (((_snapedView.center.y - self.contentOffset.y) < (_snapedView.bounds.size.height / 2)) + && self.contentOffset.y > 0) { + return BMLongPressDragCellCollectionViewScrollDirectionUp; + + } else if (((self.bounds.size.width + self.contentOffset.x) - (_snapedView.center.x)) < (_snapedView.bounds.size.width / 2) + && (self.bounds.size.width + self.contentOffset.x) < self.contentSize.width) { + return BMLongPressDragCellCollectionViewScrollDirectionRight; + + } else if (((_snapedView.center.x - self.contentOffset.x) < (_snapedView.bounds.size.width) / 2) + && self.contentOffset.x > 0) { + return BMLongPressDragCellCollectionViewScrollDirectionLeft; + } + + return BMLongPressDragCellCollectionViewScrollDirectionNone; +} + +// 处理 UICollectionView 数据源 +- (void)_updateSourceData { + // 获取数据源 + NSMutableArray *array = [self.dataSource dataSourceWithDragCellCollectionView:self].mutableCopy; + // 处理数据 + BOOL dataTypeCheck = ([self numberOfSections] != 1 || ([self numberOfSections] == 1 && [array[0] isKindOfClass:NSArray.class])); + if (dataTypeCheck) { + for (int i = 0; i < array.count; i ++) { + // 如果是嵌套数组 就把数组里面的数组换为 NSMutableArray。 + [array replaceObjectAtIndex:i withObject:[array[i] mutableCopy]]; + } + } + if (_currentIndexPath.section == _oldIndexPath.section) { + + NSMutableArray *orignalSection = dataTypeCheck ? array[_oldIndexPath.section] : array; + + if (_currentIndexPath.item > _oldIndexPath.item) { + for (NSUInteger i = _oldIndexPath.item; i < _currentIndexPath.item ; i ++) { + [orignalSection exchangeObjectAtIndex:i withObjectAtIndex:i + 1]; + } + + } else { + for (NSUInteger i = _oldIndexPath.item; i > _currentIndexPath.item ; i --) { + [orignalSection exchangeObjectAtIndex:i withObjectAtIndex:i - 1]; + } + } + + } else { + NSMutableArray *orignalSection = array[_oldIndexPath.section]; + NSMutableArray *currentSection = array[_currentIndexPath.section]; + [currentSection insertObject:orignalSection[_oldIndexPath.item] atIndex:_currentIndexPath.item]; + // https://github.com/liangdahong/BMLongPressDragCellCollectionView/issues/16 + // 这里之前删除指定对象会有问题,如果数据源中的对象为 [字符串 而且是一样的时候, 因为 OC 中对字符串的内存做了特殊处理 ],会把相同的字符串全部删除导致崩溃 + // [orignalSection removeObject:orignalSection[_oldIndexPath.item]]; + [orignalSection removeObjectAtIndex:_oldIndexPath.item]; + } + // ==========处理数据 + // 更新外面的数据源 + // 这里会触发外面的使用者修改数据源,但外面的使用者可以会在此调用 reloadData 相关方法,所以做了拦截操作 + // https://github.com/liangdahong/BMLongPressDragCellCollectionView/issues/14 + self.banReload = YES; + [self.delegate dragCellCollectionView:self newDataArrayAfterMove:array]; + self.banReload = NO; +} + +- (void)_setEdgeTimer{ + if (!_edgeTimer) { + _edgeTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(_edgeScroll)]; + [_edgeTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } +} + +- (void)_stopEdgeTimer{ + if (_edgeTimer) { + [_edgeTimer invalidate]; + _edgeTimer = nil; + } +} + +- (void)_edgeScroll { + BMLongPressDragCellCollectionViewScrollDirection scrollDirection = [self _setScrollDirection]; + + if (BMLongPressDragCellCollectionViewScrollDirectionNone == scrollDirection) { + return; + } + + switch (scrollDirection) { + case BMLongPressDragCellCollectionViewScrollDirectionLeft:{ + // 这里的动画必须设为 NO + [self setContentOffset:CGPointMake(self.contentOffset.x - _dragSpeed, self.contentOffset.y) animated:NO]; + _snapedView.center = CGPointMake(_snapedView.center.x - _dragSpeed, _snapedView.center.y); + _lastPoint.x -= _dragSpeed; + } + break; + case BMLongPressDragCellCollectionViewScrollDirectionRight:{ + [self setContentOffset:CGPointMake(self.contentOffset.x + _dragSpeed, self.contentOffset.y) animated:NO]; + _snapedView.center = CGPointMake(_snapedView.center.x + _dragSpeed, _snapedView.center.y); + _lastPoint.x += _dragSpeed; + } + break; + case BMLongPressDragCellCollectionViewScrollDirectionUp:{ + [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentOffset.y - _dragSpeed) animated:NO]; + _snapedView.center = CGPointMake(_snapedView.center.x, _snapedView.center.y - _dragSpeed); + _lastPoint.y -= _dragSpeed; + } + break; + case BMLongPressDragCellCollectionViewScrollDirectionDown:{ + [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentOffset.y + _dragSpeed) animated:NO]; + _snapedView.center = CGPointMake(_snapedView.center.x, _snapedView.center.y + _dragSpeed); + _lastPoint.y += _dragSpeed; + } + break; + default: + break; + } + + // 如果Cell 拖拽到了边沿时 + // 截图视图位置移动 + [UIView animateWithDuration:0.1 animations:^{ + self.snapedView.center = self.lastPoint; + }]; + + // 获取应该交换的 Cell 的位置 + NSIndexPath *index1 = [self _getChangedNullIndexPath]; + NSIndexPath *index = nil; + + index = index1 ? : [self _getChangedIndexPath]; + + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionView:changedDragAtPoint:)]) { + [self.delegate dragCellCollectionView:self changedDragAtPoint:_lastPoint]; + } + + if (!index) { + return; + } + + // 是否可以交换 + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionViewShouldBeginExchange:sourceIndexPath:toIndexPath:)]) { + if (![self.delegate dragCellCollectionViewShouldBeginExchange:self sourceIndexPath:_oldIndexPath toIndexPath:index]) { + return; + } + } + _currentIndexPath = index; + + UICollectionViewCell *cell1 = [self cellForItemAtIndexPath:_currentIndexPath]; + + NSIndexPath *sourceIndexPath = _oldIndexPath; + NSIndexPath *toIndexPath = _currentIndexPath; + if (!index1) { + self.oldPoint = cell1.center; + // 更新数据源 + [self _updateSourceData]; + // 移动 cell + [self moveItemAtIndexPath:_oldIndexPath toIndexPath:_currentIndexPath]; + // 设置移动后的起始 indexPath + _oldIndexPath = _currentIndexPath; + // 为了防止在缓存池取出的 Cell 已隐藏, + // 以后可以优化 + [self reloadItemsAtIndexPaths:@[_oldIndexPath]]; + + } else { + // 更新数据源 + [self _updateSourceData]; + // 移动 cell + [self moveItemAtIndexPath:_oldIndexPath toIndexPath:_currentIndexPath]; + self.oldPoint = [self cellForItemAtIndexPath:_currentIndexPath].center; + // 设置移动后的起始 indexPath + _oldIndexPath = _currentIndexPath; + [self reloadItemsAtIndexPaths:@[_oldIndexPath]]; + } + + // 交换结束时 + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionViewShouldEndExchange:sourceIndexPath:toIndexPath:)]) { + [self.delegate dragCellCollectionViewShouldEndExchange:self sourceIndexPath:sourceIndexPath toIndexPath:toIndexPath]; + } +} + +#pragma mark - 事件响应 + +- (void)handlelongGesture:(UILongPressGestureRecognizer *)longGesture { + CGPoint point = [longGesture locationInView:self]; + NSIndexPath *indexPath = [self indexPathForItemAtPoint:point]; + switch (longGesture.state) { + case UIGestureRecognizerStateBegan: { + // 手势开始 + self.userInteractionEnabled = NO; + _oldIndexPath = indexPath; + // 判断手势落点位置是否在 Cell 上, 没有按在 cell 上就 break + if (_oldIndexPath == nil) { + self.longGesture.enabled = NO; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (self.canDrag) { + self.longGesture.enabled = YES; + } + }); + break; + } + + // 将要开始拖拽时,询问此位置的Cell是否可以拖拽 + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionViewShouldBeginMove:indexPath:)]) { + if (![self.delegate dragCellCollectionViewShouldBeginMove:self indexPath:_oldIndexPath]) { + _oldIndexPath = nil; + self.longGesture.enabled = NO; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (self.canDrag) { + self.longGesture.enabled = YES; + } + }); + break; + } + } + self.isEndDrag = NO; + // 取出正在长按的cell + UICollectionViewCell *cell = [self cellForItemAtIndexPath:_oldIndexPath]; + self.oldPoint = cell.center; + // 先置 nil + if (_snapedView) { + _snapedView = nil; + } + // 是否外部提供拖拽View + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionView: startDragAtIndexPath:)]) { + _snapedView = [self.delegate dragCellCollectionView:self startDragAtIndexPath:indexPath]; + } + if (!_snapedView) { + // 使用系统截图功能,得到 cell 的快照 view + _snapedView = [cell snapshotViewAfterScreenUpdates:NO]; + } + + // 修改颜色 + if (_dragSnapedViewBackgroundColor) { + _snapedView.backgroundColor = _dragSnapedViewBackgroundColor; + } + + // 让外面做一些特殊处理 + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionView: dragView:indexPath:)]) { + [self.delegate dragCellCollectionView:self dragView:_snapedView indexPath:indexPath]; + } + + // 设置frame + _snapedView.frame = cell.frame; + // 添加到 collectionView 不然无法显示 + [self addSubview:_snapedView]; + // 截图后隐藏当前 cell + cell.hidden = YES; + + // 获取当前触摸的中心点 + CGPoint currentPoint = point; + + // 动画放大和移动到触摸点下面 + [UIView animateWithDuration:0.25 animations:^{ + self.snapedView.transform = CGAffineTransformMakeScale(self.dragZoomScale, self.dragZoomScale); + self.snapedView.center = CGPointMake(currentPoint.x, currentPoint.y); + self.snapedView.alpha = self.dragCellAlpha; + }]; + } + break; + case UIGestureRecognizerStateChanged: { + // 开启 collectionView 的边缘自动滚动检测 + // https://github.com/liangdahong/BMLongPressDragCellCollectionView/issues/15 + if (!_edgeTimer) { + [self _setEdgeTimer]; + } + + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionView:changedDragAtPoint:)]) { + [self.delegate dragCellCollectionView:self changedDragAtPoint:point]; + } + // 当前手指位置 + _lastPoint = point; + // 截图视图位置移动 + [UIView animateWithDuration:0.1 animations:^{ + self.snapedView.center = self.lastPoint; + }]; + + NSIndexPath *index1 = [self _getChangedNullIndexPath]; + NSIndexPath *index = nil; + + if (index1) { + index = index1; + } else { + index = [self _getChangedIndexPath]; + } + + // 没有取到 || 距离隐藏的 Cell 最近时就返回 + if (!index) { + break; + } + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionViewShouldBeginExchange:sourceIndexPath:toIndexPath:)]) { + if (![self.delegate dragCellCollectionViewShouldBeginExchange:self sourceIndexPath:_oldIndexPath toIndexPath:index]) { + break; + } + } + + _currentIndexPath = index; + UICollectionViewCell *cell1 = [self cellForItemAtIndexPath:_currentIndexPath]; + + NSIndexPath *sourceIndexPath = _oldIndexPath; + NSIndexPath *toIndexPath = _currentIndexPath; + if (!index1) { + self.oldPoint = cell1.center; + // 更新数据源 + [self _updateSourceData]; + // 移动 cell + [self moveItemAtIndexPath:_oldIndexPath toIndexPath:_currentIndexPath]; + // 设置移动后的起始 indexPath + _oldIndexPath = _currentIndexPath; + [self reloadItemsAtIndexPaths:@[_oldIndexPath]]; + + } else { + // 更新数据源 + [self _updateSourceData]; + // 移动 cell + [self moveItemAtIndexPath:_oldIndexPath toIndexPath:_currentIndexPath]; + self.oldPoint = [self cellForItemAtIndexPath:_currentIndexPath].center; + // 设置移动后的起始 indexPath + _oldIndexPath = _currentIndexPath; + [self reloadItemsAtIndexPaths:@[_oldIndexPath]]; + } + + // 交换结束时 + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionViewShouldEndExchange:sourceIndexPath:toIndexPath:)]) { + [self.delegate dragCellCollectionViewShouldEndExchange:self sourceIndexPath:sourceIndexPath toIndexPath:toIndexPath]; + } + + break; + } + break; + + default: { + self.userInteractionEnabled = YES; + if (self.delegate && [self.delegate respondsToSelector:@selector(dragCellCollectionView:endedDragAtPoint:)]) { + [self.delegate dragCellCollectionView:self endedDragAtPoint:point]; + } + + if (longGesture.isEnabled) { + + if (!self.oldIndexPath) { + return; + } + UICollectionViewCell *cell = [self cellForItemAtIndexPath:_oldIndexPath]; + // 结束动画过程中停止交互,防止出问题 + self.userInteractionEnabled = NO; + // 结束拖拽了 + self.isEndDrag = YES; + // 给截图视图一个动画移动到隐藏 cell 的新位置 + [UIView animateWithDuration:0.25 animations:^{ + if (!cell) { + self.snapedView.center = self.oldPoint; + } else { + self.snapedView.center = cell.center; + } + self.snapedView.transform = CGAffineTransformMakeScale(1.0f, 1.0f); + self.snapedView.alpha = 1.0; + } completion:^(BOOL finished) { + // 移除截图视图、显示隐藏的 cell 并开启交互 + [self.snapedView removeFromSuperview]; + self.snapedView = nil; + cell.hidden = NO; + self.userInteractionEnabled = YES; + }]; + } + // 关闭定时器 + self.oldIndexPath = nil; + [self _stopEdgeTimer]; + } + break; + } +} + +- (void)dragMoveItemToIndexPath:(NSIndexPath *)newIndexPath { + if (self.isEndDrag) { + return; + } + self.isEndDrag = YES; + self.longGesture.enabled = NO; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (self.canDrag) { + self.longGesture.enabled = YES; + } + }); + + if (_oldIndexPath) { + _currentIndexPath = newIndexPath; + // 更新数据源 + [self _updateSourceData]; + // 移动 cell + [self moveItemAtIndexPath:_oldIndexPath toIndexPath:_currentIndexPath]; + // 设置移动后的起始 indexPath + _oldIndexPath = newIndexPath; + [self reloadItemsAtIndexPaths:@[newIndexPath]]; + + UICollectionViewCell *cell = [self cellForItemAtIndexPath:_oldIndexPath]; + cell.hidden = YES; + + // 结束动画过程中停止交互,防止出问题 + self.userInteractionEnabled = NO; + // 结束拖拽了 + self.isEndDrag = YES; + // 给截图视图一个动画移动到隐藏 cell 的新位置 + [UIView animateWithDuration:0.25 animations:^{ + if (!cell) { + self.snapedView.center = self.oldPoint; + } else { + self.snapedView.center = cell.center; + } + self.snapedView.transform = CGAffineTransformMakeScale(1.0f, 1.0f); + self.snapedView.alpha = 1.0; + } completion:^(BOOL finished) { + // 移除截图视图、显示隐藏的 cell 并开启交互 + [self.snapedView removeFromSuperview]; + cell.hidden = NO; + self.userInteractionEnabled = YES; + }]; + } + + // 关闭定时器 + _oldIndexPath = nil; + [self _stopEdgeTimer]; +} + +@end diff --git a/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionViewDataSource.h b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionViewDataSource.h new file mode 100644 index 0000000..dfb5676 --- /dev/null +++ b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionViewDataSource.h @@ -0,0 +1,47 @@ +// MIT License +// +// Copyright (c) 2019 https://liangdahong.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class BMLongPressDragCellCollectionView; + +@protocol BMLongPressDragCellCollectionViewDataSource + +@required + +/** + 获取数据源,内部会在合适的时候获取最新的数据源,必须实现 + + @param dragCellCollectionView dragCellCollectionView + @return 最新的数据源 + + - (nullable NSArray *)dataSourceWithDragCellCollectionView:(BMLongPressDragCellCollectionView *)dragCellCollectionView { + return self.dataArray; + } + */ +- (nullable NSArray *> *)dataSourceWithDragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionViewDelegate.h b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionViewDelegate.h new file mode 100644 index 0000000..cd3789b --- /dev/null +++ b/keyBoard/Class/Vender/BMLongPressDragCellCollectionView/BMLongPressDragCellCollectionViewDelegate.h @@ -0,0 +1,98 @@ +// MIT License +// +// Copyright (c) 2019 https://liangdahong.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class BMLongPressDragCellCollectionView; + +@protocol BMLongPressDragCellCollectionViewDelegate + +@required + +/** + Cell 有交换时调用 + + @param dragCellCollectionView dragCellCollectionView + @param newDataArray 最新的数据源,需要使用者保存,在拖拽的时候 Cell 已经移动,需保存最新的数据源 + + - (void)dragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView newDataArrayAfterMove:(nullable NSArray *> *)newDataArray { + self.dataArray = [newDataArray copy]; + } + */ +- (void)dragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView newDataArrayAfterMove:(nullable NSArray *> *)newDataArray; + +@optional + +/** + 将要开始拖拽时,询问此位置的 Cell 是否能拖拽 + + @param dragCellCollectionView dragCellCollectionView + @param indexPath indexPath + @return YES: 正常拖拽和移动 NO:此 Cell 不可拖拽,如:增加按钮等。 + */ +- (BOOL)dragCellCollectionViewShouldBeginMove:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView indexPath:(NSIndexPath *)indexPath; + +/** + 将要开始拖拽时,向外面获取需要拖拽的 View,如果不实现就内部自动处理。 + + @param dragCellCollectionView dragCellCollectionView + @param indexPath indexPath + @return dragView + */ +- (UIView *)dragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView startDragAtIndexPath:(NSIndexPath *)indexPath; + +/// 开始拖拽时,让外面的使用者可以对拖拽的 View 做额外操作 +/// @param dragCellCollectionView dragCellCollectionView +/// @param dragView dragView +/// @param indexPath indexPath +- (void)dragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView dragView:(UIView *)dragView indexPath:(NSIndexPath *)indexPath; + +/// 正在拖拽时 +/// @param dragCellCollectionView dragCellCollectionView +/// @param point 手指触摸点对于 collectionView 的位置 +/// 可以参考 https://github.com/liangdahong/ToutiaoDemo/blob/master/ToutiaoDemo/Classes/Edit/Controller/BMChannelEditVC.m 的使用 +- (void)dragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView changedDragAtPoint:(CGPoint)point; + +/// cell 将要交换时,询问是否能交换 +/// @param dragCellCollectionView dragCellCollectionView +/// @param sourceIndexPath 原来的 IndexPath +/// @param destinationIndexPath 将要交换的 IndexPath +/// YES: 正常拖拽和移动 NO:此Cell不可拖拽,如:增加按钮等。 +- (BOOL)dragCellCollectionViewShouldBeginExchange:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView sourceIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; + +/// 结束交换时 +/// @param dragCellCollectionView dragCellCollectionView +/// @param sourceIndexPath 原来的 IndexPath +/// @param destinationIndexPath 被交换的 IndexPath +- (void)dragCellCollectionViewShouldEndExchange:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView sourceIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; + +/// 结束拖拽时 +/// @param dragCellCollectionView dragCellCollectionView +/// @param point 手指触摸点对于 collectionView 的位置 +/// 可以参考 https://github.com/liangdahong/ToutiaoDemo/blob/master/ToutiaoDemo/Classes/Edit/Controller/BMChannelEditVC.m 的使用 +- (void)dragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView endedDragAtPoint:(CGPoint)point; + +@end + +NS_ASSUME_NONNULL_END