UIScrollView实现原理

很多东西我们只是在用,深入一步去了解其实现原理,可以加强我们对用到东西的认识,更好的去使用,可以提升我们得认识事物的深度,加强基础。这样才能更好的去使用和创造新的东西。

本文我们尝试着去理解UIScrollView的实现原理,并自己去实现一个简单的ScrollView。

UIScrollView基础

滚动视图,支持内容滚动、缩放等功能。

  • contentSize

scrollView内容的大小,contentSize是CGSzie类型的,可以设置scrollView内容的长和宽。

例如这样设置 self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame*2));滚动范围水平上不滚动,竖直方向上可以滚动一个屏的高度,即可以展示两个屏幕高度的视图内容。

  • contentOffset

CGPoint类型,内容左上角相对ScrollView左上角的点。这个属性可以确定ScrollView的滚动位置。

  • contentInset

UIEdgeInsets类型,这个属性可以给ScrollView的边增加额外的滚动区域。

例如这样设置

self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame));
self.scrollView.contentInset = UIEdgeInsetsMake(100, 100, 100, 100);

contentSize设置了内容大小就是整个屏幕大小,而可滑动的范围上左下右各是100。

img

效果是这样的:黑灰色是ScroollView,棕色是ScrollView上的一个子视图,我们可以滑动ScroollView内容视图,并超出ScrollView。contentInset就是设置了超出的范围。

UIScrollView的滚动原理

滚动原理

  • frame:该view在父view坐标系统中的位置和大小,以父视图左上角为坐标0点。
  • bounds:该view在本地坐标系统中的位置和大小,以view自己的坐标系为参照,以自己初始位置为0点。

滚动时scrollView的frame是不会变的,变的是bounds的origin。

我们可以给视图上添加一个ScrollView,滚动时候通过其代理来观察frame和bounds的变化。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSLog(@"frame:%@  bounds:%@",NSStringFromCGRect(self.view.frame),NSStringFromCGRect(self.scrollView.bounds));
}

得出的结论就是,frame是一直不变的,bounds的origin.x和origin.y在变化,这个取决于左右滚动还是上下滚动。

滚动原理实现

知道ScrollView的滚动原理后,结合原理试着去实现一个可滚动的ScrollView,这样更加加深我们对ScroolView的认识和理解。

  • 首先创建一个继承UIView的View

创建一个普通的View-MMScrollView,这个View就是我们要实现的ScroolView。

  • 必要属性

ScrollView有一些基础的属性,contentSize、panGestureRecognizer,这两个属性是实现ScrollView的滚动必不可少的属性。

  • 实现滚动

创建panGestureRecognizer手势,结合contentSize,在pan手势的方法里来实现实现滚动控制:

- (void)panGesRecognizerAction:(UIPanGestureRecognizer *)panGestureRecognizer
{
    if (panGestureRecognizer.state == UIGestureRecognizerStateChanged)
    {
        CGPoint translation = [panGestureRecognizer translationInView:self.superview];
        
        CGRect bounds = self.bounds;
        bounds.origin.x -= translation.x;
        bounds.origin.y -= translation.y;

        // It will never scroll in horizontal direction if the contentSize.width > CGRectGetWidth(self.frame)
        if (self.contentSize.width > CGRectGetWidth(self.frame)) {
            if (bounds.origin.x < 0) {
                bounds.origin.x = 0;
            }
            if (bounds.origin.x+CGRectGetWidth(self.frame) >= self.contentSize.width) {
                bounds.origin.x = self.contentSize.width-CGRectGetWidth(self.frame);
            }
        }
        else {
            bounds.origin.x = 0;
        }

        // It will never scroll in vertically direction if the self.contentSize.height > CGRectGetHeight(self.frame)
        if (self.contentSize.height > CGRectGetHeight(self.frame))
        {
            if (bounds.origin.y < 0) {
                bounds.origin.y = 0;
            }
            if (bounds.origin.y+CGRectGetHeight(self.frame) >= self.contentSize.height) {
                bounds.origin.y = self.contentSize.height-CGRectGetHeight(self.frame);
            }
        } else {
            bounds.origin.y = 0;
        }

        self.bounds = bounds;
        
        [panGestureRecognizer setTranslation:CGPointZero inView:self];
    }
}

我这里的实现算法比较low,大概看看就好了,肯定会有更好的实现方式。

这里是完整代码:

// MMScrollView.h

@interface MMScrollView : UIView

@property (nonatomic) CGSize contentSize; // default CGSizeZero. Set the scrollview content boundary.

// Change `panGestureRecognizer.allowedTouchTypes` to limit scrolling to a particular set of touch types.
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer;

@end

// MMScrollView.m

#import "MMScrollView.h"

@interface MMScrollView ()

@property (nonatomic, readwrite) UIPanGestureRecognizer *panGestureRecognizer;

@end

@implementation MMScrollView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        
        self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesRecognizerAction:)];
        [self addGestureRecognizer:self.panGestureRecognizer];
    }
    return self;
}

- (void)panGesRecognizerAction:(UIPanGestureRecognizer *)panGestureRecognizer
{
    if (panGestureRecognizer.state == UIGestureRecognizerStateChanged)
    {
        CGPoint translation = [panGestureRecognizer translationInView:self.superview];
        
        CGRect bounds = self.bounds;
        bounds.origin.x -= translation.x;
        bounds.origin.y -= translation.y;
        
        if (self.contentSize.width > CGRectGetWidth(self.frame)) {
            if (bounds.origin.x < 0) {
                bounds.origin.x = 0;
            }
            if (bounds.origin.x+CGRectGetWidth(self.frame) >= self.contentSize.width) {
                bounds.origin.x = self.contentSize.width-CGRectGetWidth(self.frame);
            }
        }
        else {
            bounds.origin.x = 0;
        }

        if (self.contentSize.height > CGRectGetHeight(self.frame))
        {
            if (bounds.origin.y < 0) {
                bounds.origin.y = 0;
            }
            if (bounds.origin.y+CGRectGetHeight(self.frame) >= self.contentSize.height) {
                bounds.origin.y = self.contentSize.height-CGRectGetHeight(self.frame);
            }
        } else {
            bounds.origin.y = 0;
        }

        self.bounds = bounds;
        
        [panGestureRecognizer setTranslation:CGPointZero inView:self];
    }
}

@end

我们可以在ViewController里面测试一下我们实现的MMScrollView:

初始化一个MMScrollView,添加到视图上,在上面添加一些子视图,设置contentSize属性,就可以滚动了。

这里我们加一个队ScrollView的bounds得监听,通过监听来看其bounds值得变化。

#import "ViewController.h"
#import "MMScrollView.h"

@interface ViewController ()

@property (nonatomic, strong) MMScrollView *myScrollView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.myScrollView = [[MMScrollView alloc] initWithFrame:self.view.bounds];
    self.myScrollView.contentSize = CGSizeMake(0, CGRectGetHeight(self.view.frame)*2);
    [self.view addSubview:self.myScrollView];
    
    for (int i = 0; i < 20; i ++) {
        UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(100, 10+i*(100+10), 100, 100)];
        subView.backgroundColor = [ViewController randomColor];
        [self.myScrollView addSubview:subView];
    }
    
    [self.myScrollView addObserver:self forKeyPath:@"bounds" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"bounds"]) {
        
        NSLog(@"%@",NSStringFromCGRect(self.myScrollView.bounds));
    }
}

+ (UIColor *)randomColor {
    CGFloat hue = (arc4random() %256/256.0);
    CGFloat saturation = (arc4random() %128/256.0) +0.5;
    CGFloat brightness = (arc4random() %128/256.0) +0.5;
    return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1.0];
}

@end

我使用iPad Air横屏进行的测试。

当ScrolllView滚动到最下面的时候,打印值是{{0, 834}, {1112, 834}},当滚动到最顶部的时候打印值是{{0, 0}, {1112, 834}}。833和1112分别是横屏下屏幕的高和宽。

我们可以看只有origin.y在变化,因为我们实现的竖直方向上的滚动。

从顶部{{0, 0}, {1112, 834}},到底部{{0, 834}, {1112, 834}},滚动了一整个屏,滚动范围是两个屏幕的高,这是我们属性contentSize里设置的。

UIScrollView的缩放原理

缩放原理

ScrollView实现缩放,是通过setZoomScale来实现的,通过设置可以使其子视图进行放大和缩小。代理方法中需要返回要设置缩放的视图,ScrollView的内部通过代理方法拿到需要缩放的视图并对其进行操作。

// return a view that will be scaled.
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.containerView;
}

// set the imageView zoom scale

self.scrollView.minimumZoomScale = 0.5;
self.scrollView.maximumZoomScale = 2;

缩放的时候,视图大小、位置等也发生了改变。还可以对其进行移动、旋转等操作,这实质上是通过transform来实现的。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.showsVerticalScrollIndicator = NO;
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.backgroundColor = [UIColor clearColor];
    self.scrollView.delegate = self;
    self.scrollView.minimumZoomScale = 0.5;
    self.scrollView.maximumZoomScale = 2;
    [self.view addSubview:self.scrollView];
    
    self.containerView = [[UIView alloc] initWithFrame:self.view.bounds];
    self.containerView.backgroundColor = [UIColor lightGrayColor];
    [self.scrollView addSubview:self.containerView];
}

// return a view that will be scaled.
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.containerView;
}

// call back any zoom scale changes
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
//    CGFloat offsetX = (scrollView.bounds.size.width > scrollView.contentSize.width) ? (scrollView.bounds.size.width - scrollView.contentSize.width)/2 : 0.0;
//    CGFloat offsetY = (scrollView.bounds.size.height > scrollView.contentSize.height) ? (scrollView.bounds.size.height - scrollView.contentSize.height)/2 : 0.0;
//    self.containerView.center = CGPointMake(scrollView.contentSize.width/2 + offsetX,scrollView.contentSize.height/2 + offsetY);
    
    // give a center point to scaled view.
    self.containerView.center = self.view.center;
}

在scrollViewDidZoom里做处理,可以实现居中缩放的功能。

缩放原理实现

通过实验我发现当缩放的时候,ScrollView本身也会被缩放

- (void)pinchGestureRecognizerAction:(UIPinchGestureRecognizer *)pinGestureRecognizer
{
    if ([self.delegate respondsToSelector:@selector(viewForZoomingInScrollView:)])
    {
        if (pinGestureRecognizer.state == UIGestureRecognizerStateEnded)
        {
            self.currentScale = pinGestureRecognizer.scale;
        }
        else if(pinGestureRecognizer.state == UIGestureRecognizerStateBegan && self.currentScale != 0.0f)
        {
            pinGestureRecognizer.scale = self.currentScale;
        }
        
        if (pinGestureRecognizer.scale !=NAN && pinGestureRecognizer.scale != 0.0)
        {
            pinGestureRecognizer.view.transform = CGAffineTransformMakeScale(pinGestureRecognizer.scale, pinGestureRecognizer.scale);
        }
    }
}

下面是整合了滚动和缩放功能的ScrollView的完整代码:

  • MMScrollView.h
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@class MMScrollView;

@protocol MMScrollViewDelegate <NSObject>

@optional

- (nullable UIView *)viewForZoomingInScrollView:(MMScrollView *)scrollView;     // return a view that will be scaled. if delegate returns nil, nothing happens

@end


@interface MMScrollView : UIView

@property (nonatomic) CGSize contentSize; // default CGSizeZero. Set the scrollview content boundary.

// Change `panGestureRecognizer.allowedTouchTypes` to limit scrolling to a particular set of touch types.
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer;

// `pinchGestureRecognizer` will return nil when zooming is disabled.
@property(nullable, nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer;

// delegate
@property (nonatomic, weak) id <MMScrollViewDelegate> delegate;

@end

NS_ASSUME_NONNULL_END
  • MMScrollView.m
#import "MMScrollView.h"

@interface MMScrollView ()

@property (nonatomic, readwrite) UIPanGestureRecognizer *panGestureRecognizer;
@property(nullable, nonatomic, readwrite) UIPinchGestureRecognizer *pinchGestureRecognizer;

@property (nonatomic) CGFloat currentScale;

@end

@implementation MMScrollView

- (UIPinchGestureRecognizer *)pinchGestureRecognizer {
    if (!_pinchGestureRecognizer) {
        _pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchGestureRecognizerAction:)];
    }
    return _pinchGestureRecognizer;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        
        self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesRecognizerAction:)];
        self.panGestureRecognizer.cancelsTouchesInView = YES;
        [self addGestureRecognizer:self.panGestureRecognizer];
    }
    return self;
}

- (void)setDelegate:(id<MMScrollViewDelegate>)delegate
{
    _delegate = delegate;
    
    if ([_delegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
        if (![self.gestureRecognizers containsObject:self.pinchGestureRecognizer]) {
            [self addGestureRecognizer:self.pinchGestureRecognizer];
        }
    }
    else {
        if ([self.gestureRecognizers containsObject:self.pinchGestureRecognizer]) {
            [self removeGestureRecognizer:self.pinchGestureRecognizer];
        }
    }
}

- (void)panGesRecognizerAction:(UIPanGestureRecognizer *)panGestureRecognizer
{
    if (panGestureRecognizer.state == UIGestureRecognizerStateChanged)
    {
        CGPoint translation = [panGestureRecognizer translationInView:self.superview];
        
        CGRect bounds = self.bounds;
        bounds.origin.x -= translation.x;
        bounds.origin.y -= translation.y;
        
        if (self.contentSize.width > CGRectGetWidth(self.frame)) {
            if (bounds.origin.x < 0) {
                bounds.origin.x = 0;
            }
            if (bounds.origin.x+CGRectGetWidth(self.frame) >= self.contentSize.width) {
                bounds.origin.x = self.contentSize.width-CGRectGetWidth(self.frame);
            }
        }
        else {
            bounds.origin.x = 0;
        }

        if (self.contentSize.height > CGRectGetHeight(self.frame))
        {
            if (bounds.origin.y < 0) {
                bounds.origin.y = 0;
            }
            if (bounds.origin.y+CGRectGetHeight(self.frame) >= self.contentSize.height) {
                bounds.origin.y = self.contentSize.height-CGRectGetHeight(self.frame);
            }
        } else {
            bounds.origin.y = 0;
        }

        self.bounds = bounds;
        
        [panGestureRecognizer setTranslation:CGPointZero inView:self];
    }
}

- (void)pinchGestureRecognizerAction:(UIPinchGestureRecognizer *)pinGestureRecognizer
{
    if ([self.delegate respondsToSelector:@selector(viewForZoomingInScrollView:)])
    {
        if (pinGestureRecognizer.state == UIGestureRecognizerStateEnded)
        {
            self.currentScale = pinGestureRecognizer.scale;
        }
        else if(pinGestureRecognizer.state == UIGestureRecognizerStateBegan && self.currentScale != 0.0f)
        {
            pinGestureRecognizer.scale = self.currentScale;
        }
        
        if (pinGestureRecognizer.scale !=NAN && pinGestureRecognizer.scale != 0.0)
        {
            pinGestureRecognizer.view.transform = CGAffineTransformMakeScale(pinGestureRecognizer.scale, pinGestureRecognizer.scale);
        }
    }
}

@end
  • ViewController
#import "ViewController.h"
#import "MMScrollView.h"

@interface ViewController ()<MMScrollViewDelegate>

@property (nonatomic, strong) MMScrollView *myScrollView;
@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.myScrollView = [[MMScrollView alloc] initWithFrame:self.view.bounds];
    self.myScrollView.backgroundColor = [UIColor darkGrayColor];
    self.myScrollView.delegate = self;
    self.myScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)*2);
    [self.view addSubview:self.myScrollView];

    self.imageView = [[UIImageView alloc] initWithFrame:self.myScrollView.bounds];
    self.imageView.backgroundColor = [UIColor brownColor];
    [self.myScrollView addSubview:self.imageView];
}

#pragma mark - MMScrollViewDelegate
// return a view that will be scaled. if delegate returns nil, nothing happens
- (nullable UIView *)viewForZoomingInScrollView:(MMScrollView *)scrollView
{
    return self.imageView;
}

@end

这个示例代码当滚动和缩放都实现的时候是有bug的,但是并不妨碍我们通过它去探索UIScrollView的基本实现原理,不妨碍我们去探索UIScrollView滚动和缩放的本质。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Morris_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值