备注:博文只贴出关键代码,其中涉及到自定义控件、数据model可以忽略。博友使用过程中可以直接用button等替换掉报错的代码即可。
************************************ h 文件 ************************************
#import <UIKit/UIKit.h>
#import "DragButton.h"
/**
* 非编辑状态,点击顶部标签回调到上一页数据
*
* @param model 点击标签model
*/
typedef void(^DragButtonTapBlock) (DragButtonModel *model);
/**
* 标签的增、减、坐标变换等,回调新数组到上一页
*
* @param newArray 新数组
*/
typedef void(^DragButtonSortBlock) (NSMutableArray *newArray);
@interface DragButtonSortView : UIScrollView
<UIScrollViewDelegate,
UIGestureRecognizerDelegate> {
NSMutableArray *_topDataArray ,*_bottomDataArray; // 顶部 + 底部数据源
NSMutableArray *_topButtonArray ,*_bottomButtonArray; // 顶部 + 底部存放button数组
BOOL _dragDown; // 向前(上)、向后(下)拖动
CGFloat _criticalValue; // 临界值---顶部button区域的最后一个button底部Y坐标
CGFloat _scrollY; // scrollView 滚动的位置,支持自动滚动到顶部
NSInteger _touchOldIndex; // 与哪个button交汇,button所在数组的索引
CGPoint startPoint, originPoint; // 拖动相关变量
CGPoint _dragStart,_dragCurrent; // 长按begin的point + 持续拖动的point
CGFloat minY, maxY; // 是不是在水平方向拖动(最小Y + 最大Y)
}
@property (nonatomic, copy) DragButtonTapBlock tapBlock; // 点击
@property (nonatomic, copy) DragButtonSortBlock dragBlock; // 拖动顺序变化
@property (nonatomic, strong) UILabel *topLabel;
@property (nonatomic, strong) UIButton *editeButton;
@property (nonatomic, strong) UILabel *bottomLabel;
@property (nonatomic, strong) UIButton *allButton;
/**
* 初始化
*
* @param frame frame
* @param dragArray 可排序的数组
* @param normalArray 底部推荐区域的数组
* @param tBlock 可排序的自定义view正常状态下点击回调
* @param dBlock 可排序的数组的任何变动回调
*
* @return 对象
*/
- (instancetype)initWithFrame:(CGRect)frame
dragArray:(NSArray *)dragArray
normalArray:(NSArray *)normalArray
tapButton:(DragButtonTapBlock)tBlock
updateDragArray:(DragButtonSortBlock)dBlock;
@end
************************************ m 文件 ************************************
#import "DragButtonSortView.h"
static NSInteger const TopButtonTag = 10000;
static NSInteger const BottomButtonTag = 20000;
static NSInteger const ButtonNumber = 3; // 一行 3 个控件
@implementation DragButtonSortView
- (instancetype)initWithFrame:(CGRect)frame
dragArray:(NSArray *)dragArray
normalArray:(NSArray *)normalArray
tapButton:(DragButtonTapBlock)tBlock
updateDragArray:(DragButtonSortBlock)dBlock {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
self.delegate = self;
_tapBlock = tBlock;
_dragBlock = dBlock;
_enterBlock = eBlock;
_topDataArray = [[NSMutableArray alloc] init];
_bottomDataArray = [[NSMutableArray alloc] init];
_topButtonArray = [[NSMutableArray alloc] init];
_bottomButtonArray = [[NSMutableArray alloc] init];
if (dragArray) {
[_topDataArray addObjectsFromArray:dragArray];
}
if (normalArray) {
[_bottomDataArray addObjectsFromArray:normalArray];
}
[self addSubview:self.topLabel];
[self addSubview:self.editeButton];
[self addSubview:self.bottomLabel];
[self addSubview:self.allButton];
[self loadTopDragButton:dragArray];
[self loadBottomNormalButton:normalArray];
self.contentOffset = CGPointMake(0, 0);
}
return self;
}
#pragma mark - 懒加载
- (UILabel *)topLabel {
if (!_topLabel) {
_topLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 0, SCREENWIDTH, 30)];
_topLabel.backgroundColor = [UIColor clearColor];
_topLabel.textColor = [UIColor grayColor];
_topLabel.textAlignment = NSTextAlignmentLeft;
_topLabel.font = [UIFont systemFontOfSize:15.0];
_topLabel.text = @"进入";
}
return _topLabel;
}
- (UIButton *)editeButton {
if (!_editeButton) {
_editeButton = [UIButton buttonWithType:UIButtonTypeSystem];
_editeButton.frame = CGRectMake(10, 0, SCREENWIDTH - 20, 30);
_editeButton.backgroundColor = [UIColor clearColor];
_editeButton.titleLabel.font = [UIFont systemFontOfSize:15.f];
_editeButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
[_editeButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[_editeButton setTitle:@"编辑" forState:UIControlStateNormal];
[_editeButton setTitle:@"完成" forState:UIControlStateSelected];
[_editeButton addTarget:self action:@selector(editeButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _editeButton;
}
- (UILabel *)bottomLabel {
if (!_bottomLabel) {
_bottomLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_bottomLabel.backgroundColor = [UIColor clearColor];
_bottomLabel.textColor = [UIColor grayColor];
_bottomLabel.textAlignment = NSTextAlignmentLeft;
_bottomLabel.font = [UIFont systemFontOfSize:15.0];
_bottomLabel.text = @" ";
}
return _bottomLabel;
}
- (UIButton *)allButton {
if (!_allButton) {
_allButton = [UIButton buttonWithType:UIButtonTypeSystem];
_allButton.frame = CGRectZero;
_allButton.backgroundColor = [UIColor clearColor];
_allButton.titleLabel.font = [UIFont systemFontOfSize:15.f];
_allButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
[_allButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[_allButton setTitle:@"更多>" forState:UIControlStateNormal];
[_allButton addTarget:self action:@selector(allButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _allButton;
}
- (void)loadTopDragButton:(NSArray *)array {
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
for (NSInteger i = 0; i < array.count; i++) {
DragButtonModel *model = array[i];
CGRect rect = [self getRectAtIndex:i topLocation:topY];
DragButton *button = [[DragButton alloc] initWithFrame:rect deleteImageViewHiden:YES dataSource:model];
button.tag = TopButtonTag + i;
button.exclusiveTouch = YES;
button.backgroundColor = RGBA(239, 239, 239, 1.0);
button.titleLabel.font = [UIFont systemFontOfSize:15.f];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button setTitle:model.name forState:UIControlStateNormal];
[button addTarget:self action:@selector(topButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
[self editeButtonGesture:button];
[_topButtonArray addObject:button];
if (i == array.count - 1) {
_criticalValue = CGRectGetMaxY(button.frame) + 20;
}
}
[self updateBottomLabelLocation];
}
- (void)loadBottomNormalButton:(NSArray *)array {
CGFloat tempHeight = 0;
CGFloat topY = CGRectGetMaxY(self.bottomLabel.frame);
for (NSInteger i = 0; i < array.count; i++) {
DragButtonModel *model = array[i];
CGRect rect = [self getRectAtIndex:i topLocation:topY];
DragButton *button = [[DragButton alloc] initWithFrame:rect deleteImageViewHiden:YES dataSource:model];
button.tag = BottomButtonTag + i;
button.frame = rect;
button.exclusiveTouch = YES;
button.backgroundColor = RGBA(239, 239, 239, 1.0);
button.titleLabel.font = [UIFont systemFontOfSize:15.f];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button setTitle:model.name forState:UIControlStateNormal];
[button addTarget:self action:@selector(bottomButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
[_bottomButtonArray addObject:button];
tempHeight = CGRectGetMaxY(button.frame) + 10;
}
CGFloat totalHeight = (tempHeight > SCREENHEIGHT) ? tempHeight : SCREENHEIGHT + 10;
self.contentSize = CGSizeMake(self.frame.size.width, totalHeight);
}
#pragma mark - 基础功能
- (void)editeButtonClick:(UIButton *)button {
self.editeButton.selected = !self.editeButton.selected;
[self updateTopLabelTitle];
BOOL hidenDeleteButton = (self.editeButton.selected) ? NO : YES;
[self updateTopButtonDrag:hidenDeleteButton];
}
- (void)allButtonClick:(UIButton *)button {
if (self.enterBlock) {
self.enterBlock();
}
}
/**
* 顶部所有button的点击方法
* 1.如果当前为编辑(拖动)状态,并且顶部所有button个数大于4(至少保留4个,反之提示),点击后从_topDataArray + _topButtonArray移除相应数据源 + 当前点击的button,
并把当前点击的button + 对应数据源插入_bottomButtonArray + _bottomDataArray
* 2.数据处理完毕,执行移动到下面button区域动画。动画结束button移除之前绑定的事件,添加下部button的点击事件,移除绑定的长按事件,并回调新的顺序
* 3.如果当前为正常状态(不能拖动),点击回调当前button对应的数据源
*
*/
- (void)topButtonClick:(DragButton *)button {
if (_editeButton.selected) { // 编辑状态,可以拖动,此时点击要移动到底部button区域
if (_topButtonArray.count > 4) { // 至少保留 4 个
NSInteger index = [_topButtonArray indexOfObject:button];
BOOL lastOne = (index == _topButtonArray.count - 1) ? YES : NO;
DragButtonModel *model = _topDataArray[index];
[_topDataArray removeObject:model];
[_bottomDataArray insertObject:model atIndex:0];
[_topButtonArray removeObject:button];
[_bottomButtonArray insertObject:button atIndex:0];
[UIView animateWithDuration:0.3 animations:^{
[self removeFromTopIsLastOne:lastOne currentIndex:index];
} completion:^(BOOL finished) {
[button removeTarget:self action:@selector(topButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[button addTarget:self action:@selector(bottomButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[button deleteImageViewHiden:YES];
[self editeButtonGesture:button];
if (self.dragBlock) {
self.dragBlock(_topDataArray);
}
}];
} else {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"至少保留 4 个标签"
message:nil
delegate:nil
cancelButtonTitle:@"返回"
otherButtonTitles: nil];
[alert show];
}
} else { // 数据回调
if (self.tapBlock) {
self.tapBlock(button.dataSourceModel);
}
}
}
/**
* 底部所有button的点击方法
* 1.移除当前点击button + 对应数据源,添加到顶部 _topButtonArray + _topDataArray 中
* 2.执行移动到顶部的动画,如果此时顶部 _topButtonArray 中最后一个button独自占一行,那么要相应调整底部lable的位置 + 底部_bottomButtonArray 要重新排序,
* 3.动画结束,移除底部button当前绑定的点击事件,添加顶部button的点击事件.此时如果是编辑(拖动)状态,那么当前点击的这个button就要显示删除按钮,反之不显示。
并且要绑定长按事件,回调新的顺序
*
*/
- (void)bottomButtonClick:(DragButton *)button {
NSInteger index = [_bottomButtonArray indexOfObject:button];
DragButtonModel *model = _bottomDataArray[index];
[_topDataArray addObject:model];
[_bottomDataArray removeObject:model];
[_topButtonArray addObject:button];
[_bottomButtonArray removeObject:button];
NSInteger percent = _topButtonArray.count % ButtonNumber;
BOOL update = (percent == 1) ? YES : NO;
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
[UIView animateWithDuration:0.3 animations:^{
button.frame = [self getRectAtIndex:_topButtonArray.count - 1 topLocation:topY];
[self moveToTopFirst:update];
} completion:^(BOOL finished) {
[button removeTarget:self action:@selector(bottomButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[button addTarget:self action:@selector(topButtonClick:) forControlEvents:UIControlEventTouchUpInside];
if (self.editeButton.selected) {
[button deleteImageViewHiden:NO];
}
[self editeButtonGesture:button];
if (self.dragBlock) {
self.dragBlock(_topDataArray);
}
}];
}
- (CGRect)getRectAtIndex:(NSInteger)index topLocation:(NSInteger)top {
CGFloat topY = top; // 起始高度
CGFloat border = 10; // 控件间隙
CGFloat viewWidth = (SCREENWIDTH - border * 4) / ButtonNumber; // 控件宽度
CGFloat viewHeight = 40; // 控件高度
CGFloat x = (viewWidth + border) * (index % ButtonNumber) + border;
CGFloat y = (viewHeight + border) * (index / ButtonNumber) + border + topY;
return CGRectMake(x, y, viewWidth, viewHeight);
}
// 移除 + 绑定长按事件
- (void)editeButtonGesture:(DragButton *)button {
NSArray *gestures = button.gestureRecognizers;
if (gestures.count > 0) {
UILongPressGestureRecognizer *longGesture = gestures.firstObject;
longGesture.delegate = nil;
[button removeGestureRecognizer:longGesture];
} else {
UILongPressGestureRecognizer *longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
[button addGestureRecognizer:longGesture];
longGesture.minimumPressDuration = 0.2;
longGesture.delegate = self;
}
}
- (void)updateTopLabelTitle {
self.topLabel.text = self.editeButton.selected ? @"拖动排序" : @"进入";
}
// 顶部 _topButtonArray 追加button(底部点击移动到顶部)
- (void)moveToTopFirst:(BOOL)firstOne {
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
NSInteger index = _topButtonArray.count - 1;
DragButton *btn = _topButtonArray[index];
btn.frame = [self getRectAtIndex:index topLocation:topY];
if (firstOne) { // 点击下面button(topButtonArray.count % 3 == 1,每一行的第一个)
[self updateBottomLabelLocation];
}
[self updateBottomButtonLocation];
}
// 点击顶部button执行的动画:最后一个不需要重新排序,其他都要重新排序
- (void)removeFromTopIsLastOne:(BOOL)lastOne currentIndex:(NSInteger)index {
if (lastOne) { // 点击上面button( 如果是最后一个_topButtonArray无需重新排序)
} else { // 如果不是最后一个 _topButtonArray 后续所有需要重新排序
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
for (NSInteger i = index; i < _topButtonArray.count; i++) {
DragButton *btn = _topButtonArray[i];
btn.frame = [self getRectAtIndex:i topLocation:topY];
}
}
// 如果topButtonArray.count % 3 == 0(每一行的第三个button),那么要相应变动 底部label.frame 及 _bottomButtonArray 中的button.frame
NSInteger percent = _topButtonArray.count % ButtonNumber;
if (percent == 0) {
[self updateBottomLabelLocation];
}
[self updateBottomButtonLocation];
}
- (void)updateTopButtonDrag:(BOOL)drag {
for (NSInteger i = 0; i < _topButtonArray.count; i++) {
DragButton *button = _topButtonArray[i];
[button deleteImageViewHiden:drag];
}
}
- (void)updateBottomLabelLocation {
if (_topButtonArray.count > 0) {
DragButton *btn = _topButtonArray[_topButtonArray.count - 1];
_criticalValue = CGRectGetMaxY(btn.frame) + 20;
self.bottomLabel.frame = CGRectMake(10, _criticalValue,SCREENWIDTH - 20, 30);
self.allButton.frame = CGRectMake(10, _criticalValue,SCREENWIDTH - 20, 30);
}
}
- (void)updateBottomButtonLocation {
CGFloat bottomY = CGRectGetMaxY(self.bottomLabel.frame);
for (NSInteger i = 0; i < _bottomButtonArray.count; i++) {
DragButton *btn = _bottomButtonArray[i];
btn.frame = [self getRectAtIndex:i topLocation:bottomY];
}
}
#pragma mark - UILongPressGestureRecognizer
/**
* 长按拖动详解 (核心代码)
* UIGestureRecognizerStateBegan : 当前button执行放大+改变透明度的动画,并从 _topButtonArray + _topDataArray 移除,记录其最大、小Y坐标
* UIGestureRecognizerStateChanged : button跟随手势移动
* 1.当前拖动Y坐标 < scrollview滑动的位移,此时scrollview滚动到顶部---看效果可知
* 2.当前拖动button与其他button交汇。
A情况 --- 水平移动,与同一行的button交汇,根据 X 坐标判断滑动方向,并执行动画
B情况 --- 垂直(上下)移动,与其他button交汇,根据 Y 坐标判断滑动方向,并执行动画,及时更新当前行的最大 + 小 Y 坐标
* UIGestureRecognizerStateEnded :当前拖动button还原frame并透明度改为1,插入到指定位置并插入对应数据源,回调最新顺序
*
*/
- (void)longPress:(UILongPressGestureRecognizer *)gesture {
DragButton *button = (DragButton *)gesture.view;
DragButtonModel *model = button.dataSourceModel;
BOOL contains = NO;
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
{
// 长按开始拖动
if (!self.editeButton.selected) {
[self updateTopButtonDrag:NO];
self.editeButton.selected = !self.editeButton.selected;
[self updateTopLabelTitle];
}
[self bringSubviewToFront:button];
_touchOldIndex = [_topButtonArray indexOfObject:button];
startPoint = [gesture locationInView:gesture.view];
originPoint = button.center;
_dragDown = NO;
[UIView animateWithDuration:0.3 animations:^{
button.transform = CGAffineTransformMakeScale(1.2, 1.2);
button.alpha = 0.7;
}];
[_topButtonArray removeObject:button];
[_topDataArray removeObject:model];
_dragStart = [gesture locationInView:gesture.view];
minY = CGRectGetMinY(button.frame);
maxY = CGRectGetMaxY(button.frame);
}
break;
case UIGestureRecognizerStateChanged:
{
// button伴随移动
CGPoint currentPoint = [gesture locationInView:gesture.view];
CGFloat deltaX = currentPoint.x - startPoint.x;
CGFloat deltaY = currentPoint.y - startPoint.y;
button.center = CGPointMake(button.center.x + deltaX, button.center.y + deltaY);
// scrollview移动到顶部
if (button.center.y < _scrollY && button.center.y > 0) {
[self setContentOffset:CGPointMake(0, 0) animated:YES];
_scrollY = 0;
}
_dragCurrent = button.center;
// 执行动画逻辑
NSInteger index = [self locationChange:button.center moveButton:button];
if (index < 0) {
contains = NO;
_dragDown = NO;
} else {
contains = YES;
DragButton *insertBtn = _topButtonArray[index];
originPoint = insertBtn.center;
if (_dragCurrent.y < maxY && _dragCurrent.y > minY) {
NSLog(@"cccccc");
if (_dragCurrent.x < _dragStart.x) {
_dragDown = NO;
[UIView animateWithDuration:0.3 animations:^{
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
for (NSInteger i = index; i < _topButtonArray.count; i++) {
DragButton *btn = _topButtonArray[i];
btn.frame = [self getRectAtIndex:i + 1 topLocation:topY];
}
}];
} else {
_dragDown = YES;
[UIView animateWithDuration:0.3 animations:^{
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
for (NSInteger i = _touchOldIndex; i < index + 1; i++) {
if (_topDataArray.count > i) {
DragButton *btn = _topButtonArray[i];
btn.frame = [self getRectAtIndex:i topLocation:topY];
}
}
}];
}
} else {
if (_dragCurrent.y < _dragStart.y) { // || _dragCurrent.x > _dragStart.x
minY = CGRectGetMinY(insertBtn.frame);
maxY = CGRectGetMaxY(insertBtn.frame);
_dragDown = NO;
NSLog(@"aaaaa");
[UIView animateWithDuration:0.3 animations:^{
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
for (NSInteger i = index; i < _topButtonArray.count; i++) {
DragButton *btn = _topButtonArray[i];
btn.frame = [self getRectAtIndex:i + 1 topLocation:topY];
}
}];
} else {
minY = CGRectGetMinY(insertBtn.frame);
maxY = CGRectGetMaxY(insertBtn.frame);
NSLog(@"bbbbbb");
_dragDown = YES;
[UIView animateWithDuration:0.3 animations:^{
CGFloat topY = CGRectGetMaxY(self.topLabel.frame);
for (NSInteger i = _touchOldIndex; i < index + 1; i++) {
if (_topDataArray.count > i) {
DragButton *btn = _topButtonArray[i];
btn.frame = [self getRectAtIndex:i topLocation:topY];
}
}
}];
}
}
_touchOldIndex = index;
}
_dragStart = button.center;
}
break;
case UIGestureRecognizerStateEnded:
{
_dragStart = CGPointZero;
[UIView animateWithDuration:0.3 animations:^{
button.transform = CGAffineTransformIdentity;
button.alpha = 1.0;
if (!contains) {
button.center = originPoint;
}
} completion:^(BOOL finished) {
if (!contains) {
NSInteger insertIndex = _dragDown ? _touchOldIndex + 1 : _touchOldIndex;
[_topButtonArray insertObject:button atIndex:insertIndex];
[_topDataArray insertObject:model atIndex:insertIndex];
if (self.dragBlock) {
self.dragBlock(_topDataArray);
}
}
}];
}
break;
default:
break;
}
}
// 拖动point是否在button内
- (NSInteger)locationChange:(CGPoint)point moveButton:(DragButton *)btn {
NSInteger index = -1;
for (NSInteger i = 0; i < _topButtonArray.count; i++) {
DragButton *currentButton = _topButtonArray[i];
if (currentButton != btn) {
if (CGRectContainsPoint(currentButton.frame, point)) {
index = i;
}
}
}
return index;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
return YES;
}
#pragma mark - UIScrollView
// scrollview的位移
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
_scrollY = scrollView.contentOffset.y;
}
@end