iOS-UIScrollView原理

一. UIScrollView屬性、方法和代理

1. UIScrollView屬性、方法

自定義系統(tǒng)的UIScrollView之前我們要先解釋UIScrollView的屬性、方法,代理方法,因?yàn)閁IScrollView的有些屬性還是比較難理解的,直接拷貝系統(tǒng)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

關(guān)于這三個屬性,網(wǎng)上有太多的解釋,可參考:http://www.itdecent.cn/p/e5582fe5dd4a

下面補(bǔ)充一下,iOS7之后出現(xiàn)了下面這個屬性,這個屬性是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

這個屬性的字面意思是自動調(diào)節(jié)ScrollView的Insets,默認(rèn)YES,比如我們像如下創(chuàng)建scrollView的時候:

scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
  1. 因?yàn)閍utomaticallyAdjustsScrollViewInsets默認(rèn)是YES,系統(tǒng)幫我們調(diào)節(jié)了ScrollVIew的Insets,所以我們的scrollView是從導(dǎo)航欄下面開始的,顯示正常。
  2. 如果我們把a(bǔ)utomaticallyAdjustsScrollViewInsets設(shè)置為NO,我們就要手動設(shè)置ScrollVIew的Insets,代碼如下:
//其中88是導(dǎo)航欄的高度
scrollView.contentInset = UIEdgeInsetsMake(88.f, 0.f, 0.f, 0.f);
scrollView.contentOffset = CGPointMake(0.f, -88.f);//這句如果不寫,不會自動滾動到指定位置
  1. 如果我們把a(bǔ)utomaticallyAdjustsScrollViewInsets設(shè)置為NO,又沒手動設(shè)置ScrollVIew的Insets,這時候就需要調(diào)節(jié)scrollView的frame的y值了。

這個屬性在iOS11已廢棄,使用UIScrollView的如下屬性代替,和上面屬性一樣的,默認(rèn)自動調(diào)節(jié),可以設(shè)置為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之后,由于劉海屏幕的出現(xiàn)有了這個屬性,它是安全區(qū)域之外的額外inserts,比如iPhone X豎屏?xí)r占滿整個屏幕的控制器的view的safeAreaInsets是(44,0,34,0) ,44和34分別是劉海和手勢操作的區(qū)域。

說了這么多介紹下面這個屬性就很簡單了

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. 當(dāng)設(shè)置UIScrollViewContentInsetAdjustmentAutomatic時:
    adjustedContentInset = safecontentInset + scrollView.contentInset
  2. 當(dāng)設(shè)置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屬性改變會調(diào)用,和代理里面scrollViewDidChangeAdjustedContentInset方法一樣的。

contentLayoutGuide和frameLayoutGuide

iOS11新增,用于描述內(nèi)容布局和整體布局信息

@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

//默認(rèn)為FALSE, 如果設(shè)置為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 豎直方向總是可以彈性滑動,默認(rèn)是NO, 當(dāng)設(shè)置為TRUE(前提是屬性bounces必須為TRUE)的時候,即使contentSize設(shè)置的width 和 height都比UIScrollView的width 和 height小,在垂直方向上都可以有滑動效果,甚至即使我們不設(shè)置contentSize都可以產(chǎn)生滑動效果; 反之,如果設(shè)置alwaysBounceVertical為FALSE, 那么當(dāng)contentSize設(shè)置的width 和 height都比UIScrollView的width 和 height小的時候,即使bounces設(shè)置為TRUE,那么不可能產(chǎn)生彈性效果
@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

//是否可分頁,默認(rèn)是FALSE, 如果設(shè)置成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) 
//當(dāng)向下滑動時,滑動條距離頂部的距離總是20
@property(nonatomic)         UIEdgeInsets                 scrollIndicatorInsets;          // default is UIEdgeInsetsZero. adjust indicators inside of insets

//滾動條樣式,是個枚舉類型:
@property(nonatomic)         UIScrollViewIndicatorStyle   indicatorStyle;                 // default is UIScrollViewIndicatorStyleDefault

//減速率,CGFloat類型,當(dāng)你滑動松開手指后的減速速率, 但是盡管decelerationRate是一個CGFloat類型,但是目前系統(tǒng)只支持以下兩種速率設(shè)置選擇:
UIScrollViewDecelerationRateNormal    值是 0.998
UIScrollViewDecelerationRateFast      值是 0.99
@property(nonatomic)         UIScrollViewDecelerationRate decelerationRate NS_AVAILABLE_IOS(3_0);

//索引展示模式,是個枚舉 : 自動顯示或隱藏 || 一直隱藏
@property(nonatomic)         UIScrollViewIndexDisplayMode indexDisplayMode API_AVAILABLE(tvos(10.2));

方法

//帶有動畫效果的設(shè)置偏移量
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;  // animate at constant velocity to new offset

//滑動到指定的可見區(qū)域(帶動畫),意思就是滑動到CGRect所組成的矩形區(qū)域,使其可見. 如已可見則什么都不做
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated;         // scroll so rect is just visible (nearest edges). nothing if rect completely visible

// 設(shè)置的時候,當(dāng)頁面加載成功出現(xiàn)時,滑動條會自動顯示出來,停留一下又自動隱藏
// 不設(shè)置的話,頁面出現(xiàn)時也不會顯示滑動條,只有在滑動過程中會顯示滑動條
- (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

關(guān)于這兩個屬性暫時只說一點(diǎn),當(dāng)scrollView上面添加一個UISliderView的時候,如果拖動UISliderView,scrollView會移動,當(dāng)我們設(shè)置delaysContentTouches = NO,canCancelContentTouches = NO的時候,UISliderView和scrollView的拖動才互不影響。
更多詳情參考:http://www.itdecent.cn/p/2c74b7a6c082

touchesShouldBegin和touchesShouldCancelInContentView

//必須子類化之后重寫,用以控制將觸摸事件傳遞給scrollView的子視圖
//在觸摸被傳遞到scrollView的子視圖之前調(diào)用。如果它返回NO,觸摸將不會被傳遞到子視圖
//這對press沒有影響
//默認(rèn)返回YES
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event inContentView:(UIView *)view;

//如果觸摸已經(jīng)傳遞到滾動視圖的子視圖,則在滾動開始之前調(diào)用。如果返回NO,觸摸將繼續(xù)傳遞到子視圖,滾動將不會發(fā)生
//如果canCancelContentTouches為NO,則不調(diào)用。如果視圖不是UIControl,默認(rèn)返回YES
- (BOOL)touchesShouldCancelInContentView:(UIView *)view;

關(guān)于動畫的一些屬性:

minimumZoomScale和maximumZoomScale

//滑動視圖的最小縮放倍數(shù),默認(rèn)是1.0
@property(nonatomic) CGFloat minimumZoomScale;     // default is 1.0

//滑動視圖的最大縮放倍數(shù),默認(rèn)是1.0(要使得縮放有效果,maximumZoomScale必須要大于minimumZoomScale)
@property(nonatomic) CGFloat maximumZoomScale;     // default is 1.0. must be > minimum zoom scale to enable zooming

例如,實(shí)現(xiàn)如下的縮放效果:

效果圖.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)
    }
}

注意:想要實(shí)現(xiàn)縮放效果,代理必須要實(shí)現(xiàn)func viewForZooming(in scrollView: UIScrollView) -> UIView?方法,否則無法實(shí)現(xiàn)縮放功能。如果要達(dá)到縮放后的一些效果操作還要實(shí)現(xiàn)代理的func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)方法。

zoomScale

//當(dāng)前的縮放比例, 默認(rèn)是1.0
@property(nonatomic) CGFloat zoomScale NS_AVAILABLE_IOS(3_0);            // default is 1.0

比如,雙擊當(dāng)前圖片,使其縮放成原來的0.8倍:

func tap() {
    // 雙擊當(dāng)前圖片,使其縮放成原來的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)
}

其他動畫屬性

//設(shè)置縮放比例,帶動畫
- (void)setZoomScale:(CGFloat)scale animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);

//把從scrollView里截取的矩形區(qū)域縮放到整個scrollView當(dāng)前可視的frame里面。如果截取的區(qū)域大于scrollView的frame時,圖片縮小,如果截取區(qū)域小于frame,會看到圖片放大。一般情況下rect需要自己計(jì)算出來。即要把用戶點(diǎn)擊坐標(biāo)附近的區(qū)域內(nèi)容在scrollViewl里進(jìn)行縮放
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);

//彈性縮放,默認(rèn)是true, 設(shè)置成false的話,當(dāng)縮放到最大或最小值的時候不會有彈性效果
@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屬性,當(dāng)沒有設(shè)置bouncesZoom的時候,如果正在縮放過程中則為false,如果縮放到最小值或者最大值時松開手指則為true; 當(dāng)設(shè)置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

//滑動到頂部,默認(rèn)是true,當(dāng)點(diǎn)擊狀態(tài)欄的時候,如果當(dāng)前UIScrollView不是處在頂部位置,那么可以直接回到頂部;如果已經(jīng)在頂部,則沒有作用;另外必須注意如果要這個屬性起作用,它的delegate方法func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool不能返回false,否則沒用。
@property(nonatomic) BOOL  scrollsToTop __TVOS_PROHIBITED;          // default is YES.

//平移手勢,get屬性,這個手勢的代理系統(tǒng)設(shè)置的scrollView,不能修改為其他的. 可以通過設(shè)置平移手勢的屬性來改變平移方式,比如設(shè)置觸摸手指的最少個數(shù)minimumNumberOfTouches
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);

//捏合手勢,也就是縮放手勢,get屬性,設(shè)置同平移手勢一樣,當(dāng)縮放禁用時返回nil。
@property(nullable, nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);


// 定向按壓手勢,給tvos用的
@property(nonatomic, readonly) UIGestureRecognizer *directionalPressGestureRecognizer ;

//鍵盤消失模式, 默認(rèn)是none,是個枚舉值
public enum UIScrollViewKeyboardDismissMode : Int {
    case none             //  無
    case onDrag       //  拖拽,只要滑動UIScrollView,鍵盤消失
    case interactive  //  交互式,拖住UIScrollView一直下滑,當(dāng)接觸到鍵盤時,鍵盤就跟著同步下滑
}
@property(nonatomic) UIScrollViewKeyboardDismissMode keyboardDismissMode NS_AVAILABLE_IOS(7_0); // default is UIScrollViewKeyboardDismissModeNone

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

更多關(guān)于圖片縮放,請參考:iOS圖片縮放

2. UIScrollView代理方法

//已經(jīng)滑動,常用來處理一些偏移量,判斷拖拽狀態(tài)等
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;                                               // any offset changes

//已經(jīng)縮放,處理一些縮放操作
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2); // any zoom scale changes

//即將開始拖拽(或許要拖拽移動一小段距離才會調(diào)用),只要一開始拖拽就會調(diào)用
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;

//即將松開手指結(jié)束拖拽
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);

//已經(jīng)松開手指,結(jié)束拖拽狀態(tài)
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

//即將開始減速
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;   // called on finger up as we are moving

//已經(jīng)結(jié)束減速, 當(dāng)UIScrollView停止時就調(diào)用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;      // called when scroll view grinds to a halt

//已經(jīng)結(jié)束滑動動畫,這個方法起作用的前提是設(shè)置了下滿面提到的兩種方法中的任意一種,否則不會起作用!
// 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

//返回一個需要縮放的視圖,需要做縮放的時候必須調(diào)用此方法,之前已經(jīng)講過,不再贅述!
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;     // return a view that will be scaled. if delegate returns nil, nothing happens

//即將開始縮放,在滑動視圖開始縮放它的內(nèi)容視圖前調(diào)用
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view NS_AVAILABLE_IOS(3_2); // called before the scroll view begins zooming its content

//已經(jīng)結(jié)束縮放狀態(tài),結(jié)束縮放手勢時調(diào)用,在最小和最大值之前進(jìn)行縮放
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale; // scale between minimum and maximum. called after any 'bounce' animations

//滑到頂部,這個方法和屬性scrollsToTop用法一致,本質(zhì)一致,返回true則是可以滑動頂部,false反之!
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;   // return a yes if you want to scroll to the top. if not defined, assumes YES

//已經(jīng)滑到頂部,當(dāng)滑動到頂部的動畫完成的時候調(diào)用
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;      // called when scrolling animation finished. may be called immediately if already at top

//adjustedContentInset屬性發(fā)生改變調(diào)用, 和[UIScrollView adjustedContentInsetDidChange]一樣,當(dāng)然這個代理方法是給外界用的
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView API_AVAILABLE(ios(11.0), tvos(11.0));

二. UIScrollView原理和自定義UIScrollView

1. UIScrollView原理

  1. 通過上面的屬性介紹,我們發(fā)現(xiàn)系統(tǒng)的UIScrollView有一個pan手勢,pan手勢的代理是UIScrollView,并且不能修改pan手勢的代理。
  2. 到底UIScrollView是怎么滑動的呢?
    UIScrollView繼承于UIView,然后自己上面添加一個手勢,UIScrollView實(shí)質(zhì)就是根據(jù)手勢的滑動修改它的bounds來進(jìn)行view的滑動,可以在代理方法里打印ScrollView的bounds值來驗(yàn)證。

2. 自定義UIScrollView

系統(tǒng)的UIScrollView特點(diǎn):

首先看一下系統(tǒng)的UIScrollView的一個特點(diǎn),如下代碼:

//灰色的scrollView添加到紅色的scrollView上
//系統(tǒng)UIScrollView現(xiàn)實(shí)情況是,可以滑動到父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手勢手勢被識別之后是不會再傳給父控件了,但是這是怎么回事呢?

我們先屏蔽系統(tǒng)的這個特點(diǎn),新建一個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];

這樣我們發(fā)現(xiàn)黃色scrollViewThree滑完之后也不會滑到scrollViewTwo上面了。

① 自定義ScrollView

下面我們就自定義一個scrollView,默認(rèn)是沒有如上特性的,我們實(shí)現(xiàn)如上特性
分兩步:

自定義一個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;
//}

//或者我試了下實(shí)現(xiàn)下面這個方法也可以
//- (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;
//
//    }
    
    //設(shè)置移動的距離為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];

運(yùn)行之后,滑動藍(lán)色View,如圖:
自定義ScrollView.png

可以發(fā)現(xiàn)。通過添加pan手勢和改變bounds,實(shí)現(xiàn)了自定義scrollView。

② 模仿系統(tǒng)UIScrollView特性

那么我們?nèi)绾螌?shí)現(xiàn)系統(tǒng)自帶的特性?
打開上面注釋掉的 //_panGesture.cancelsTouchesInView = NO; 以及//gestureRecognizerShouldBegin方法,就可以實(shí)現(xiàn),代碼如上。

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];

運(yùn)行之后如圖:
屏幕快照 2019-10-18 上午9.48.17.png

藍(lán)色View拖完之后可以把紅色View拖出來,這樣就實(shí)現(xiàn)了系統(tǒng)的那個特性。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容