Как использовать AutoLayout для размещения UIButtons в горизонтальных линиях (обтекание, выравнивание по левому краю)?

Мне нужно программно создать пару UIButtons с различной шириной в моем приложении (iOS 6.0 и выше).

Я хочу отображать кнопки в стиле "обтекания": начиная с левого края, каждая кнопка должна располагаться рядом друг с другом по горизонтали (в определенном порядке), и если кнопка не помещается в текущую "строку" , он должен начинать новую строку с левого края ниже предыдущей строки.

Примечание. Мне не нужна таблица/сетка, так как кнопки имеют разную ширину, и я хочу, чтобы они располагались рядом друг с другом.

пример макета

Я мог бы вручную рассчитать рамку каждой кнопки в своем коде, но должен ли я вместо этого использовать AutoLayout (с программно созданными ограничениями NSLayoutConstraints)? Как именно мне нужно настроить его?

РЕДАКТИРОВАТЬ: после прочтения главы 4 «Промежуточный автоматический макет» "iOS 6 by Tutorials" Я не уверен, что с помощью чистого AutoLayout можно реализовать эту требуемую мне функцию "обтекания".


person thomers    schedule 06.11.2013    source источник
comment
Я предлагаю вам прочитать эту статью. raywenderlich.com/20881   -  person Igor    schedule 06.11.2013
comment
@Igor в этой статье вообще не упоминаются программно созданные ограничения NSLayout! (Но это напомнило мне, что у меня есть iOS 6 By Tutorials, в которой есть глава Intermediate Auto Layout, которая должна охватывать основы — так что все равно спасибо ;-))   -  person thomers    schedule 06.11.2013


Ответы (3)


Мое текущее решение выглядит так: нет AutoLayout, но вручную устанавливаются правильные ограничения для каждого случая (первая кнопка, крайняя левая кнопка в новой строке, любая другая кнопка).

(Я предполагаю, что установка рамки для каждой кнопки напрямую приведет к более читаемому коду, чем использование NSLayoutConstraints)

NSArray *texts = @[ @"A", @"Short", @"Button", @"Longer Button", @"Very Long Button", @"Short", @"More Button", @"Any Key"];

int indexOfLeftmostButtonOnCurrentLine = 0;
NSMutableArray *buttons = [[NSMutableArray alloc] init];
float runningWidth = 0.0f;
float maxWidth = 300.0f;
float horizontalSpaceBetweenButtons = 10.0f;
float verticalSpaceBetweenButtons = 10.0f;

for (int i=0; i<texts.count; i++) {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button setTitle:[texts objectAtIndex:i] forState:UIControlStateNormal];
    [button sizeToFit];
    button.translatesAutoresizingMaskIntoConstraints = NO;

    [self.view addSubview:button];

    // check if first button or button would exceed maxWidth
    if ((i == 0) || (runningWidth + button.frame.size.width > maxWidth)) {
        // wrap around into next line
        runningWidth = button.frame.size.width;

        if (i== 0) {
            // first button (top left)
            // horizontal position: same as previous leftmost button (on line above)
            NSLayoutConstraint *horizontalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0f constant:horizontalSpaceBetweenButtons];
            [self.view addConstraint:horizontalConstraint];

            // vertical position:
            NSLayoutConstraint *verticalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop              multiplier:1.0f constant:verticalSpaceBetweenButtons];
            [self.view addConstraint:verticalConstraint];


        } else {
            // put it in new line
            UIButton *previousLeftmostButton = [buttons objectAtIndex:indexOfLeftmostButtonOnCurrentLine];

            // horizontal position: same as previous leftmost button (on line above)
            NSLayoutConstraint *horizontalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:previousLeftmostButton attribute:NSLayoutAttributeLeft multiplier:1.0f constant:0.0f];
            [self.view addConstraint:horizontalConstraint];

            // vertical position:
            NSLayoutConstraint *verticalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:previousLeftmostButton attribute:NSLayoutAttributeBottom multiplier:1.0f constant:verticalSpaceBetweenButtons];
            [self.view addConstraint:verticalConstraint];

            indexOfLeftmostButtonOnCurrentLine = i;
        }
    } else {
        // put it right from previous buttom
        runningWidth += button.frame.size.width + horizontalSpaceBetweenButtons;

        UIButton *previousButton = [buttons objectAtIndex:(i-1)];

        // horizontal position: right from previous button
        NSLayoutConstraint *horizontalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:previousButton attribute:NSLayoutAttributeRight multiplier:1.0f constant:horizontalSpaceBetweenButtons];
        [self.view addConstraint:horizontalConstraint];

        // vertical position same as previous button
        NSLayoutConstraint *verticalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:previousButton attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
        [self.view addConstraint:verticalConstraint];
    }

    [buttons addObject:button];
}
person thomers    schedule 06.11.2013
comment
Я бы не стал устанавливать рамки для представлений напрямую, когда вы используете автоматическую компоновку. - person Abizern; 06.11.2013
comment
Да, я имел в виду установку фреймов напрямую, без использования AutoLayout. - person thomers; 06.11.2013
comment
Спасибо за это, но у вас есть ошибка на verticalConstraint для первой кнопки? Разве вы не должны установить верхнюю часть кнопки в верхнюю часть содержащего представления, а не влево? - person mohrtan; 07.10.2014
comment
Мортан, спасибо, исправил. (Код здесь не на 100% соответствует тому, что я делал в реальном приложении, поэтому кажется, что эта ошибка возникла из-за копирования/вставки.) - person thomers; 08.10.2014

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

Он также лучше справляется с макетами при вращении.

person Abizern    schedule 06.11.2013
comment
Спасибо, я не думал об использовании CollectionView, но я не думаю, что с ним можно реализовать этот макет Flow/Wrap Around, где все элементы коллекции (кнопки) выровнены по левому краю и имеют разное расстояние до правого края. . - person thomers; 06.11.2013
comment
Вы можете определить свой макет в представлении коллекции по своему усмотрению. При желании можно разложить вещи по кругу. - person Abizern; 06.11.2013
comment
Я добавил изображение нужного мне макета. Возможно ли это с CollectionView? (Думаю, мне придется реализовать/подкласс UICollectionViewLayout - не уверен, что это стоит усилий только для 3-10 кнопок или около того) - person thomers; 06.11.2013
comment
Это не усилие, если оно для этого предназначено. - person Abizern; 06.11.2013
comment
Знаете ли вы, что мне нужно сделать, чтобы правый край плавал (чтобы все крайние правые элементы находились на разном расстоянии от правого края представления, как на моем изображении)? - person thomers; 06.11.2013
comment
Мне нужно сделать то же самое, я пытаюсь сделать это с представлением коллекции, но застреваю на том, чтобы сделать ширину ячейки динамической, используя автомакет, а также высоту представления коллекции (чтобы предотвратить любую прокрутку). @Abizern, у вас есть какой-нибудь пример кода того, как это можно сделать с помощью представления коллекции? - person Ash; 13.02.2014

Вот еще один пример того, как мы можем реализовать обтекание макета с помощью автоматического макета:

    @interface SCHorizontalWrapView : UIView
        @property(nonatomic)NSMutableArray *wrapConstrains;
    @end


@implementation SCHorizontalWrapView {
    CGFloat intrinsicHeight;
    BOOL updateConstraintsCalled;
}

-(id)init {
    self = [super init];
    if (self) {
        [UIView autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisVertical];
            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisHorizontal];
            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisHorizontal];
            [self autoSetContentCompressionResistancePriorityForAxis:ALAxisVertical];
        }];
    }
    return self;
}

-(void)updateConstraints {
    if (self.needsUpdateConstraints) {
        if (updateConstraintsCalled == NO) {
            updateConstraintsCalled = YES;
            [self updateWrappingConstrains];
            updateConstraintsCalled = NO;
        }

        [super updateConstraints];
    }
}

-(NSMutableArray *)wrapConstrains {
    if (_wrapConstrains == nil) {
        _wrapConstrains = [NSMutableArray new];

    }
    return _wrapConstrains;
}

-(CGSize)intrinsicContentSize {
    return CGSizeMake(UIViewNoIntrinsicMetric, intrinsicHeight);
}

-(void)setViews:(NSArray*)views {
    if (self.wrapConstrains.count > 0) {
        [UIView autoRemoveConstraints:self.wrapConstrains];
        [self.wrapConstrains removeAllObjects];
    }

    NSArray *subviews = self.subviews;
    for (UIView *view in subviews) {
        [view removeFromSuperview];
    }
    for (UIView *view in views) {
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:view];
        CGFloat leftPadding = 0;
        [view autoSetDimension:ALDimensionWidth toSize:CGRectGetWidth(self.frame) - leftPadding relation:NSLayoutRelationLessThanOrEqual];
    }
}


-(void)updateWrappingConstrains {

    NSArray *subviews = self.subviews;
    UIView *previewsView = nil;
    CGFloat leftOffset = 0;
    CGFloat itemMargin = 5;
    CGFloat topPadding = 0;
    CGFloat itemVerticalMargin = 5;
    CGFloat currentX = leftOffset;
    intrinsicHeight = topPadding;
    int lineIndex = 0;
    for (UIView *view in subviews) {
        CGSize size = view.intrinsicContentSize;
        if (previewsView) {
            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topPadding relation:NSLayoutRelationGreaterThanOrEqual]];
            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:leftOffset relation:NSLayoutRelationGreaterThanOrEqual]];

            CGFloat width = size.width;
            currentX += itemMargin;
            if (currentX + width <= CGRectGetWidth(self.frame)) {
                [self.wrapConstrains addObject:[view autoConstrainAttribute:ALEdgeLeading toAttribute:ALEdgeTrailing ofView:previewsView withOffset:itemMargin relation:NSLayoutRelationEqual]];
                [self.wrapConstrains addObject:[view autoAlignAxis:ALAxisBaseline toSameAxisOfView:previewsView]];
                currentX += size.width;
            }else {
                [self.wrapConstrains addObject: [view autoConstrainAttribute:ALEdgeTop toAttribute:ALEdgeBottom ofView:previewsView withOffset:itemVerticalMargin relation:NSLayoutRelationGreaterThanOrEqual]];
                currentX = leftOffset + size.width;
                intrinsicHeight += size.height + itemVerticalMargin;
                lineIndex++;
            }

        }else {
            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topPadding relation:NSLayoutRelationEqual]];
            [self.wrapConstrains addObject:[view autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:leftOffset relation:NSLayoutRelationEqual]];
            intrinsicHeight += size.height;
            currentX += size.width;
        }



        [view setNeedsUpdateConstraints];
        [view updateConstraintsIfNeeded];
        [view setNeedsLayout];
        [view layoutIfNeeded];

        previewsView = view;

    }
    [self invalidateIntrinsicContentSize];
}
@end

Здесь я использую PureLayout для определения ограничений.

Вы можете использовать этот класс следующим образом:

SCHorizontalWrapView *wrappingView = [[SCHorizontalWrapView alloc] initForAutoLayout];
//parentView is some view
[parentView addSubview:wrappingView];


[tagsView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:padding];
[tagsView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:padding];
[tagsView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:locationView withOffset:padding relation:NSLayoutRelationGreaterThanOrEqual];
[tagsView setNeedsLayout];
[tagsView layoutIfNeeded];
[tagsView setNeedsUpdateConstraints];
[tagsView updateConstraintsIfNeeded];
NSMutableArray *views = [NSMutableArray new];
//texts is some array of nsstrings
for (NSString *text in texts) {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.translatesAutoresizingMaskIntoConstraints = NO;
    [button setTitle:text forState:UIControlStateNormal];
    button.backgroundColor = [UIColor lightGrayColor];
    [views addObject:button];
}
[tagsView setViews:views];
person hsafarya    schedule 15.03.2014