Meniru perilaku aspek-fit menggunakan batasan AutoLayout di Xcode 6

336

Saya ingin menggunakan AutoLayout untuk mengukur dan mengatur tampilan dengan cara yang mengingatkan pada mode konten fit-aspek UIImageView.

Saya memiliki subview di dalam tampilan wadah di Interface Builder. Subview ini memiliki beberapa aspek rasio bawaan yang ingin saya hormati. Ukuran tampilan kontainer tidak diketahui sampai runtime.

Jika rasio aspek tampilan wadah lebih lebar dari subview, maka saya ingin tinggi subview sama dengan tinggi tampilan induk.

Jika rasio aspek tampilan wadah lebih tinggi dari tampilan subview, maka saya ingin lebar tampilan subview sama dengan lebar tampilan induk.

Dalam kedua kasus saya berharap subview dipusatkan secara horizontal dan vertikal dalam tampilan wadah.

Apakah ada cara untuk mencapai ini menggunakan batasan AutoLayout di Xcode 6 atau di versi sebelumnya? Idealnya menggunakan Interface Builder, tetapi jika tidak mungkin adalah mungkin untuk mendefinisikan kendala seperti itu secara terprogram.

chris838
sumber

Jawaban:

1046

Anda tidak menggambarkan skala-untuk-pas; Anda menggambarkan aspek-fit. (Saya telah mengedit pertanyaan Anda dalam hal ini.) Subview menjadi sebesar mungkin dengan tetap mempertahankan rasio aspek dan sepenuhnya masuk ke dalam induknya.

Bagaimanapun, Anda dapat melakukan ini dengan tata letak otomatis. Anda dapat melakukannya sepenuhnya di IB pada Xcode 5.1. Mari kita mulai dengan beberapa tampilan:

beberapa pandangan

Tampilan hijau muda memiliki rasio aspek 4: 1. Tampilan hijau gelap memiliki rasio aspek 1: 4. Saya akan mengatur batasan sehingga tampilan biru memenuhi bagian atas layar, tampilan merah muda memenuhi bagian bawah layar, dan setiap tampilan hijau melebar sebanyak mungkin dengan tetap mempertahankan rasio aspek dan menyesuaikannya dengan wadah.

Pertama, saya akan membuat batasan pada keempat sisi tampilan biru. Saya akan menempelkannya ke tetangga terdekat di setiap tepi, dengan jarak 0. Saya pastikan untuk mematikan margin:

kendala biru

Perhatikan bahwa saya belum memperbarui bingkai. Saya merasa lebih mudah untuk meninggalkan ruang di antara tampilan ketika membuat batasan, dan hanya mengatur konstanta ke 0 (atau apa pun) dengan tangan.

Selanjutnya, saya jepit tepi kiri, bawah, dan kanan tampilan pink ke tetangga terdekat. Saya tidak perlu mengatur batasan tepi atas karena tepi atasnya sudah dibatasi ke tepi bawah tampilan biru.

kendala merah muda

Saya juga membutuhkan batasan ketinggian yang sama antara tampilan pink dan biru. Ini akan membuat mereka masing-masing mengisi setengah layar:

masukkan deskripsi gambar di sini

Jika saya memberi tahu Xcode untuk memperbarui semua frame sekarang, saya mendapatkan ini:

kontainer diletakkan

Jadi kendala yang saya buat sejauh ini benar. Saya membatalkan itu dan mulai bekerja pada tampilan hijau muda.

Menyesuaikan aspek tampilan hijau muda membutuhkan lima kendala:

  • Kendala rasio aspek prioritas yang diperlukan pada tampilan hijau muda. Anda dapat membuat batasan ini dalam xib atau storyboard dengan Xcode 5.1 atau yang lebih baru.
  • Batasan prioritas yang diperlukan membatasi lebar tampilan hijau muda menjadi kurang dari atau sama dengan lebar wadahnya.
  • Batasan prioritas tinggi yang mengatur lebar tampilan hijau muda menjadi sama dengan lebar wadahnya.
  • Batasan prioritas yang diperlukan membatasi ketinggian tampilan hijau muda menjadi kurang dari atau sama dengan ketinggian wadahnya.
  • Batasan prioritas tinggi yang mengatur ketinggian tampilan hijau muda menjadi sama dengan ketinggian wadahnya.

Mari kita pertimbangkan dua batasan lebar. Batasan yang kurang dari atau sama dengan sendirinya, tidak cukup untuk menentukan lebar tampilan hijau terang; banyak lebar akan sesuai dengan kendala. Karena ada ambiguitas, autolayout akan mencoba memilih solusi yang meminimalkan kesalahan pada kendala lain (prioritas tinggi tetapi tidak diperlukan). Meminimalkan kesalahan berarti membuat lebar sedekat mungkin dengan lebar wadah, sementara tidak melanggar batasan yang diperlukan kurang dari atau sama.

Hal yang sama terjadi dengan batasan ketinggian. Dan karena batasan aspek-rasio juga diperlukan, ia hanya dapat memaksimalkan ukuran subview sepanjang satu sumbu (kecuali wadah kebetulan memiliki rasio aspek yang sama dengan subview).

Jadi pertama-tama saya membuat batasan rasio aspek:

aspek atas

Lalu saya membuat batasan lebar dan tinggi yang sama dengan wadah:

ukuran sama atas

Saya perlu mengedit batasan ini menjadi batasan yang kurang dari atau sama dengan:

top kurang dari atau sama ukuran

Selanjutnya saya perlu membuat set lain dengan lebar dan tinggi sama kendala dengan wadah:

ukuran sama lagi

Dan saya perlu membuat batasan baru ini kurang dari prioritas yang disyaratkan:

sama dengan atas tidak diperlukan

Akhirnya, Anda meminta subview dipusatkan di wadahnya, jadi saya akan membuat batasan-batasan itu:

berpusat atas

Sekarang, untuk menguji, saya akan memilih controller tampilan dan meminta Xcode untuk memperbarui semua frame. Inilah yang saya dapatkan:

tata letak atas salah

Ups! Subview telah diperluas untuk sepenuhnya mengisi wadahnya. Jika saya pilih, saya dapat melihat bahwa sebenarnya itu dipertahankan rasio aspek, tapi itu melakukan aspect- mengisi bukan sebuah aspect- fit .

Masalahnya adalah bahwa pada kendala yang kurang dari atau sama, itu penting pandangan mana yang ada di setiap ujung kendala, dan Xcode telah mengatur kendala yang berlawanan dari harapan saya. Saya dapat memilih masing-masing dari dua kendala dan membalikkan item pertama dan kedua. Sebagai gantinya, saya hanya akan memilih subview dan mengubah batasan menjadi lebih dari-atau-sama:

memperbaiki kendala teratas

Xcode memperbarui tata letak:

tata letak atas yang benar

Sekarang saya melakukan semua hal yang sama dengan tampilan hijau gelap di bagian bawah. Saya perlu memastikan rasio aspeknya adalah 1: 4 (Xcode mengubah ukurannya dengan cara yang aneh karena tidak memiliki kendala). Saya tidak akan menunjukkan langkah lagi karena mereka sama. Inilah hasilnya:

tata letak atas dan bawah yang benar

Sekarang saya dapat menjalankannya di simulator iPhone 4S, yang memiliki ukuran layar berbeda dari IB yang digunakan, dan uji rotasi:

Tes iphone 4s

Dan saya dapat menguji di dalam iPhone 6 simulator:

tes iphone 6

Saya telah mengunggah storyboard terakhir saya ke intisari ini untuk kenyamanan Anda.

rob mayoff
sumber
27
Saya menggunakan LICEcap . Ini gratis (GPL) dan catatan langsung ke GIF. Selama GIF animasi tidak lebih dari 2 MB, Anda dapat mempostingnya ke situs ini seperti gambar lainnya.
rob mayoff
1
@LucaTorella Dengan hanya kendala prioritas tinggi, tata letaknya ambigu. Hanya karena mendapatkan hasil yang Anda inginkan hari ini tidak berarti itu akan ada di versi iOS berikutnya. Misalkan subview menginginkan rasio aspek 1: 1, superview berukuran 99 × 100, dan Anda hanya memiliki rasio aspek yang disyaratkan dan kendala kesetaraan prioritas tinggi. Kemudian tata letak otomatis dapat mengatur subview ke ukuran 99 × 99 (dengan total kesalahan 1) atau ukuran 100 × 100 (dengan total kesalahan 1). Satu ukuran sesuai dengan aspek; yang lainnya adalah aspek-isi. Menambahkan kendala yang diperlukan menghasilkan solusi kesalahan paling unik.
rob mayoff
1
@LucaTorella Terima kasih atas koreksi mengenai ketersediaan kendala rasio aspek.
rob mayoff
11
10.000 aplikasi lebih baik dengan hanya satu posting ... luar biasa.
DonVaughn
2
Wow .. Jawaban terbaik di sini di stack overflow yang pernah saya lihat .. Terima kasih untuk sir ini ..
0yeoj
24

Rob, jawaban Anda luar biasa! Saya juga tahu bahwa pertanyaan ini secara khusus tentang mencapai ini dengan menggunakan tata letak otomatis. Namun, hanya sebagai referensi, saya ingin menunjukkan bagaimana hal ini dapat dilakukan dalam kode. Anda mengatur tampilan atas dan bawah (biru dan merah muda) seperti yang ditunjukkan Rob. Kemudian Anda membuat custom AspectFitView:

AspectFitView.h :

#import <UIKit/UIKit.h>

@interface AspectFitView : UIView

@property (nonatomic, strong) UIView *childView;

@end

AspectFitView.m :

#import "AspectFitView.h"

@implementation AspectFitView

- (void)setChildView:(UIView *)childView
{
    if (_childView) {
        [_childView removeFromSuperview];
    }

    _childView = childView;

    [self addSubview:childView];
    [self setNeedsLayout];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (_childView) {
        CGSize childSize = _childView.frame.size;
        CGSize parentSize = self.frame.size;
        CGFloat aspectRatioForHeight = childSize.width / childSize.height;
        CGFloat aspectRatioForWidth = childSize.height / childSize.width;

        if ((parentSize.height * aspectRatioForHeight) > parentSize.height) {
            // whole height, adjust width
            CGFloat width = parentSize.width * aspectRatioForWidth;
            _childView.frame = CGRectMake((parentSize.width - width) / 2.0, 0, width, parentSize.height);
        } else {
            // whole width, adjust height
            CGFloat height = parentSize.height * aspectRatioForHeight;
            _childView.frame = CGRectMake(0, (parentSize.height - height) / 2.0, parentSize.width, height);
        }
    }
}

@end

Selanjutnya, Anda mengubah kelas tampilan biru dan merah muda di storyboard menjadi AspectFitViews. Akhirnya Anda mengatur dua outlet ke viewcontroller Anda topAspectFitViewdan bottomAspectFitViewdan mengaturnya childViewdi viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *top = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 100)];
    top.backgroundColor = [UIColor lightGrayColor];

    UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)];
    bottom.backgroundColor = [UIColor greenColor];

    _topAspectFitView.childView = top;
    _bottomAspectFitView.childView = bottom;
}

Jadi tidak sulit untuk melakukan ini dalam kode dan masih sangat mudah beradaptasi dan bekerja dengan tampilan yang bervariasi dan rasio aspek yang berbeda.

Perbarui Juli 2015 : Temukan aplikasi demo di sini: https://github.com/jfahrenkrug/SPWKAspectFitView

Johannes Fahrenkrug
sumber
1
@DanielGalasko Terima kasih, Anda benar. Saya menyebutkan hal itu dalam jawaban saya. Saya hanya berpikir itu akan menjadi referensi yang berguna untuk membandingkan langkah apa yang terlibat dalam kedua solusi.
Johannes Fahrenkrug
1
Sangat membantu untuk memiliki referensi terprogram juga.
ctpenrose
@JohannesFahrenkrug jika Anda memiliki proyek tutorial, dapatkah Anda berbagi? terima kasih :)
Erhan Demirci
1
@ErhanDemirci Tentu, ini dia: github.com/jfahrenkrug/SPWKAspectFitView
Johannes Fahrenkrug
8

Saya membutuhkan solusi dari jawaban yang diterima, tetapi dieksekusi dari kode. Cara paling elegan yang saya temukan adalah menggunakan kerangka Masonry .

#import "Masonry.h"

...

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(view.mas_height).multipliedBy(aspectRatio);
    make.size.lessThanOrEqualTo(superview);
    make.size.equalTo(superview).with.priorityHigh();
    make.center.equalTo(superview);
}];
Tomasz Bąk
sumber
5

Ini untuk macOS.

Saya memiliki masalah untuk menggunakan cara Rob untuk mencapai aspek fit pada aplikasi OS X. Tapi saya membuatnya dengan cara lain - Alih-alih menggunakan lebar dan tinggi, saya menggunakan ruang depan, belakang, atas dan bawah.

Pada dasarnya, tambahkan dua spasi utama di mana satu adalah> = 0 @ 1000 prioritas yang diperlukan dan yang lain adalah = 0 @ 250 prioritas rendah. Lakukan pengaturan yang sama untuk trailing, ruang atas dan bawah.

Tentu saja, Anda perlu mengatur rasio aspek dan pusat X dan pusat Y.

Dan kemudian pekerjaan selesai!

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

brianLikeApple
sumber
Bagaimana Anda bisa melakukan ini secara terprogram?
FateNuller
@FateNuller Hanya mengikuti langkah-langkahnya?
brianLikeApple
4

Saya mendapati diri saya menginginkan perilaku aspek- isi sehingga UIImageView akan mempertahankan rasio aspeknya sendiri dan sepenuhnya mengisi tampilan wadah. Yang membingungkan, UIImageView saya melanggar KEDUA prioritas tinggi sama dengan lebar dan tinggi sama kendala (dijelaskan dalam jawaban Rob) dan rendering pada resolusi penuh.

Solusinya adalah dengan menetapkan Prioritas Ketahanan Kompresi Konten UIImageView lebih rendah daripada prioritas kendala dengan lebar sama dan tinggi sama:

Ketahanan Kompresi Konten

Gregor
sumber
2

Ini adalah port jawaban @ rob_mayoff yang sangat baik untuk pendekatan kode-sentris, menggunakan NSLayoutAnchorobjek dan porting ke Xamarin. Bagi saya, NSLayoutAnchordan kelas terkait telah membuat AutoLayout lebih mudah diprogram:

public class ContentView : UIView
{
        public ContentView (UIColor fillColor)
        {
            BackgroundColor = fillColor;
        }
}

public class MyController : UIViewController 
{
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();

        //Starting point:
        var view = new ContentView (UIColor.White);

        blueView = new ContentView (UIColor.FromRGB (166, 200, 255));
        view.AddSubview (blueView);

        lightGreenView = new ContentView (UIColor.FromRGB (200, 255, 220));
        lightGreenView.Frame = new CGRect (20, 40, 200, 60);

        view.AddSubview (lightGreenView);

        pinkView = new ContentView (UIColor.FromRGB (255, 204, 240));
        view.AddSubview (pinkView);

        greenView = new ContentView (UIColor.Green);
        greenView.Frame = new CGRect (80, 20, 40, 200);
        pinkView.AddSubview (greenView);

        //Now start doing in code the things that @rob_mayoff did in IB

        //Make the blue view size up to its parent, but half the height
        blueView.TranslatesAutoresizingMaskIntoConstraints = false;
        var blueConstraints = new []
        {
            blueView.LeadingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.LeadingAnchor),
            blueView.TrailingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TrailingAnchor),
            blueView.TopAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TopAnchor),
            blueView.HeightAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.HeightAnchor, (nfloat) 0.5)
        };
        NSLayoutConstraint.ActivateConstraints (blueConstraints);

        //Make the pink view same size as blue view, and linked to bottom of blue view
        pinkView.TranslatesAutoresizingMaskIntoConstraints = false;
        var pinkConstraints = new []
        {
            pinkView.LeadingAnchor.ConstraintEqualTo(blueView.LeadingAnchor),
            pinkView.TrailingAnchor.ConstraintEqualTo(blueView.TrailingAnchor),
            pinkView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor),
            pinkView.TopAnchor.ConstraintEqualTo(blueView.BottomAnchor)
        };
        NSLayoutConstraint.ActivateConstraints (pinkConstraints);


        //From here, address the aspect-fitting challenge:

        lightGreenView.TranslatesAutoresizingMaskIntoConstraints = false;
        //These are the must-fulfill constraints: 
        var lightGreenConstraints = new []
        {
            //Aspect ratio of 1 : 5
            NSLayoutConstraint.Create(lightGreenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, lightGreenView, NSLayoutAttribute.Width, (nfloat) 0.20, 0),
            //Cannot be larger than parent's width or height
            lightGreenView.WidthAnchor.ConstraintLessThanOrEqualTo(blueView.WidthAnchor),
            lightGreenView.HeightAnchor.ConstraintLessThanOrEqualTo(blueView.HeightAnchor),
            //Center in parent
            lightGreenView.CenterYAnchor.ConstraintEqualTo(blueView.CenterYAnchor),
            lightGreenView.CenterXAnchor.ConstraintEqualTo(blueView.CenterXAnchor)
        };
        //Must-fulfill
        foreach (var c in lightGreenConstraints) 
        {
            c.Priority = 1000;
        }
        NSLayoutConstraint.ActivateConstraints (lightGreenConstraints);

        //Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
        var lightGreenLowPriorityConstraints = new []
         {
            lightGreenView.WidthAnchor.ConstraintEqualTo(blueView.WidthAnchor),
            lightGreenView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor)
        };
        //Lower priority
        foreach (var c in lightGreenLowPriorityConstraints) 
        {
            c.Priority = 750;
        }

        NSLayoutConstraint.ActivateConstraints (lightGreenLowPriorityConstraints);

        //Aspect-fit on the green view now
        greenView.TranslatesAutoresizingMaskIntoConstraints = false;
        var greenConstraints = new []
        {
            //Aspect ratio of 5:1
            NSLayoutConstraint.Create(greenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, greenView, NSLayoutAttribute.Width, (nfloat) 5.0, 0),
            //Cannot be larger than parent's width or height
            greenView.WidthAnchor.ConstraintLessThanOrEqualTo(pinkView.WidthAnchor),
            greenView.HeightAnchor.ConstraintLessThanOrEqualTo(pinkView.HeightAnchor),
            //Center in parent
            greenView.CenterXAnchor.ConstraintEqualTo(pinkView.CenterXAnchor),
            greenView.CenterYAnchor.ConstraintEqualTo(pinkView.CenterYAnchor)
        };
        //Must fulfill
        foreach (var c in greenConstraints) 
        {
            c.Priority = 1000;
        }
        NSLayoutConstraint.ActivateConstraints (greenConstraints);

        //Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
        var greenLowPriorityConstraints = new []
        {
            greenView.WidthAnchor.ConstraintEqualTo(pinkView.WidthAnchor),
            greenView.HeightAnchor.ConstraintEqualTo(pinkView.HeightAnchor)
        };
        //Lower-priority than above
        foreach (var c in greenLowPriorityConstraints) 
        {
            c.Priority = 750;
        }

        NSLayoutConstraint.ActivateConstraints (greenLowPriorityConstraints);

        this.View = view;

        view.LayoutIfNeeded ();
    }
}
Larry OBrien
sumber
1

Mungkin ini adalah jawaban terpendek, dengan Masonry , yang juga mendukung aspek-isi dan peregangan.

typedef NS_ENUM(NSInteger, ContentMode) {
    ContentMode_aspectFit,
    ContentMode_aspectFill,
    ContentMode_stretch
}

// ....

[containerView addSubview:subview];

[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    if (contentMode == ContentMode_stretch) {
        make.edges.equalTo(containerView);
    }
    else {
        make.center.equalTo(containerView);
        make.edges.equalTo(containerView).priorityHigh();
        make.width.equalTo(content.mas_height).multipliedBy(4.0 / 3); // the aspect ratio
        if (contentMode == ContentMode_aspectFit) {
            make.width.height.lessThanOrEqualTo(containerView);
        }
        else { // contentMode == ContentMode_aspectFill
            make.width.height.greaterThanOrEqualTo(containerView);
        }
    }
}];
Tuan Ming
sumber