如何实现执行contentStretch反转的UIView?

时间:2012-02-29 03:48:36

标签: ios animation uiview geometry

UIView contentStretch属性定义了一个区域,在该区域内拉伸内容以填充视图。这很好,但我发现自己需要完全相反的东西:我想要一个视图,我可以定义一个不伸展的矩形,但外边缘拉伸直到它们填满视图。

  +-------------------------------------------+
  |                                           |
  |         +----------+                      |
  |         |          |                      |
  |         |          |                      |
  |         |  Content |      Stretch to fill |
  |         |          |                      |
  |         |          |                      |
  |         +----------+                      |
  |                                           |
  +-------------------------------------------+

所以在上面的视图中,外部rect是我提出的视图。内部矩形是不可拉伸的内容。理想情况下,我希望能够将不可伸缩内容的中心点定位在外部矩形内的任何位置,并且仍然将外部位填充到边缘。

示例使用场景:带有透明中心“孔”的黑色覆盖图,跟随鼠标/触摸,用于使用手电筒等探测场景。

我认为这样做的一种方法是绘制内容UIView,然后绘制其他四个视图,大小适当,覆盖外部区域,但我希望有一个单视图解决方案,以实现更流畅的动画。我猜我需要有一个UIView并使用Core Animation来搞定它的图层?

2 个答案:

答案 0 :(得分:2)

所以我的第一次尝试(在我对这个问题的另一个答案中),在drawRect:中执行所有操作,在iPad 2上有点滞后。我决定通过创建单独的CALayer来重做它。每个切片让Core Animation担心缩放切片。

在此版本中,工作是在我的自定义layoutSubviews子类的UIView方法中完成的。只要视图的大小发生变化(包括视图首次出现时),系统就会调用layoutSubviews。我们还可以要求系统使用layoutSubviews消息来呼叫setNeedsLayout。系统自动合并布局请求,就像它合并绘制请求一样。

我需要三个私有实例变量:

@implementation OutstretchView {
    CALayer *_slices[3][3];
    BOOL _imageDidChange : 1;
    BOOL _hasSlices : 1;
}

我的layoutSubviews方法首先处理没有图片或没有fixedRect的简单案例:

- (void)layoutSubviews {
    [super layoutSubviews];

    if (!self.image) {
        [self hideSlices];
        self.layer.contents = nil;
        return;
    }

    if (CGRectIsNull(self.fixedRect)) {
        [self hideSlices];
        self.layer.contents = (__bridge id)self.image.CGImage;
        return;
    }

如果我还没有创建它们,那就懒得创建九个切片层:

    if (!_hasSlices)
        [self makeSlices];

如果图像已更改,则会重新计算切片图层的图像。 _imageDidChange标志设置在setImage:

    if (_imageDidChange) {
        [self setSliceImages];
        _imageDidChange = NO;
    }

最后,它设置九个切片层中每一个的帧。

    [self setSliceFrames];
}

当然,所有真正的工作都发生在辅助方法中,但它们非常简单。隐藏切片(当没有图像或没有fixedRect时)是微不足道的:

- (void)hideSlices {
    if (!_hasSlices)
        return;
    for (int y = 0; y < 3; ++y) {
        for (int x = 0; x < 3; ++x) {
            _slices[y][x].hidden = YES;
        }
    }
}

创建切片图层也很简单:

- (void)makeSlices {
    if (_hasSlices)
        return;
    CALayer *myLayer = self.layer;
    for (int y = 0; y < 3; ++y) {
        for (int x = 0; x < 3; ++x) {
            _slices[y][x] = [CALayer layer];
            [myLayer addSublayer:_slices[y][x]];
        }
    }
    _hasSlices = YES;
}

要制作切片图像并设置切片图层帧,我需要从其他答案中获取rect辅助函数:

static CGRect rect(CGFloat *xs, CGFloat *ys) {
    return CGRectMake(xs[0], ys[0], xs[1] - xs[0], ys[1] - ys[0]);
}

创建切片图像需要一些工作,因为我必须计算切片之间边界的坐标。我使用CGImageCreateWithImageInRect从用户提供的图像创建切片图像。

- (void)setSliceImages {
    UIImage *image = self.image;
    CGImageRef cgImage = image.CGImage;
    CGFloat scale = image.scale;
    CGRect fixedRect = self.fixedRect;
    fixedRect.origin.x *= scale;
    fixedRect.origin.y *= scale;
    fixedRect.size.width *= scale;
    fixedRect.size.height *= scale;
    CGFloat xs[4] = { 0, fixedRect.origin.x, CGRectGetMaxX(fixedRect), CGImageGetWidth(cgImage) };
    CGFloat ys[4] = { 0, fixedRect.origin.y, CGRectGetMaxY(fixedRect), CGImageGetHeight(cgImage) };

    for (int y = 0; y < 3; ++y) {
        for (int x = 0; x < 3; ++x) {
            CGImageRef imageSlice = CGImageCreateWithImageInRect(cgImage, rect(xs + x, ys + y));
            _slices[y][x].contents = (__bridge id)imageSlice;
            CGImageRelease(imageSlice);
        }
    }
}

设置切片图层帧类似,但我不必在此处理图像比例。 (也许我应该使用UIScreen比例?嗯。这很令人困惑。我没有在Retina设备上试过它。)

- (void)setSliceFrames {
    CGRect bounds = self.bounds;
    CGRect fixedRect = self.fixedRect;
    CGPoint fixedCenter = self.fixedCenter;
    fixedRect = CGRectOffset(fixedRect, fixedCenter.x - fixedRect.size.width / 2, fixedCenter.y - fixedRect.size.height / 2);
    CGFloat xs[4] = { bounds.origin.x, fixedRect.origin.x, CGRectGetMaxX(fixedRect), CGRectGetMaxX(bounds) };
    CGFloat ys[4] = { bounds.origin.y, fixedRect.origin.y, CGRectGetMaxY(fixedRect), CGRectGetMaxY(bounds) };

    for (int y = 0; y < 3; ++y) {
        for (int x = 0; x < 3; ++x) {
            _slices[y][x].frame = rect(xs + x, ys + y);
        }
    }
}

这个版本在iPad 2上看起来更顺畅。它在旧设备上也可以很好用。

我的测试项目的这个版本是on github too

答案 1 :(得分:1)

据推测,你不需要其他四个观点,你需要八个观点。内部“固定”矩形将视图划分为九个切片:

+----+-----+---------+
|    |     |         |
+----+-----+---------|
|    |fixed|         |
|    |     |         |
+----+-----+---------+
|    |     |         |
|    |     |         |
|    |     |         |
+----+-----+---------+

请注意,九个切片中的每个切片可能需要不同的水平和垂直缩放组合。最终看起来像这样:

outside stretching demo

无论如何,我认为仅仅通过创建覆盖UIView的{​​{1}}子类来做到这一点并不难。

我命名了我的子类drawRect:并给它了三个属性:

OutstretchView

@interface OutstretchView : UIView @property (nonatomic) UIImage *image; @property (nonatomic) CGRect fixedRect; // in image coordinates @property (nonatomic) CGPoint fixedCenter; // in view coordinates @end 属性定义了永远不应拉伸的图像部分。 fixedRect属性定义了视图中应该绘制部分图像的位置。

要实际绘制图像的九个切片,定义一个在图像坐标中采用fixedCenter并在视图坐标中采用CGRect并绘制图像的指定部分的方法会很有帮助在视图的指定部分。我使用Quartz 2D的剪辑和CTM功能来完成“繁重的工作”:

CGRect

我还将使用一个小辅助方法,从两个X坐标的数组和两个Y坐标的数组创建一个- (void)drawRect:(CGRect)imageRect ofImage:(UIImage *)image inRect:(CGRect)viewRect { CGContextRef gc = UIGraphicsGetCurrentContext(); CGContextSaveGState(gc); { CGContextClipToRect(gc, viewRect); CGContextTranslateCTM(gc, viewRect.origin.x, viewRect.origin.y); CGContextScaleCTM(gc, viewRect.size.width / imageRect.size.width, viewRect.size.height / imageRect.size.height); CGContextTranslateCTM(gc, -imageRect.origin.x, -imageRect.origin.y); [image drawAtPoint:CGPointZero]; } CGContextRestoreGState(gc); }

CGRect

有了这些助手,我可以实施static CGRect rect(CGFloat *xs, CGFloat *ys) { return CGRectMake(xs[0], ys[0], xs[1] - xs[0], ys[1] - ys[0]); } 。首先,我处理了无法绘制图像或没有定义drawRect:的简单情况:

fixedRect

然后我计算视图坐标中的矩形,其中将绘制图像的固定部分:

- (void)drawRect:(CGRect)dirtyRect {
    UIImage *image = self.image;
    if (!image)
        return;

    CGRect imageBounds = (CGRect){ CGPointZero, image.size };
    CGRect viewBounds = self.bounds;

    CGRect imageFixedRect = self.fixedRect;
    if (CGRectIsNull(imageFixedRect)) {
        [image drawInRect:viewBounds];
        return;
    }

接下来,我创建一个数组,其中包含四个(是,四个)X坐标,用于定义视图坐标空间中切片的边缘,以及一个用于Y坐标的类似数组,以及另外两个用于图像坐标空间中坐标的数组:

    CGPoint imageFixedCenter = self.fixedCenter;
    CGRect viewFixedRect = CGRectOffset(imageFixedRect, imageFixedCenter.x - imageFixedRect.size.width / 2, imageFixedCenter.y - imageFixedRect.size.height / 2);

最后是简单的部分,绘制切片:

    CGFloat viewSlicesX[4] = { viewBounds.origin.x, viewFixedRect.origin.x, CGRectGetMaxX(viewFixedRect), CGRectGetMaxX(viewBounds) };
    CGFloat viewSlicesY[4] = { viewBounds.origin.y, viewFixedRect.origin.y, CGRectGetMaxY(viewFixedRect), CGRectGetMaxY(viewBounds) };
    CGFloat imageSlicesX[4] = { imageBounds.origin.x, imageFixedRect.origin.x, CGRectGetMaxX(imageFixedRect), CGRectGetMaxX(imageBounds) };
    CGFloat imageSlicesY[4] = { imageBounds.origin.y, imageFixedRect.origin.y, CGRectGetMaxY(imageFixedRect), CGRectGetMaxY(imageBounds) };

我的iPad 2有点迟钝。

我的测试项目的这个版本是on github