如何实现执行 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?的主要内容,如果未能解决你的问题,请参考以下文章

Java中如何实现字符串反转?

如何用JS实现字符串反转

如何对 XAML 绑定值执行计算:反转、相乘、减去或相加?

字符串处理:如何实现字符串的反转

如何实现字符串的反转及替换?

字符串如何实现反转?python实现