【項目Github地址】
先展示一張效果圖:

從圖片上可以看出都是彈簧效果,自然而然就會想到用系統(tǒng)的UIScrollView類來實現(xiàn)。
接下來我將仔細的一步步的教你完成這個項目。
首先咱們先創(chuàng)建一個工程,打開ViewController.m文件,我們先簡單創(chuàng)建一個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
運行之后,你將看到如下圖的效果:

但是你們也會發(fā)現(xiàn)有如下的反應,向上向下并沒有彈性效果:

因為此時scrollView的高度和它contentSize的高度是相同的,所以它在橫向上是可以滾動的,而縱向上就不可以了。但是我們想要縱向也有彈性效果,腫么辦?如果你熟悉UIScrollView,你就曉得有一個屬性可以讓它簡單實現(xiàn)了。
咱們在創(chuàng)建scrollView的地方加入一行代碼:
...
self.scrollView.alwaysBounceVertical = YES; // <--
[self.view addSubview:self.scrollView];
效果圖如下:

但此時可能你會發(fā)現(xiàn)另一個問題,此時的
scrollView滾動的方向沒有限制:

那咱們就去
UIScrollView頭文件里去瞧瞧,說不定蘋果爸爸已經(jīng)給我們預設了解決辦法了也說不定。很快你就會看到有這樣一個屬性定義
@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的模擬器上是沒問題,但是如果你在真機上運行的話,試試你就會發(fā)現(xiàn),當你滑動角度在45°左右的時候,前面設置的方向鎖屬性就不生效沒用了。

蘋果官方也承認這個是個BUG了。在stackoverflow上也有在討論這個問題。
我在上面選擇了一個解決方式,實現(xiàn)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高度大于本身的高度吧,那么也能手移動多少,他就移動多少了。那高度設為多少合適呢?你可以先自己嘗試看看,我最終的結(jié)果是兩倍的本身高度,感覺非常巧妙,最后視圖消失與否都通過pagingEnabled已經(jīng)搞定了。
修改代碼如下:
// #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;
效果圖如下:

好了,基本大功告成了。但是還有一個致命的問題,一般人找不到解決辦法,我也是不斷嘗試才發(fā)現(xiàn)原來還可以這么整。
問題就是,當手指在非視圖區(qū)域移動時,視圖是不會動不會有反應的,一旦移動到靠近視圖的時候,視圖才開始移動的。如圖:

先分析一下,UIScrollView的滾動時因為內(nèi)部封裝的pan手勢,那我們可不可以拿到手勢調(diào)用的方法,重寫它,然后判斷如果手指沒有到達位置時,手勢的UIGestureRecognizerStateChanged事件就不傳遞給父類,父類就不能處理,那么視圖就不會移動了。
我們先來找找事件方法:

打斷點發(fā)現(xiàn),手勢屬性里有個
SEL方法handlePan:,嗯,應該就是它了,創(chuàng)建一個UIScrollView的子類重寫這個方法吧。
// 創(chuàng)建類
@interface LYScrollView : UIScrollView
@end
@implementation LYScrollView
- (void)handlePan:(UIPanGestureRecognizer *)pan {
NSLog(@"%s", __func__);
}
@end
.
.
#import "LYScrollView.h"
// 替換創(chuàng)建方法
// @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];
很是令人欣慰啊,方法回調(diào)了。

可是問題又出現(xiàn)了,當滿足條件的時候,我要把事件還給父類的,可是編譯器通過不了:

(個人想的比較挫的解決方法啊,有好的方式麻煩告知下哈。)
再創(chuàng)建一個中間類,聲明一個
handlePan:方法。。。
@interface MyScrollView : UIScrollView
- (void)handlePan:(UIPanGestureRecognizer *)pan;
@end
@interface LYScrollView : MyScrollView
@end
// 去除`.m`文件因為沒有實現(xiàn)該方法而有的警告問題
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation MyScrollView
#pragma clang diagnostic pop
@end
最終方法實現(xiàn)如下:
@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];調(diào)用一下,將手勢的位置初始化為零后,再傳給父類,不然就位置突變了。

大問題都解決完啦,剩下的背景色漸變、出現(xiàn)消失、再封裝等實現(xiàn)就不在這里贅述了,可去看下工程代碼。