Как реализовать UIView, который выполняет инверсию contentStretch? - PullRequest
2 голосов
/ 29 февраля 2012

Свойство UIView contentStretch определяет область, внутри которой растягивается содержимое для заполнения представления. Это здорово и все, но мне нужно прямо противоположное: я хочу представление, в котором я могу определить прямоугольник, который не растягивается, но растягивает внешние края, пока они не заполнят представление.

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

Таким образом, в приведенном выше представлении внешний прямоугольник является моим предложенным видом. Внутренний прямоугольник - это не растягиваемый контент. В идеале я хотел бы иметь возможность расположить центральную точку нерастягиваемого содержимого в любом месте внешнего прямоугольника и по-прежнему заполнять внешние биты до края.

Пример сценария использования: черное наложение с прозрачной центральной "дырой", которая следует за мышью / касанием для исследования сцены с фонариком и т. Д.

Один из способов сделать это - нарисовать содержимое UIView, а затем нарисовать четыре других вида, соответствующих размеру, чтобы покрыть внешнюю область, но я надеялся на решение с одним видом для более плавной анимации. Я предполагаю, что мне нужно иметь один UIView и использовать Core Animation, чтобы выйти из игры и связываться со своими слоями?

Ответы [ 2 ]

2 голосов
/ 29 февраля 2012

Так что моя первая попытка (в моем другом ответе на этот вопрос), делать все в drawRect:, была немного запаздывающей на моем iPad 2. Я решил повторить его, создав отдельный CALayer для каждого среза, и пусть Основная анимация беспокоится о масштабировании фрагментов.

В этой версии работа выполняется в методе 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. Она может очень хорошо работать и на старых устройствах.

Эта версия моего тестового проекта тоже на github .

1 голос
/ 29 февраля 2012

Предположительно, вам не понадобятся четыре других вида, вам понадобится восемь.Внутренний «фиксированный» прямоугольник делит вид на девять срезов:

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

Обратите внимание, что для каждого из девяти срезов может потребоваться различная комбинация горизонтального и вертикального масштабирования.В конечном итоге это выглядит так:

outside stretching demo

В любом случае, я подумал, что это не будет слишком сложно сделать, просто создав подкласс UIView, который переопределяет 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 в изображениикоординаты и CGRect в координатах вида, и рисует указанную часть изображения в указанной части вида.Я использую функции клипа и CTM в Quartz 2D для выполнения «тяжелой работы»:

- (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 из массива двух координат X и массивас двумя координатами Y:

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.

Эта версия моего тестового проекта на github .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...