UIScrollView масштабируется из представления с именем -ve

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

Я реализовал Zoom в этом scrollview.

Когда Масштабирование вида прокрутки отрегулирует размер представления Zoomable в соответствии с масштабом. НО ЭТО НЕ РЕГУЛИРУЕТ ПРОИСХОЖДЕНИЕ.

Итак, если у меня есть представление с фреймом {0, -500}, {1000, 1000}

Я уменьшаю масштаб до 0,5, это даст мне новый фрейм {0, -500}, {500, 500}

Ясно, что это не очень хорошо, весь вид увеличен из scrollview. Я хочу, чтобы кадр был {0, -250}, {500, 500}

Я могу немного поработать в методе scrollViewDidZoom, правильно отрегулировав исходное положение. Это работает, но зум не является гладким. Изменение места происшествия приводит к его переходу.

В документации для UIView я замечаю (относительно свойства frame):

Предупреждение. Если свойство преобразования не является преобразованием идентичности, значение этого свойства undefined, и поэтому его следует игнорировать.

Не совсем понятно, почему это так.

Я подхожу к этой проблеме неправильно? Каков наилучший способ его исправить?

Спасибо


Ниже приведен некоторый исходный код из тестового приложения, которое я использую:

В ViewController..

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.bigView = [[BigView alloc] initWithFrame: CGRectMake(0, -400, 1000, 1000)];

    [self.bigScroll addSubview: bigView];
    self.bigScroll.delegate = self;
    self.bigScroll.minimumZoomScale = 0.2;
    self.bigScroll.maximumZoomScale = 5;
    self.bigScroll.contentSize = bigView.bounds.size;
}

-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView  {
    return bigView;
}

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {    
//    bigView.frame = CGRectMake(0, -400 * scrollView.zoomScale,
//                               bigView.frame.size.width, bigView.frame.size.height);

    bigView.center = CGPointMake(500 * scrollView.zoomScale, 100 * scrollView.zoomScale);
}

И затем в представлении...

- (void)drawRect:(CGRect)rect
{
    // Drawing code
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextFillRect(ctx, CGRectMake(100, 500, 10, 10));

    for (int i = 0; i < 1000; i += 100) {
        CGContextStrokeRect(ctx, CGRectMake(0, i, 1000, 3));        
    }
}

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

Ответ 1

Вам не нужно использовать свойство frame - и не должно, поскольку Apple очень твердое предупреждение. В таких случаях вы обычно можете использовать bounds и center для достижения вашего результата.

В вашем случае вы можете игнорировать все свойства subview. Предполагая, что ваш subview является viewForZoomingInScrollView, вы можете использовать свойства scrollView contentOffset и zoomScale

- (void) setMinOffsets:(UIScrollView*)scrollView
    {
        CGFloat minOffsetX = MIN_OFFSET_X*scrollView.zoomScale;
        CGFloat minOffsetY = MIN_OFFSET_Y*scrollView.zoomScale;

        if ( scrollView.contentOffset.x < minOffsetX
          || scrollView.contentOffset.y < minOffsetY ) {

            CGFloat offsetX = (scrollView.contentOffset.x > minOffsetX)?
                               scrollView.contentOffset.x : minOffsetX;

            CGFloat offsetY = (scrollView.contentOffset.y > minOffsetY)?
                               scrollView.contentOffset.y : minOffsetY;

            scrollView.contentOffset = CGPointMake(offsetX, offsetY);
        }
    }

Вызовите его из scrollViewDidScroll и scrollViewDidZoom в свой делегат scrollView. Это должно работать гладко, но если у вас есть сомнения, вы также можете реализовать его, подклассифицируя scrollView и вызывая его с помощью layoutSubviews. В своем примере с PhotoScroller Apple центрирует контент scrollView, переопределяя layoutSubviews - хотя безумно они игнорируют свои собственные предупреждения и настраивают свойство subview frame для достижения этого.

Обновление

Вышеупомянутый метод устраняет "отскок", поскольку scrollView достигает предела. Если вы хотите сохранить отскок, вы можете напрямую изменить свойство центра просмотра:

- (void) setViewCenter:(UIScrollView*)scrollView
    {
        UIView* view = [scrollView subviews][0];
        CGFloat centerX = view.bounds.size.width/2-MIN_OFFSET_X;
        CGFloat centerY = view.bounds.size.height/2-MIN_OFFSET_Y;

        centerX *=scrollView.zoomScale;
        centerY *=scrollView.zoomScale;

        view.center = CGPointMake(centerX, centerY);
    }

update 2

Из вашего обновленного вопроса (с кодом), я вижу, что ни одно из этих решений не устраняет проблему. То, что, кажется, происходит, состоит в том, что чем больше вы делаете свое смещение, тем резче становится движение зума. С смещением 100 точек действие все еще довольно плавное, но со смещением 500 точек оно неприемлемо грубо. Это частично связано с вашей процедурой drawRect и частично связано с (слишком большим) перерасчётом, происходящим в scrollView для отображения правильного содержимого. Поэтому у меня есть другое решение...

В вашем диспетчере viewController установите для параметра customView bounds/frame origin значение normal (0,0). Вместо этого мы будем компенсировать контент, используя слои. Вам нужно будет добавить структуру QuartzCore в свой проект и #import его в пользовательское представление.

В пользовательском представлении инициализируйте два CAShapeLayers - один для поля, другой для строк. Если они имеют один и тот же уровень заполнения и инсульта, вам нужен только один CAShapeLayer (для этого примера я изменил цвета заливки и штриха). Каждый CAShapeLayer поставляется с собственным CGContext, который вы можете инициализировать один раз на слой с цветами, ширинами линий и т.д. Затем, чтобы сделать CAShapelayer, сделать все, что вам нужно сделать, это установить его свойство path с помощью CGPath.

#import "CustomView.h"
#import <QuartzCore/QuartzCore.h>

@interface CustomView()
@property (nonatomic, strong) CAShapeLayer* shapeLayer1;
@property (nonatomic, strong) CAShapeLayer* shapeLayer2;
@end

@implementation CustomView

    #define MIN_OFFSET_X 100
    #define MIN_OFFSET_Y 500

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initialiseLayers];
    }
    return self;
}


- (void) initialiseLayers
{
    CGRect layerBounds  = CGRectMake( MIN_OFFSET_X,MIN_OFFSET_Y
                          , self.bounds.size.width + MIN_OFFSET_X
                          , self.bounds.size.height+ MIN_OFFSET_Y);

    self.shapeLayer1 = [[CAShapeLayer alloc] init];
    [self.shapeLayer1 setFillColor:[UIColor clearColor].CGColor];
    [self.shapeLayer1 setStrokeColor:[UIColor yellowColor].CGColor];
    [self.shapeLayer1 setLineWidth:1.0f];
    [self.shapeLayer1 setOpacity:1.0f];

    self.shapeLayer1.anchorPoint = CGPointMake(0, 0);
    self.shapeLayer1.bounds = layerBounds;
    [self.layer addSublayer:self.shapeLayer1];

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

В отличие от представлений, суперслое не автоматически обрезает содержимое подслоев, которые лежат вне его границ. Вместо этого суперслое позволяет по умолчанию отображать его подслои в полном объеме.
(Apple Docs, построение иерархии слоев)

    self.shapeLayer2 = [[CAShapeLayer alloc] init];

    [self.shapeLayer2 setFillColor:[UIColor blueColor].CGColor];
    [self.shapeLayer2 setStrokeColor:[UIColor clearColor].CGColor];
    [self.shapeLayer2 setLineWidth:0.0f];
    [self.shapeLayer2 setOpacity:1.0f];

    self.shapeLayer2.anchorPoint = CGPointMake(0, 0);
    self.shapeLayer2.bounds = layerBounds;
    [self.layer addSublayer:self.shapeLayer2];

    [self drawIntoLayer1];
    [self drawIntoLayer2];
}

Задайте путь безье для каждого слоя формы, затем передайте его в:

- (void) drawIntoLayer1 {

    UIBezierPath* path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointMake(0,0)];

    for (int i = 0; i < self.bounds.size.height+MIN_OFFSET_Y; i += 100) {
        [path moveToPoint:
                CGPointMake(0,i)];
        [path addLineToPoint:
                CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i)];
        [path addLineToPoint:
                CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i+3)];
        [path addLineToPoint:
                CGPointMake(0, i+3)];
        [path closePath];
    }

    [self.shapeLayer1 setPath:path.CGPath];
}

- (void) drawIntoLayer2 {
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:
            CGRectMake(100+MIN_OFFSET_X, MIN_OFFSET_Y, 10, 10)];
    [self.shapeLayer2 setPath:path.CGPath];
}

Это устраняет необходимость в drawRect - вам нужно только перерисовать свои слои, если вы измените свойство пути. Даже если вы измените свойство пути так часто, как вы бы назвали drawRect, рисунок теперь должен быть значительно более эффективным. И поскольку path является анимированным свойством, вы также получаете анимацию, загружаемую бесплатно, если вам это нужно.

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

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