iOS-UIScrollView原理

一. UIScrollView属性、方法和代理

1. UIScrollView属性、方法

自定义系统的UIScrollView之前我们要先解释UIScrollView的属性、方法,代理方法,因为UIScrollView的有些属性还是比较难理解的,直接拷贝系统UIScrollView.h文件一个一个解释,如下:

contentOffset,contentSize,contentInset

@property(nonatomic)         CGPoint                      contentOffset;                  // default CGPointZero
@property(nonatomic)         CGSize                       contentSize;                    // default CGSizeZero
@property(nonatomic)         UIEdgeInsets                 contentInset;                   // default UIEdgeInsetsZero. add additional scroll area around content

关于这三个属性,网上有太多的解释,可参考:https://www.jianshu.com/p/e5582fe5dd4a

下面补充一下,iOS7之后出现了下面这个属性,这个属性是UIViewController的,如下:

automaticallyAdjustsScrollViewInsets

@property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets API_DEPRECATED("Use UIScrollView's contentInsetAdjustmentBehavior instead", ios(7.0,11.0),tvos(7.0,11.0)); // Defaults to YES

这个属性的字面意思是自动调节ScrollView的Insets,默认YES,比如我们像如下创建scrollView的时候:

scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
  1. 因为automaticallyAdjustsScrollViewInsets默认是YES,系统帮我们调节了ScrollVIew的Insets,所以我们的scrollView是从导航栏下面开始的,显示正常。
  2. 如果我们把automaticallyAdjustsScrollViewInsets设置为NO,我们就要手动设置ScrollVIew的Insets,代码如下:
//其中88是导航栏的高度
scrollView.contentInset = UIEdgeInsetsMake(88.f, 0.f, 0.f, 0.f);
scrollView.contentOffset = CGPointMake(0.f, -88.f);//这句如果不写,不会自动滚动到指定位置
  1. 如果我们把automaticallyAdjustsScrollViewInsets设置为NO,又没手动设置ScrollVIew的Insets,这时候就需要调节scrollView的frame的y值了。

这个属性在iOS11已废弃,使用UIScrollView的如下属性代替,和上面属性一样的,默认自动调节,可以设置为Never

contentInsetAdjustmentBehavior

/* Configure the behavior of adjustedContentInset.
 Default is UIScrollViewContentInsetAdjustmentAutomatic.
 */
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));

下面再介绍一个属性

safeAreaInsets

@property (nonatomic,readonly) UIEdgeInsets safeAreaInsets API_AVAILABLE(ios(11.0),tvos(11.0));

iOS11之后,由于刘海屏幕的出现有了这个属性,它是安全区域之外的额外inserts,比如iPhone X竖屏时占满整个屏幕的控制器的view的safeAreaInsets是(44,0,34,0) ,44和34分别是刘海和手势操作的区域。

说了这么多介绍下面这个属性就很简单了

adjustedContentInset

/* When contentInsetAdjustmentBehavior allows, UIScrollView may incorporate
 its safeAreaInsets into the adjustedContentInset.
 */
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0),tvos(11.0));

正如注释所写:

  1. 当设置UIScrollViewContentInsetAdjustmentAutomatic时:
    adjustedContentInset = safecontentInset + scrollView.contentInset
  2. 当设置UIScrollViewContentInsetAdjustmentNever时:
    adjustedContentInset = scrollView.contentInset

adjustedContentInsetDidChange方法

/* Also see -scrollViewDidChangeAdjustedContentInset: in the UIScrollViewDelegate protocol.
 */
- (void)adjustedContentInsetDidChange API_AVAILABLE(ios(11.0),tvos(11.0)) NS_REQUIRES_SUPER;

adjustedContentInset属性改变会调用,和代理里面scrollViewDidChangeAdjustedContentInset方法一样的。

contentLayoutGuide和frameLayoutGuide

iOS11新增,用于描述内容布局和整体布局信息

@property(nonatomic,readonly,strong) UILayoutGuide *contentLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));
@property(nonatomic,readonly,strong) UILayoutGuide *frameLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));

属性

更多属性的解释可见注释,如下:

//代理
@property(nullable,nonatomic,weak) id<UIScrollViewDelegate>        delegate;                       // default nil. weak reference

//默认为FALSE, 如果设置为TRUE,那么在推拖拽UIScrollView的时候,会锁住水平或竖直方向的滑动
@property(nonatomic,getter=isDirectionalLockEnabled) BOOL directionalLockEnabled;         // default NO. if YES, try to lock vertical or horizontal scrolling while dragging

//是否有回弹效果
@property(nonatomic)         BOOL                         bounces;                        // default YES. if YES, bounces past edge of content and back again

//alwaysBounceVertical 竖直方向总是可以弹性滑动,默认是NO, 当设置为TRUE(前提是属性bounces必须为TRUE)的时候,即使contentSize设置的width 和 height都比UIScrollView的width 和 height小,在垂直方向上都可以有滑动效果,甚至即使我们不设置contentSize都可以产生滑动效果; 反之,如果设置alwaysBounceVertical为FALSE, 那么当contentSize设置的width 和 height都比UIScrollView的width 和 height小的时候,即使bounces设置为TRUE,那么不可能产生弹性效果
@property(nonatomic)         BOOL                         alwaysBounceVertical;           // default NO. if YES and bounces is YES, even if content is smaller than bounds, allow drag vertically

//同上
@property(nonatomic)         BOOL                         alwaysBounceHorizontal;         // default NO. if YES and bounces is YES, even if content is smaller than bounds, allow drag horizontally

//是否可分页,默认是FALSE, 如果设置成TRUE, 则可分页
@property(nonatomic,getter=isPagingEnabled) BOOL          pagingEnabled __TVOS_PROHIBITED;// default NO. if YES, stop on multiples of view bounds

//是否可以滚动
@property(nonatomic,getter=isScrollEnabled) BOOL          scrollEnabled;                  // default YES. turn off any dragging temporarily

//是否展示水平方向滚动条
@property(nonatomic)         BOOL                         showsHorizontalScrollIndicator; // default YES. show indicator while we are tracking. fades out after tracking

//是否展示垂直方向滚动条
@property(nonatomic)         BOOL                         showsVerticalScrollIndicator;   // default YES. show indicator while we are tracking. fades out after tracking

//滑动条的边缘插入,即是距离上、左、下、右的距离
//例如:testScrollView.scrollIndicatorInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) 
//当向下滑动时,滑动条距离顶部的距离总是20
@property(nonatomic)         UIEdgeInsets                 scrollIndicatorInsets;          // default is UIEdgeInsetsZero. adjust indicators inside of insets

//滚动条样式,是个枚举类型:
@property(nonatomic)         UIScrollViewIndicatorStyle   indicatorStyle;                 // default is UIScrollViewIndicatorStyleDefault

//减速率,CGFloat类型,当你滑动松开手指后的减速速率, 但是尽管decelerationRate是一个CGFloat类型,但是目前系统只支持以下两种速率设置选择:
UIScrollViewDecelerationRateNormal    值是 0.998
UIScrollViewDecelerationRateFast      值是 0.99
@property(nonatomic)         UIScrollViewDecelerationRate decelerationRate NS_AVAILABLE_IOS(3_0);

//索引展示模式,是个枚举 : 自动显示或隐藏 || 一直隐藏
@property(nonatomic)         UIScrollViewIndexDisplayMode indexDisplayMode API_AVAILABLE(tvos(10.2));

方法

//带有动画效果的设置偏移量
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;  // animate at constant velocity to new offset

//滑动到指定的可见区域(带动画),意思就是滑动到CGRect所组成的矩形区域,使其可见. 如已可见则什么都不做
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated;         // scroll so rect is just visible (nearest edges). nothing if rect completely visible

// 设置的时候,当页面加载成功出现时,滑动条会自动显示出来,停留一下又自动隐藏
// 不设置的话,页面出现时也不会显示滑动条,只有在滑动过程中会显示滑动条
- (void)flashScrollIndicators;             // displays the scroll indicators for a short time. This should be done whenever you bring the scroll view to front.

属性

//是否一直追踪
//返回YES表示用户手指一直接触着scrollView(包括手指一直拖动scrollView)没有松开   返回NO表示手指离开scrollView 此时scrollView做自由滚动 
@property(nonatomic,readonly,getter=isTracking)     BOOL tracking;        // returns YES if user has touched. may not yet have started dragging

//是否正在拖动
@property(nonatomic,readonly,getter=isDragging)     BOOL dragging;        // returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging

//是否正在减速
@property(nonatomic,readonly,getter=isDecelerating) BOOL decelerating;    // returns YES if user isn't dragging (touch up) but scroll view is still moving

delaysContentTouches和canCancelContentTouches

@property(nonatomic) BOOL delaysContentTouches;       // default is YES. if NO, we immediately call -touchesShouldBegin:withEvent:inContentView:. this has no effect on presses
@property(nonatomic) BOOL canCancelContentTouches;    // default is YES. if NO, then once we start tracking, we don't try to drag if the touch moves. this has no effect on presses

关于这两个属性暂时只说一点,当scrollView上面添加一个UISliderView的时候,如果拖动UISliderView,scrollView会移动,当我们设置delaysContentTouches = NO,canCancelContentTouches = NO的时候,UISliderView和scrollView的拖动才互不影响。
更多详情参考:https://www.jianshu.com/p/2c74b7a6c082

touchesShouldBegin和touchesShouldCancelInContentView

//必须子类化之后重写,用以控制将触摸事件传递给scrollView的子视图
//在触摸被传递到scrollView的子视图之前调用。如果它返回NO,触摸将不会被传递到子视图
//这对press没有影响
//默认返回YES
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event inContentView:(UIView *)view;

//如果触摸已经传递到滚动视图的子视图,则在滚动开始之前调用。如果返回NO,触摸将继续传递到子视图,滚动将不会发生
//如果canCancelContentTouches为NO,则不调用。如果视图不是UIControl,默认返回YES
- (BOOL)touchesShouldCancelInContentView:(UIView *)view;

关于动画的一些属性:

minimumZoomScale和maximumZoomScale

//滑动视图的最小缩放倍数,默认是1.0
@property(nonatomic) CGFloat minimumZoomScale;     // default is 1.0

//滑动视图的最大缩放倍数,默认是1.0(要使得缩放有效果,maximumZoomScale必须要大于minimumZoomScale)
@property(nonatomic) CGFloat maximumZoomScale;     // default is 1.0. must be > minimum zoom scale to enable zooming

例如,实现如下的缩放效果:

效果图.gif

代码如下:

class ViewController: UIViewController, UIScrollViewDelegate {
    
    private var testScrollView: UIScrollView!
    private var testImgView: UIImageView!
 
    override func viewDidLoad() {
        super.viewDidLoad()
        
        testScrollView = UIScrollView(frame: CGRect(x: 50, y: 264, width: UIScreen.main.bounds.width - 100, height: 200))
        testScrollView.contentSize = CGSize(width: UIScreen.main.bounds.width - 100 , height: 200)
        testScrollView.delegate = self
        testScrollView.backgroundColor = UIColor.orange
        testScrollView.minimumZoomScale = 0.5
        testScrollView.maximumZoomScale = 2
        view.addSubview(testScrollView)
        
        testImgView = UIImageView(frame: testScrollView.bounds)
        testImgView.image = #imageLiteral(resourceName: "testimage2.jpg")
        testScrollView.addSubview(testImgView)
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return testImgView
    }
    
    func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {

        let offSetX = scrollView.bounds.width > scrollView.contentSize.width ? (scrollView.bounds.width - scrollView.contentSize.width) * 0.5 : 0.0
        let offSetY = scrollView.bounds.height > scrollView.contentSize.height ? (scrollView.bounds.height - scrollView.contentSize.height) * 0.5 : 0.0
        testImgView.center = CGPoint(x: scrollView.contentSize.width * 0.5 + offSetX, y: scrollView.contentSize.height * 0.5 + offSetY)
    }
}

注意:想要实现缩放效果,代理必须要实现func viewForZooming(in scrollView: UIScrollView) -> UIView?方法,否则无法实现缩放功能。如果要达到缩放后的一些效果操作还要实现代理的func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)方法。

zoomScale

//当前的缩放比例, 默认是1.0
@property(nonatomic) CGFloat zoomScale NS_AVAILABLE_IOS(3_0);            // default is 1.0

比如,双击当前图片,使其缩放成原来的0.8倍:

func tap() {
    // 双击当前图片,使其缩放成原来的0.8倍
    testScrollView.zoomScale = 0.8
    // 使图片居中
    let offSetX = testScrollView.bounds.width > testScrollView.contentSize.width ? (testScrollView.bounds.width - testScrollView.contentSize.width) * 0.5 : 0.0
    let offSetY = testScrollView.bounds.height > testScrollView.contentSize.height ? (testScrollView.bounds.height - testScrollView.contentSize.height) * 0.5 : 0.0
    testImgView.center = CGPoint(x: testScrollView.contentSize.width * 0.5 + offSetX, y: testScrollView.contentSize.height * 0.5 + offSetY)
}

其他动画属性

//设置缩放比例,带动画
- (void)setZoomScale:(CGFloat)scale animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);

//把从scrollView里截取的矩形区域缩放到整个scrollView当前可视的frame里面。如果截取的区域大于scrollView的frame时,图片缩小,如果截取区域小于frame,会看到图片放大。一般情况下rect需要自己计算出来。即要把用户点击坐标附近的区域内容在scrollViewl里进行缩放
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);

//弹性缩放,默认是true, 设置成false的话,当缩放到最大或最小值的时候不会有弹性效果
@property(nonatomic) BOOL  bouncesZoom;          // default is YES. if set, user can go past min/max zoom while gesturing and the zoom will animate to the min/max value at gesture end

//get属性,是否正在缩放
@property(nonatomic,readonly,getter=isZooming)       BOOL zooming;       // returns YES if user in zoom gesture

//get属性,当没有设置bouncesZoom的时候,如果正在缩放过程中则为false,如果缩放到最小值或者最大值时松开手指则为true; 当设置bouncesZoom = false的时候,如果正在缩放过程中zoomScale > 1时则为false,并且缩放到最大值时松开手指也是false。
@property(nonatomic,readonly,getter=isZoomBouncing)  BOOL zoomBouncing;  // returns YES if we are in the middle of zooming back to the min/max value

//滑动到顶部,默认是true,当点击状态栏的时候,如果当前UIScrollView不是处在顶部位置,那么可以直接回到顶部;如果已经在顶部,则没有作用;另外必须注意如果要这个属性起作用,它的delegate方法func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool不能返回false,否则没用。
@property(nonatomic) BOOL  scrollsToTop __TVOS_PROHIBITED;          // default is YES.

//平移手势,get属性,这个手势的代理系统设置的scrollView,不能修改为其他的. 可以通过设置平移手势的属性来改变平移方式,比如设置触摸手指的最少个数minimumNumberOfTouches
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);

//捏合手势,也就是缩放手势,get属性,设置同平移手势一样,当缩放禁用时返回nil。
@property(nullable, nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);


// 定向按压手势,给tvos用的
@property(nonatomic, readonly) UIGestureRecognizer *directionalPressGestureRecognizer ;

//键盘消失模式, 默认是none,是个枚举值
public enum UIScrollViewKeyboardDismissMode : Int {
    case none             //  无
    case onDrag       //  拖拽,只要滑动UIScrollView,键盘消失
    case interactive  //  交互式,拖住UIScrollView一直下滑,当接触到键盘时,键盘就跟着同步下滑
}
@property(nonatomic) UIScrollViewKeyboardDismissMode keyboardDismissMode NS_AVAILABLE_IOS(7_0); // default is UIScrollViewKeyboardDismissModeNone

//系统的刷新控件,就是一个菊花
@property (nonatomic, strong, nullable) UIRefreshControl *refreshControl NS_AVAILABLE_IOS(10_0) __TVOS_PROHIBITED;

更多关于图片缩放,请参考:iOS图片缩放

2. UIScrollView代理方法

//已经滑动,常用来处理一些偏移量,判断拖拽状态等
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;                                               // any offset changes

//已经缩放,处理一些缩放操作
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2); // any zoom scale changes

//即将开始拖拽(或许要拖拽移动一小段距离才会调用),只要一开始拖拽就会调用
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;

//即将松开手指结束拖拽
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);

//已经松开手指,结束拖拽状态
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

//即将开始减速
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;   // called on finger up as we are moving

//已经结束减速, 当UIScrollView停止时就调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;      // called when scroll view grinds to a halt

//已经结束滑动动画,这个方法起作用的前提是设置了下满面提到的两种方法中的任意一种,否则不会起作用!
// func setContentOffset(_ contentOffset: CGPoint, animated: 
// func scrollRectToVisible(_ rect: CGRect, animated: Bool)
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView; // called when setContentOffset/scrollRectVisible:animated: finishes. not called if not animating

//返回一个需要缩放的视图,需要做缩放的时候必须调用此方法,之前已经讲过,不再赘述!
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;     // return a view that will be scaled. if delegate returns nil, nothing happens

//即将开始缩放,在滑动视图开始缩放它的内容视图前调用
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view NS_AVAILABLE_IOS(3_2); // called before the scroll view begins zooming its content

//已经结束缩放状态,结束缩放手势时调用,在最小和最大值之前进行缩放
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale; // scale between minimum and maximum. called after any 'bounce' animations

//滑到顶部,这个方法和属性scrollsToTop用法一致,本质一致,返回true则是可以滑动顶部,false反之!
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;   // return a yes if you want to scroll to the top. if not defined, assumes YES

//已经滑到顶部,当滑动到顶部的动画完成的时候调用
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;      // called when scrolling animation finished. may be called immediately if already at top

//adjustedContentInset属性发生改变调用, 和[UIScrollView adjustedContentInsetDidChange]一样,当然这个代理方法是给外界用的
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView API_AVAILABLE(ios(11.0), tvos(11.0));

二. UIScrollView原理和自定义UIScrollView

1. UIScrollView原理

  1. 通过上面的属性介绍,我们发现系统的UIScrollView有一个pan手势,pan手势的代理是UIScrollView,并且不能修改pan手势的代理。
  2. 到底UIScrollView是怎么滑动的呢?
    UIScrollView继承于UIView,然后自己上面添加一个手势,UIScrollView实质就是根据手势的滑动修改它的bounds来进行view的滑动,可以在代理方法里打印ScrollView的bounds值来验证。

2. 自定义UIScrollView

系统的UIScrollView特点:

首先看一下系统的UIScrollView的一个特点,如下代码:

//灰色的scrollView添加到红色的scrollView上
//系统UIScrollView现实情况是,可以滑动到父scrollView上
UIScrollView *scrollViewOne = [[UIScrollView alloc] initWithFrame:CGRectMake(0.f, 100.f, self.view.frame.size.width, 300.f)];
scrollViewOne.backgroundColor = [UIColor redColor];
scrollViewOne.contentSize = CGSizeMake(self.view.frame.size.width*2, 300.f);

UIScrollView *scrollViewTwo = [[UIScrollView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.view.frame.size.width, 300.f)];
scrollViewTwo.contentSize = CGSizeMake(self.view.frame.size.width*2, 300.f);
scrollViewTwo.backgroundColor = [UIColor lightGrayColor];

[self.view addSubview:scrollViewOne];
[scrollViewOne addSubview:scrollViewTwo];

红色的scrollView上面添加一个灰色的scrollView,灰色的scrollView滑完之后可以滑动到红色的scrollView上。

效果图如下:
灰色的可以滑动到红色的上面.png

按照我们iOS手势手势被识别之后是不会再传给父控件了,但是这是怎么回事呢?

我们先屏蔽系统的这个特点,新建一个EOCScrollView类继承于UIScrollView,重写以下方法:
EOCScrollView.m代码

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        self.panGestureRecognizer.delegate = self;
    }
    return self;
}

#pragma mark - 重写手势代理,如果是右滑,则禁用掉mainScrollView自带的
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;

    CGFloat pointX = [pan translationInView:self].x;
    if (pointX < 0 && self.contentOffset.x == self.contentSize.width-self.frame.size.width) {
        return NO;
    }
    return YES;
}

ViewController.m里面代码如下

EOCScrollView *scrollViewOne = [[EOCScrollView alloc] initWithFrame:CGRectMake(0.f, 100.f, self.view.frame.size.width, 300.f)];
scrollViewOne.backgroundColor = [UIColor redColor];

scrollViewOne.contentSize = CGSizeMake(self.view.frame.size.width*2, 300.f);

EOCScrollView *scrollViewTwo = [[EOCScrollView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.view.frame.size.width, 300.f)];
scrollViewTwo.contentSize = CGSizeMake(self.view.frame.size.width*2, 600.f);
scrollViewTwo.backgroundColor = [UIColor lightGrayColor];

EOCScrollView *scrollViewThree = [[EOCScrollView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.view.frame.size.width, 300.f)];
scrollViewThree.contentSize = CGSizeMake(self.view.frame.size.width*2, 300.f);
scrollViewThree.backgroundColor = [UIColor yellowColor];

[self.view addSubview:scrollViewOne];
[scrollViewOne addSubview:scrollViewTwo];
[scrollViewTwo addSubview:scrollViewThree];

这样我们发现黄色scrollViewThree滑完之后也不会滑到scrollViewTwo上面了。

① 自定义ScrollView

下面我们就自定义一个scrollView,默认是没有如上特性的,我们实现如上特性
分两步:

自定义一个EOCCustomScrollView继承于UIView
代码如下:
EOCCustomScrollView.h文件

#import <UIKit/UIKit.h>
#import "EOCPanGestureOne.h"
@interface EOCCustomScrollView : UIView

@property(nonatomic, assign)CGSize contentSize;
@property(nonatomic, strong)EOCPanGestureOne *panGesture;

@end

EOCCustomScrollView.m文件

#import "EOCCustomScrollView.h"

@interface EOCCustomScrollView()<UIGestureRecognizerDelegate>
@end

@implementation EOCCustomScrollView

- (instancetype)initWithFrame:(CGRect)frame {
    
    self = [super initWithFrame:frame];
    
    _panGesture = [[EOCPanGestureOne alloc] initWithTarget:self action:@selector(panAction:)];
    _panGesture.delegate = self;
    //_panGesture.cancelsTouchesInView = NO;
    [self addGestureRecognizer:_panGesture];
    
    return self;
}

//- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer {
//
//    NSLog(@"gestureRecognizerShouldBegin %@", gestureRecognizer.view);
//    if (self.bounds.origin.x == self.contentSize.width - self.frame.size.width) {
//
//        //往左滑动
//        CGPoint transitionPoint = [gestureRecognizer translationInView:self];
//        if (transitionPoint.x < 0) {
//            return NO;
//        }
//        return YES;
//    }
//
//    return YES;
//}

//或者我试了下实现下面这个方法也可以
//- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
//{
//    return YES;
//}

- (void)panAction:(UIPanGestureRecognizer *)gestureRecognizer {
    
    NSLog(@"bounds.x %f", self.bounds.origin.x);
    CGRect tmpBounds = self.bounds;
    
    //x,y方向移动的位置,左上小于0,右下大于0
    CGPoint transitionPoint = [gestureRecognizer translationInView:self];
    
    
    ///移动距离是有最大值和最小值
    CGFloat minimumOffset = 0.f;
    CGFloat maxOffset = _contentSize.width - tmpBounds.size.width;
    CGFloat actualOffset = fmax(minimumOffset, fmin(maxOffset, (tmpBounds.origin.x - transitionPoint.x)));
    tmpBounds.origin.x = actualOffset;
    
    
//    if (transitionPoint.x < 0 && (tmpBounds.origin.x - transitionPoint.x) <= (_contentSize.width - tmpBounds.size.width)) {  //往左
//
//        tmpBounds.origin.x -= transitionPoint.x;
//
//    } else if (transitionPoint.x > 0 && (tmpBounds.origin.x - transitionPoint.x) >= 0) {  //往右
//
//        tmpBounds.origin.x -= transitionPoint.x;
//
//    }
    
    //设置移动的距离为0,不让他叠加,不然移动的很快
    [gestureRecognizer setTranslation:CGPointZero inView:self];
    self.bounds = tmpBounds;
}

ViewController.m代码

EOCCustomScrollView *scrollView = [[EOCCustomScrollView alloc] initWithFrame:CGRectMake(150.f, 88.f, 100, 150.f)];
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width*2, scrollView.frame.size.height);
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];

///添加子view
UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(50.f, 20.f, 40.f, 40.f)];
blueView.backgroundColor = [UIColor blueColor];
[scrollView addSubview:blueView];

运行之后,滑动蓝色View,如图:
自定义ScrollView.png

可以发现。通过添加pan手势和改变bounds,实现了自定义scrollView。

② 模仿系统UIScrollView特性

那么我们如何实现系统自带的特性?
打开上面注释掉的 //_panGesture.cancelsTouchesInView = NO; 以及//gestureRecognizerShouldBegin方法,就可以实现,代码如上。

ViewController.m代码如下:

//EOCCustomScrollViewOne以及以下都是继承EOCCustomScrollView
EOCCustomScrollViewOne *scrollViewOne = [[EOCCustomScrollViewOne alloc] initWithFrame:CGRectMake(0.f, 100.f, self.view.frame.size.width, 300.f)];
scrollViewOne.contentSize = CGSizeMake(self.view.frame.size.width*2, 300.f);
scrollViewOne.backgroundColor = [UIColor redColor];

EOCCustomScrollViewTwo *scrollViewTwo = [[EOCCustomScrollViewTwo alloc] initWithFrame:CGRectMake(0.f, 0.f, self.view.frame.size.width, 300.f)];
scrollViewTwo.contentSize = CGSizeMake(self.view.frame.size.width, 300.f);
scrollViewTwo.backgroundColor = [UIColor blueColor];

//    EOCCustomScrollViewThree *scrollViewThree = [[EOCCustomScrollViewThree alloc] initWithFrame:CGRectMake(0.f, 0.f, self.view.frame.size.width, 300.f)];
//    scrollViewThree.contentSize = CGSizeMake(self.view.frame.size.width*2, 300.f);
//    scrollViewThree.backgroundColor = [UIColor yellowColor];

[self.view addSubview:scrollViewOne];
[scrollViewOne addSubview:scrollViewTwo];
//    [scrollViewTwo addSubview:scrollViewThree];

运行之后如图:
屏幕快照 2019-10-18 上午9.48.17.png

蓝色View拖完之后可以把红色View拖出来,这样就实现了系统的那个特性。

Demo地址:https://github.com/iamkata/scrollView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容