【项目Github地址】
先展示一张效果图:
从图片上可以看出都是弹簧效果,自然而然就会想到用系统的UIScrollView
类来实现。
接下来我将仔细的一步步的教你完成这个项目。
首先咱们先创建一个工程,打开ViewController.m
文件,我们先简单创建一个UIScrollView,让他的contentSize为两倍的屏幕宽度,再加入两张图片。
代码如下:
#import "ViewController.h"
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
#define kScreenHeight [UIScreen mainScreen].bounds.size.height
#define kLeftRightMargin 8
#define kBottomMargin 16
#define kImageViewWidth (kScreenWidth - kLeftRightMargin*2)
#define kImageViewHeight (kImageViewWidth*392/344.5)
#define kImageViewY (kScreenHeight - kBottomMargin - kImageViewHeight)
@interface ViewController () <UIScrollViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIImageView *leftView;
@property (nonatomic, strong) UIImageView *rightView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.delegate = self;
self.scrollView.contentSize = CGSizeMake(kScreenWidth*2, kScreenHeight);
self.scrollView.pagingEnabled = YES;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.showsVerticalScrollIndicator = NO;
[self.view addSubview:self.scrollView];
self.leftView = [[UIImageView alloc] initWithFrame:CGRectMake(kLeftRightMargin, kImageViewY, kImageViewWidth, kImageViewHeight)];
self.leftView.backgroundColor = [UIColor yellowColor];
self.leftView.layer.cornerRadius = 15;
self.leftView.layer.masksToBounds = YES;
[self.scrollView addSubview:self.leftView];
self.rightView = [[UIImageView alloc] initWithFrame:CGRectMake(kLeftRightMargin+kScreenWidth, kImageViewY, kImageViewWidth, kImageViewHeight)];
self.rightView.backgroundColor = [UIColor greenColor];
self.rightView.layer.cornerRadius = 15;
self.rightView.layer.masksToBounds = YES;
[self.scrollView addSubview:self.rightView];
}
@end
运行之后,你将看到如下图的效果:
但是你们也会发现有如下的反应,向上向下并没有弹性效果:
因为此时scrollView
的高度和它contentSize
的高度是相同的,所以它在横向上是可以滚动的,而纵向上就不可以了。但是我们想要纵向也有弹性效果,肿么办?如果你熟悉UIScrollView
,你就晓得有一个属性可以让它简单实现了。
咱们在创建scrollView
的地方加入一行代码:
...
self.scrollView.alwaysBounceVertical = YES; // <--
[self.view addSubview:self.scrollView];
效果图如下:
但此时可能你会发现另一个问题,此时的
scrollView
滚动的方向没有限制:
那咱们就去
UIScrollView
头文件里去瞧瞧,说不定苹果爸爸已经给我们预设了解决办法了也说不定。很快你就会看到有这样一个属性定义
@property(nonatomic,getter=isDirectionalLockEnabled) BOOL directionalLockEnabled; // default NO. if YES, try to lock vertical or horizontal scrolling while dragging
那就用起来:
self.scrollView.alwaysBounceVertical = YES;
self.scrollView.directionalLockEnabled = YES; // <--
[self.view addSubview:self.scrollView];
突然很神奇般的,好像是有效了。至少我在iOS10.3的模拟器上是没问题,但是如果你在真机上运行的话,试试你就会发现,当你滑动角度在45°左右的时候,前面设置的方向锁属性就不生效没用了。
苹果官方也承认这个是个BUG了。在stackoverflow上也有在讨论这个问题。
我在上面选择了一个解决方式,实现UIScrollView
的delegate
,代码如下:
@interface ViewController () <UIScrollViewDelegate>
...
@property (nonatomic, assign) CGPoint beginDragPoint;
@end
@implementation ViewController
...
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
_beginDragPoint = scrollView.contentOffset;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.x == _beginDragPoint.x) {
self.scrollView.contentOffset = CGPointMake(_beginDragPoint.x, (self.scrollView.contentOffset.y));
} else {
self.scrollView.contentOffset = CGPointMake((self.scrollView.contentOffset.x), _beginDragPoint.y);
}
}
@end
接下来说说另一个问题,手势向下移动过程中,手指移动多少视图就移动多少,而我们写的工程却还是弹性的。
我的思路就是,那么就让scrollView
的contentSize
高度大于本身的高度吧,那么也能手移动多少,他就移动多少了。那高度设为多少合适呢?你可以先自己尝试看看,我最终的结果是两倍的本身高度,感觉非常巧妙,最后视图消失与否都通过pagingEnabled
已经搞定了。
修改代码如下:
// #define kImageViewY (kScreenHeight - kBottomMargin - kImageViewHeight)
#define kImageViewY (kScreenHeight*2 - kBottomMargin - kImageViewHeight)
self.scrollView.delegate = self;
// self.scrollView.contentSize = CGSizeMake(kScreenWidth*2, kScreenHeight);
self.scrollView.contentSize = CGSizeMake(kScreenWidth*2, kScreenHeight*2); // <--
self.scrollView.contentOffset = CGPointMake(0, kScreenHeight); // <--
self.scrollView.pagingEnabled = YES;
效果图如下:
好了,基本大功告成了。但是还有一个致命的问题,一般人找不到解决办法,我也是不断尝试才发现原来还可以这么整。
问题就是,当手指在非视图区域移动时,视图是不会动不会有反应的,一旦移动到靠近视图的时候,视图才开始移动的。如图:
先分析一下,UIScrollView
的滚动时因为内部封装的pan手势,那我们可不可以拿到手势调用的方法,重写它,然后判断如果手指没有到达位置时,手势的UIGestureRecognizerStateChanged
事件就不传递给父类,父类就不能处理,那么视图就不会移动了。
我们先来找找事件方法:
打断点发现,手势属性里有个
SEL
方法handlePan:
,嗯,应该就是它了,创建一个UIScrollView
的子类重写这个方法吧。
// 创建类
@interface LYScrollView : UIScrollView
@end
@implementation LYScrollView
- (void)handlePan:(UIPanGestureRecognizer *)pan {
NSLog(@"%s", __func__);
}
@end
.
.
#import "LYScrollView.h"
// 替换创建方法
// @property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) LYScrollView *scrollView;
...
// self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView = [[LYScrollView alloc] initWithFrame:self.view.bounds];
很是令人欣慰啊,方法回调了。
可是问题又出现了,当满足条件的时候,我要把事件还给父类的,可是编译器通过不了:
(个人想的比较挫的解决方法啊,有好的方式麻烦告知下哈。)
再创建一个中间类,声明一个
handlePan:
方法。。。
@interface MyScrollView : UIScrollView
- (void)handlePan:(UIPanGestureRecognizer *)pan;
@end
@interface LYScrollView : MyScrollView
@end
// 去除`.m`文件因为没有实现该方法而有的警告问题
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation MyScrollView
#pragma clang diagnostic pop
@end
最终方法实现如下:
@implementation LYScrollView {
BOOL _canMove;
}
- (void)handlePan:(UIPanGestureRecognizer *)pan {
CGPoint point = [pan locationInView:pan.view];
if (pan.state == UIGestureRecognizerStateBegan) {
[pan setTranslation:CGPointZero inView:pan.view];
[super handlePan:pan];
} else if (pan.state == UIGestureRecognizerStateChanged) {
// 一旦手指位置到达视图时,则开始移动
if (!_canMove && point.y > kImageViewY) {
_canMove = YES;
[pan setTranslation:CGPointZero inView:pan.view];
}
if (_canMove) {
[super handlePan:pan];
}
} else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled) {
_canMove = NO;
[pan setTranslation:CGPointZero inView:pan.view];
[super handlePan:pan];
}
}
@end
这边需要[pan setTranslation:CGPointZero inView:pan.view];
调用一下,将手势的位置初始化为零后,再传给父类,不然就位置突变了。
大问题都解决完啦,剩下的背景色渐变、出现消失、再封装等实现就不在这里赘述了,可去看下工程代码。