如何实现执行contentStretch逆的UIView?

UIView contentStretch属性定义了内容被拉伸以填充视图的区域。 这是伟大的,但我发现自己需要的是完全相反的:我想要一个视图,我可以定义一个矩形不伸展,但有外边缘伸展,直到他们填充视图。

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

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

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

我想这样做的一个方法是绘制内容UIView,然后绘制其他四个视图,适当的大小,以覆盖外部区域,但我希望为一个单一的解决scheme更stream畅的animation。 我猜我需要有一个单一的UIView,并使用核心animation下来,混乱它的图层?

所以我第一次尝试(在这个问题的其他答案),在drawRect:做所有事情,在我的iPad 2上有点迟缓。我决定通过为每个片创build一个单独的CALayer来重做它,让Core Animation担心缩放切片。

在这个版本中,工作是在我自定义的UIView子类的layoutSubviews方法中完成的。 只要视图的大小发生变化(包括视图第一次出现时),系统就会调用layoutSubviews 。 我们也可以让系统使用setNeedsLayout消息调用layoutSubviews 。 系统自动合并布局请求,例如合并绘制请求。

我将需要三个私有实例variables:

 @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; } 

然后,如果我还没有创build它们,它懒洋洋地创build九个片层:

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

创build切片层也是微不足道的:

 - (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]); } 

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

 - (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上看起来更平滑。它也可能在旧设备上运行得非常好。

我的testing项目的这个版本也在github上 。

大概你不需要四个其他的意见,你需要八个。 内部的“固定”矩形将视图分成九个片段:

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

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

外面伸展演示

无论如何,我认为这并不难,只要创build一个覆盖drawRect:UIView子类即可。

我命名我的子类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和视图坐标中的CGRect一起使用,并在视图的指定部分中绘制指定的图像部分。 我使用Quartz 2D的剪辑和CTMfunction来执行“繁重工作”:

 - (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); } 

我还将使用一个小辅助方法,从两个X坐标数组和一个两个Y坐标数组创build一个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; } 

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

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

接下来,我制作了四个(是,四个)X坐标的数组,这些坐标定义了视图坐标空间中切片的边缘,Y坐标是类似的数组,另外两个坐标是图像坐标空间中的坐标:

  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) }; 

最后是容易的部分,绘制切片:

  for (int y = 0; y < 3; ++y) { for (int x = 0; x < 3; ++x) { [self drawRect:rect(imageSlicesX + x, imageSlicesY + y) ofImage:image inRect:rect(viewSlicesX + x, viewSlicesY + y)]; } } } 

这是我的iPad 2有点迟缓。

我的testing项目的这个版本在github上 。