如何实现执行 contentStretch 反转的 UIView?
Posted
技术标签:
【中文标题】如何实现执行 contentStretch 反转的 UIView?【英文标题】:How to implement UIView that performs inverse of contentStretch? 【发布时间】:2012-02-29 03:48:36 【问题描述】:UIView contentStretch 属性定义了一个区域,在该区域内内容被拉伸以填充视图。这很好,但我发现自己需要完全相反的东西:我想要一个视图,我可以定义一个不拉伸但外边缘拉伸直到填满视图的矩形。
+-------------------------------------------+
| |
| +----------+ |
| | | |
| | | |
| | Content | Stretch to fill |
| | | |
| | | |
| +----------+ |
| |
+-------------------------------------------+
所以在上面的视图中,外部矩形是我提出的视图。内部矩形是不可拉伸的内容。理想情况下,我希望能够将不可拉伸内容的中心点定位在外部矩形内的任何位置,并且仍然让外部位填充到边缘。
示例使用场景:带有透明中心“孔”的黑色覆盖层跟随鼠标/触摸,用于使用手电筒等探索场景。
我想这样做的一种方法是绘制内容 UIView,然后绘制其他四个视图,大小适当,以覆盖外部区域,但我希望有一个单视图解决方案来实现更流畅的动画效果。我猜我需要一个 UIView 并使用 Core Animation 来弄乱它的图层?
【问题讨论】:
哇,好问题...我的想法是伪造它。添加一个黑色叠加层,但有一个 UIView 跟随触摸,它实际上只是在黑色叠加层下方投影一个 imageView 的一部分。 但是您如何通过黑色覆盖层看到下面的部分?假设内容矩形具有透明位。 这是一个放大镜视图的链接,它接受 UIViews 作为要放大的内容。我还没有研究过实现,所以我不能提供细节,但它按承诺工作:github.com/acoomans/ios-MagnifyingGlass 【参考方案1】:所以我的第一次尝试(在我对这个问题的另一个回答中)在我的 iPad 2 上做所有事情在drawRect:
上有点滞后。我决定通过为每个切片创建一个单独的CALayer
来重做它并让Core Animation 担心缩放切片。
在这个版本中,工作是在我自定义的UIView
子类的layoutSubviews
方法中完成的。每当视图的大小发生变化时(包括视图首次出现时),系统都会调用layoutSubviews
。我们还可以要求系统使用setNeedsLayout
消息调用layoutSubviews
。系统会自动合并布局请求,就像合并绘图请求一样。
我需要三个私有实例变量:
@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。
【讨论】:
【参考方案2】:大概你不需要四个其他视图,你需要八个。内部“固定”矩形将视图分为九个切片:
+----+-----+---------+
| | | |
+----+-----+---------|
| |fixed| |
| | | |
+----+-----+---------+
| | | |
| | | |
| | | |
+----+-----+---------+
请注意,九个切片中的每一个都可能需要不同的水平和垂直缩放组合。它最终看起来像这样:
无论如何,我认为通过创建一个覆盖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
属性定义了应该在视图中的哪个位置绘制图像的一部分。
要实际绘制图像的九个切片,定义一个方法是有帮助的的看法。我使用 Quartz 2D 的剪辑和 CTM 功能来完成“繁重的工作”:
- (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 坐标的数组创建一个 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 上有点滞后。
我的测试项目的这个版本是on github。
【讨论】:
以上是关于如何实现执行 contentStretch 反转的 UIView?的主要内容,如果未能解决你的问题,请参考以下文章