UIScrollView 技巧(2)

Zooming 缩放


为了实现一个scrollview可以缩放其内容,你可以设置scroll view的minimumZoomScale 和 maximumZoomScale属性让至少一个不为1(默认就是1)。你还需要在scroll view的委托中实现 viewForZoomingInScrollView: 方法来指定哪个是可缩放的scrollview 子视图。这个scrollview然后就会对它的子视图应用一个缩放转换。至于缩放多少是由scrollview的 zoomScale决定的。通常情况下我们需要scrollview的所有子视图都可以缩放,所以我们一般把所有的子视图放在一个content view中,再把这个content view添加到scrollview上。

v.tag = 999;
sv.minimumZoomScale = 1.0;
sv.maximumZoomScale = 2.0;
sv.delegate = self;

v是sv(scrollview)的一个子视图,这个子视图就是将要可以缩放的视图

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return [scrollView viewWithTag:999];
}

这样,用户就可以通过手势缩放scrollview的内容了。为了让用户缩放到临界值时不能再缩放了(默认是可以缩放超过临界值一点,当用户松开手时,scrollview会弹回临界值),我们可以设置bouncesZoom为NO。

为了让用户缩放scrollview内容时仍然可以使用scrollRectToVisible:animated:方法来滚动到指定的区域,scrollview默认会根据当前的zoomScale属性来设置它的contentSize属性。

当minimumZoomScale小于1,如果scrollview的内容缩放得比scrollview还小,那么这个缩放的视图会固定在scrollview的左上角。如果你不喜欢这样,可以通过子类化UIScrollView,重写它的layoutSubviews方法,或者通过scrollview的委托实现 scrollViewDidZoom:方法。下面是WWDC 2010视频上的一段代码演示,让缩放视图比scrollview小时,固定在scrollview的中间:

- (void)layoutSubviews {
    [super layoutSubviews];
    UIView* v = [self.delegate viewForZoomingInScrollView:self];
    CGFloat svw = self.bounds.size.width;
    CGFloat svh = self.bounds.size.height;
    CGFloat vw = v.frame.size.width;
    CGFloat vh = v.frame.size.height;
    CGRect f = v.frame;
    if (vw < svw)
        f.origin.x = (svw - vw) / 2.0;
    else
        f.origin.x = 0;
    if (vh < svh)
        f.origin.y = (svh - vh) / 2.0;
    else
        f.origin.y = 0;
    v.frame = f;
}

用代码进行缩放


  • setZoomScale:animated:

    contentOffset属性会自动适应,让当前的缩放内容始终在scrollview的中心,同时让内容全屏显示在scrollview上。

  • zoomToRect:animated:

    同上。

下面我们通过一个双击(UITapGestureRecognizer),让scrollview缩放到最大值,原始值,最小值:

- (void) tapped: (UIGestureRecognizer*) tap {
    UIView* v = tap.view;
    UIScrollView* sv = (UIScrollView*)v.superview;
    if (sv.zoomScale < 1) {
        [sv setZoomScale:1 animated:YES];
        CGPoint pt =
            CGPointMake((v.bounds.size.width - sv.bounds.size.width)/2.0,0);
        [sv setContentOffset:pt animated:NO];
    }
    else if (sv.zoomScale < sv.maximumZoomScale)
        [sv setZoomScale:sv.maximumZoomScale animated:YES];
    else
        [sv setZoomScale:sv.minimumZoomScale animated:YES];
}

缩放时,让内容更清晰


默认情况下,当一个scrollview缩放时,它仅仅是对这个缩放视图提供一个缩放转换。这个缩放视图的绘制内容是之前它的图层缓存的内容,所以当我们缩放这个视图时,原来像素的图片相当于绘制得更大,意味着内容会不清晰。

一个解决方法是使用CATiledLayer提供的功能,CATiledLayer不仅仅提供滚动,还提供缩放。我们需要用到它的另外两个属性:

  • levelsOfDetail

    假如这个数值是2,意味着我们又两种显示级别,可以是2x或者1x大小

  • levelsOfDetailBias

    有多少个显示级别是大于1x的。例如,levelsOfDetail为2,如果我们希望当放大时,大小为2x,缩小时,大小为1x,那么这个值就是1。默认是0,就是说大小从 0.5x 到 1x变化。

下面是一个小例子,scrollview 的contentview绘制了30个字:

- (void)loadView {
    UIScrollView* sv = [[UIScrollView alloc] initWithFrame:
                        [[UIScreen mainScreen] applicationFrame]];
    self.view = sv;
    CGRect f = CGRectMake(0, 0, self.view.bounds.size.width,
                          self.view.bounds.size.height * 2);
    TiledView* content = [[TiledView alloc] initWithFrame:f];
    content.tag = 999;
    CATiledLayer* lay = (CATiledLayer*)content.layer;
    lay.tileSize = f.size;
    lay.levelsOfDetail = 2;
    lay.levelsOfDetailBias = 1;
    [self.view addSubview:content];
    [sv setContentSize: f.size];
    sv.minimumZoomScale = 1.0;
    sv.maximumZoomScale = 2.0;
    sv.delegate = self;
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return [scrollView viewWithTag:999];
}

下面是TiledView的代码:

+ (Class) layerClass {
    return [CATiledLayer class];
}
-(void)drawRect:(CGRect)r {
    [[UIColor whiteColor] set];
    UIRectFill(self.bounds);
    [[UIColor blackColor] set];
    UIFont* f = [UIFont fontWithName:@"Helvetica" size:18];
    // height consists of 31 spacers with 30 texts between them
    CGFloat viewh = self.bounds.size.height;
    CGFloat spacerh = 10;
    CGFloat texth = (viewh - (31*spacerh))/30.0;
    CGFloat y = spacerh;
    for (int i = 0; i < 30; i++) {
        NSString* s = [NSString stringWithFormat:@"This is label %d", i];
        [s drawAtPoint:CGPointMake(10,y) withFont:f];
        y += texth + spacerh;
    }
    // uncomment the following to see the tiling
    /*
    UIBezierPath* bp = [UIBezierPath bezierPathWithRect:r];
    [[UIColor redColor] setStroke];
    [bp stroke];
    */
}

如果不使用CATiledLayer,我们可以使用另一种方法,但是这个方法不能乱用,过度用。就是在scrollview 的委托中实现下面的方法:

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView
                       withView:(UIView *)view
                        atScale:(float)scale {
    view.contentScaleFactor = scale * [UIScreen mainScreen].scale;
}

如果这个zoomScale,屏幕分辨率,还有这个缩放视图的大小很大,你将需要一个非常大的图形上下文在内存中,可能会导致你的app内存用完。

UIScrollView 的触摸


假如我们有一个很大的地图图片,比屏幕大,我们希望用户滚动这个地图来查看,同时还可以拖动一个标记到一个新的位置,下面是一个小例子:

UIScrollView* sv = [UIScrollView new];
self.sv = sv;
[self.view addSubview:sv];
sv.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[sv]|" 
                                               options:0 metrics:nil views:@{@"sv":sv}]];
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[sv]|" 
                                               options:0 metrics:nil views:@{@"sv":sv}]];
UIImageView* imv = [[UIImageView alloc] initWithImage: [UIImage imageNamed:@"map.jpg"]];
[sv addSubview:imv];
imv.translatesAutoresizingMaskIntoConstraints = NO;
// constraints here mean "content view is the size of the map image view"
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imv]|"
                                               options:0 metrics:nil views:@{@"imv":imv}]];
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imv]|" 
                                               options:0 metrics:nil views:@{@"imv":imv}]];
UIImageView* flag = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"redflag.png"]];
[sv addSubview: flag];
UIPanGestureRecognizer* pan = [[UIPanGestureRecognizer alloc] initWithTarget:self
                                                             action:@selector(dragging:)];
[flag addGestureRecognizer:pan];
flag.userInteractionEnabled = YES;

下面的拖动手势识别,当拖动到边缘的时候,scrollview还可以自动移动来适应新的位置,也就是用户可以在整副图片上拖动标记:

- (void) dragging: (UIPanGestureRecognizer*) p {
    UIView* v = p.view;
    if (p.state == UIGestureRecognizerStateBegan ||
            p.state == UIGestureRecognizerStateChanged) {
        CGPoint delta = [p translationInView: v.superview];
        CGPoint c = v.center;
        c.x += delta.x; c.y += delta.y;
        v.center = c;
        [p setTranslation: CGPointZero inView: v.superview];
    } 
    // autoscroll
    if (p.state == UIGestureRecognizerStateChanged) {
        CGPoint loc = [p locationInView:self.view.superview];
        CGRect f = self.view.frame;
        UIScrollView* sv = self.sv;
        CGPoint off = sv.contentOffset;
        CGSize sz = sv.contentSize;
        CGPoint c = v.center;
        // to the right
        if (loc.x > CGRectGetMaxX(f) - 30) {
            CGFloat margin = sz.width - CGRectGetMaxX(sv.bounds);
            if (margin > 6) {
                off.x += 5;
                sv.contentOffset = off;
                c.x += 5;
                v.center = c;
                [self keepDragging:p];
            } 
        }
        // to the left
        if (loc.x < f.origin.x + 30) {
            CGFloat margin = off.x;
            if (margin > 6) {
                // ... omitted ...
            }
        }
        // to the bottom
        if (loc.y > CGRectGetMaxY(f) - 30) {
            CGFloat margin = sz.height - CGRectGetMaxY(sv.bounds);
            if (margin > 6) {
                // ... omitted ...
            }
        }
        // to the top
        if (loc.y < f.origin.y + 30) {
            CGFloat margin = off.y;
            if (margin > 6) {
                // ... omitted ...
            }
        } 
    }
}


- (void) keepDragging: (UIPanGestureRecognizer*) p {
    float delay = 0.1;
    dispatch_time_t popTime =
        dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC);
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self dragging: p];
    });
}

下面的例子是,我想在初始化的时候,标记是在屏幕外面的,当用户向右轻扫时才让这个标记从屏幕外以动画形式移动回界面里,但是这个向右滑动的手势会被scrollview内置的手势识别,我们自定义的手势无法识别,怎么办?没关系,我们添加一个手势识别检测依赖:

UISwipeGestureRecognizer* swipe =
    [[UISwipeGestureRecognizer alloc]
        initWithTarget:self action:@selector(swiped:)];
[sv addGestureRecognizer:swipe];
[sv.panGestureRecognizer requireGestureRecognizerToFail:swipe];

只有在我们自定义的手势识别结束后才开始识别内部的手势:

- (void) swiped: (UISwipeGestureRecognizer*) g {
    if (g.state == UIGestureRecognizerStateEnded ||
            g.state == UIGestureRecognizerStateCancelled) {
        UIImageView* flag =
            [[UIImageView alloc] initWithImage:
                [UIImage imageNamed:@"redflag.png"]];
        UIPanGestureRecognizer* pan = [[UIPanGestureRecognizer alloc]
                                       initWithTarget:self
                                       action:@selector(dragging:)];
        [flag addGestureRecognizer:pan];
        flag.userInteractionEnabled = YES;
        UIScrollView* sv = self.sv;
        CGPoint p = sv.contentOffset;
        CGRect f = flag.frame;
        f.origin = p;
        f.origin.x -= flag.bounds.size.width;
        flag.frame = f;
        [sv addSubview: flag];
        // thanks for the flag, now stop operating altogether
        g.enabled = NO;
        [UIView animateWithDuration:0.25 animations:^{
            CGRect f = flag.frame;
            f.origin.x = p.x;
            flag.frame = f;
        }]; 
    }
}

UIScrollView 性能优化


  • 能不透明的内容,尽量不透明。

  • 如果你需要绘制一个阴影,不要让系统为这个图层计算阴影的路径现状,你可以设置shadowPath,或者使用Core Graphics 来创建一个阴影来绘制。同样的,避免让视图图层的阴影与其它图层的透明度叠加在一起;例如,如果图层背景是白色的,不透明地绘制时本身会就在这个白色背景上绘制了阴影。

  • 不要让绘制系统为你拉伸你的图片,你应该为当前的屏幕分辨率提供合适大小的图片。

  • 在紧要关头,你可以设置图层的shouldRasterize属性为YES,来消除渲染操作的海量样本。你可以在滚动时设置这个shouldRasterize属性为YES,当滚动结束时又恢复成NO。

苹果官方也说,可以设置一个视图的clearsContextBeforeDrawing属性为NO,这样可能造成一点不同,但是具体没有验证会有什么不同或者是优化。

原文地址:https://www.cnblogs.com/YungMing/p/4346772.html