MyLayout&TangramKit 的重大升級!

米米狗· 2019-05-20
本文來自 歐陽大哥2013 ,作者 米米狗

MyLayoutTangramKit是一套基于frame之上的UI界面布局庫的OC版本和Swift版本。目前最新版本升級為MyLayout1.7.0和TangramKit1.4.0。

OC1.7.0: github.com/youngsoft/M…

Swift1.4.0: github.com/youngsoft/T…

這次升級的主要目的是為了和AutoLayout結合的更加緊密。

這不是一篇推廣文,而是介紹AutoLayout和MyLayout&TangramKit是如何實現視圖尺寸自適應的以及二者是如何結合在一起的。所以希望您耐著性子繼續往下看

AutoLayout的尺寸自適應

AutoLayout中有兩種類型的尺寸自適應:一類是以UILabel和UITextView為代表視圖的尺寸自適應,這類視圖中的寬度和高度有時候需要根據自身內容來確定自己的寬度和高度。也就是說這類視圖有自己的固有內容尺寸(intrinsicContentSize)。在UIView類中提供了一個可供重載的方法:

- (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);

如果某類視圖有自己的固有內容尺寸則需要重載這個方法的實現。這個方法返回根據自身內容而計算出來的固有內容尺寸的size,如果沒有固有內容尺寸則方法返回一個特殊的默認值UIViewNoIntrinsicMetric(-1)。很明顯UIView類的返回值是默認值,而UILabel和UITextView這些類則重載了這個方法并返回了根據自身內容計算出來的尺寸。 當一個視圖有自己的固有內容尺寸時,就不需要再為視圖設置寬度或者高度約束。這也就是為什么一般情況下不對UILabel視圖設置寬度和高度約束時系統也能正常完成布局。系統內部的實現中如果布局引擎在布局時發現某個視圖沒有設置高度或者寬度約束那么就會去調用這個視圖的intrinsicContentSize方法,如果這個方法返回了正常的尺寸則視圖就按這個尺寸來進行渲染和展示,如果這個方法返回值的某個維度是UIViewNoIntrinsicMetric則表明某個維度也沒有固有內容尺寸從而實現約束缺失的現象。

另外一類是一些容器視圖的高度或者寬度希望根據其中的子視圖來確定。比如一些界面中有父視圖的尺寸由子視圖的尺寸來確定的;還比如UIScrollView中為了能實現滾動需要根據添加到里面的子視圖來調整contentSize的尺寸;又比如某些UITableViewCell中的高度是動態的,其高度尺寸是由里面的子視圖來確定的。在這些類中并沒有重載intrinsicContentSize的實現,所以需要提供一種新的設置方法來實現這種尺寸自適應的能力。

1. 容器視圖實現尺寸自適應

對于一個容器父視圖來說,當要實現父視圖的尺寸依賴所有子視圖的尺寸來實現自適應時,要設置的約束依賴不是通過尺寸約束來實現而是通過位置約束來實現。假設有如下的布局:

我們希望父容器視圖S的尺寸是自適應的,那么就需要設置S視圖的右邊邊界等于子視圖B的右邊邊界,同時需要設置S視圖的底部邊界等于子視圖C的底部邊界??梢鑰闖隼匆迪指溉萜魘油糞的尺寸自適應時不是通過設置寬度和高度的尺寸依賴來實現的而是通過設置讓父視圖的邊界依賴于某個子視圖的邊界來實現的。具體代碼展示如下:

//這里忽略了視圖的創建代碼。
//本文對AutoLayout進行約束設置都是用iOS9以后所提供的進行約束設置的簡易方法。
[A.leftAnchor constraintEqualToAnchor:S.leftAnchor constant:10].active = YES;
[A.topAnchor constraintEqualToAnchor:S.topAnchor  constant:10].active = YES;
[A.widthAnchor constraintEqualToConstant:100].active = YES;
[A.heightAnchor constraintEqualToConstant:30].active = YES;
    
[B.leftAnchor constraintEqualToAnchor:S.leftAnchor constant:80].active = YES;
[B.topAnchor constraintEqualToAnchor:A.bottomAnchor constant:20].active = YES;
[B.widthAnchor constraintEqualToConstant:200].active = YES;
[B.heightAnchor constraintEqualToConstant:30].active = YES;
    
[C.leftAnchor constraintEqualToAnchor:S.leftAnchor constant:30].active = YES;
[C.topAnchor constraintEqualToAnchor:B.bottomAnchor constant:20].active = YES;
[C.widthAnchor constraintEqualToConstant:50].active = YES;
[C.heightAnchor constraintEqualToConstant:40].active = YES;
    
//假設S是添加在某個視圖控制器中。
[S.leftAnchor constraintEqualToAnchor:self.view.leftAnchor constant:20].active = YES;
[S.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:90].active = YES;
//右邊邊界依賴B子視圖的右邊,下邊邊界依賴C子視圖的下邊實現尺寸自適應
[S.rightAnchor constraintEqualToAnchor:B.rightAnchor constant:20].active = YES;
[S.bottomAnchor constraintEqualToAnchor:C.bottomAnchor constant:20].active = YES;

可以看出這種實現的機制有一定的局限性!那就是當添加或者刪除子視圖時以及調整了某個子視圖的位置和尺寸時就需要重新調整父視圖的自適應約束設置。

2.UIScrollView的滾動

對于UIScrollView來說需要設置contentSize來實現滾動的能力。但是基于約束設置的布局體系來說,因為很多約束都是通過依賴來實現的,因此要計算contentSize并不是那么的容易和簡單。為此當UIScrollView要和AutoLayout進行結合使用并實現滾動能力的話就不能直接將所有子視圖都添加到UIScrollView中去, 而是需要中間建立一個容器視圖,首先將容器視圖添加到UIScrollView中去,然后再將所有子視圖添加到容器視圖中去。在設置約束依賴時將容器視圖的上下左右分別依賴UIScrollView視圖的上下左右邊界,如果需要上下滾動則將容器視圖中的最底部子視圖的底部邊界依賴容器視圖的底部邊界。如果不需要上下滾動則改為將容器視圖的高度等于UIScrollView視圖高度即可。 如果需要左右滾動則將容器視圖中的最右邊子視圖的右邊邊界依賴于容器視圖的右邊邊界。如果不需要水平滾動則改為將容器視圖的寬度等于UIScrollView視圖的寬度。通過這樣的設置后UIScrollView視圖的contentSize將得到自動的計算。下面是具體的實例代碼:

//1.創建一個滾動視圖,并設置好約束,這個約束可以是AutoLayout也可以是frame的,這里為了簡單就用frame。
    UIScrollView *scrollView = [UIScrollView new];
    [self.view addSubview:scrollView];
    scrollView.frame = CGRectMake(100, 100, 100, 100);
    
    //2.創建一個容器視圖, 這個容器視圖放入滾動視圖中,保證滾動視圖只有一個容器子視圖。
    UIView *containerView = [UIView new];
    containerView.translatesAutoresizingMaskIntoConstraints = NO;
    containerView.backgroundColor = [UIColor orangeColor];
    [scrollView addSubview:containerView];
    
    //3.將所有的子視圖A,B,C都添加到容器視圖中。
    UILabel *A = [UILabel new];
    A.text = @"A";
    A.translatesAutoresizingMaskIntoConstraints = NO;
    A.backgroundColor = [UIColor redColor];
    [containerView addSubview:A];
    
    UILabel *B = [UILabel new];
    B.text = @"B";
    B.translatesAutoresizingMaskIntoConstraints = NO;
    B.backgroundColor = [UIColor greenColor];
    [containerView addSubview:B];
    
    UILabel *C = [UILabel new];
    C.text = @"C";
    C.translatesAutoresizingMaskIntoConstraints = NO;
    C.backgroundColor = [UIColor blueColor];
    [containerView addSubview:C];
    
    //4.分別設置容器視圖中子視圖的約束依賴。
    [A.leftAnchor constraintEqualToAnchor:containerView.leftAnchor constant:10].active = YES;
    [A.topAnchor constraintEqualToAnchor:containerView.topAnchor  constant:10].active = YES;
    [A.widthAnchor constraintEqualToConstant:100].active = YES;
    [A.heightAnchor constraintEqualToConstant:30].active = YES;
    
    [B.leftAnchor constraintEqualToAnchor:containerView.leftAnchor constant:80].active = YES;
    [B.topAnchor constraintEqualToAnchor:A.bottomAnchor constant:20].active = YES;
    [B.widthAnchor constraintEqualToConstant:200].active = YES;
    [B.heightAnchor constraintEqualToConstant:30].active = YES;
    
    [C.leftAnchor constraintEqualToAnchor:containerView.leftAnchor constant:30].active = YES;
    [C.topAnchor constraintEqualToAnchor:B.bottomAnchor constant:20].active = YES;
    [C.widthAnchor constraintEqualToConstant:50].active = YES;
    [C.heightAnchor constraintEqualToConstant:40].active = YES;
    
    //5.設置容器視圖的依賴約束,讓容器視圖的四個邊界分別等于滾動視圖的四個邊界,這里必須要這樣設置。
    [containerView.leftAnchor constraintEqualToAnchor:scrollView.leftAnchor].active = YES;
    [containerView.topAnchor constraintEqualToAnchor:scrollView.topAnchor].active = YES;
    [containerView.rightAnchor constraintEqualToAnchor:scrollView.rightAnchor].active = YES;
    [containerView.bottomAnchor constraintEqualToAnchor:scrollView.bottomAnchor].active = YES;

    //6.關鍵的一步,如果需要上下滾動則將容器視圖中的最底部子視圖這里是C的底部邊界依賴于容器視圖的底部邊界。如果不需要上下滾動則不要這樣設置,而是改為將容器視圖的高度等于滾動視圖高度。
    [C.bottomAnchor constraintEqualToAnchor:containerView.bottomAnchor].active = YES;
    //[containerView.heightAnchor constraintEqualToAnchor:scrollView.heightAnchor].active = YES;
    //7.關鍵的一步,如果需要左右滾動則將容器視圖中的最右部子視圖這里是B的右邊邊界依賴于容器視圖的右邊邊界。如果不需要水平滾動則不要這樣設置,而是改為將容器視圖的寬度等于滾動視圖的寬度
    [B.rightAnchor constraintEqualToAnchor:containerView.rightAnchor].active = YES;
    //[containerView.widthAnchor constraintEqualToAnchor:scrollView.widthAnchor].active = YES;

如果是使用Storyboard來設置約束依賴的步驟和流程也是一樣的。上面的約束設置實現視圖滾動的機制也有一定的局限性!那就是一旦在容器視圖中添加子視圖時就需要重新調整容器視圖的右邊界和下邊界的約束依賴。這就需要將舊的邊界約束依賴記住,并在設置新的邊界依賴前刪除舊的約束依賴。

3.UITableViewCell的高度自適應

UITableViewCell要實現高度自適應,需要在UITableViewDelegate中的方法:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return UITableViewAutomaticDimension;
}

實現中針對某個cell返回一個特定高度值UITableViewAutomaticDimension。然后在UITableViewCell的派生類的視圖代碼布局處或者在-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中進行特殊約束設置即可。在上面的第1節中有介紹如何將一個容器視圖的尺寸設置為自適應,而一般情況下在編寫UITableViewCell的布局代碼時,都將所有的子視圖添加到contentView這個視圖中,因此要實現UITableViewCell的高度自適應時,只需要將contentView當做是一個容器視圖,然后按照第1節中介紹的布局約束設置方法就可以實現高度自適應了。

MyLayout&TangramKit的尺寸自適應

MyLayout&TangramKit中的一個重要的能力是支持布局視圖尺寸自適應的自動計算,也就是說布局視圖的寬度或者高度可以根據子視圖的尺寸來自行確定,而不需要去明確的依賴某個子視圖來實現這種能力。對于MyLayout來說可以設置布局視圖的wrapContentHeight或者wrapContentWidth為YES來實現這種能力,而對于TangramKit來說可以設置布局視圖的tg_width, tg_height的值為.wrap來實現這種能力。比如一個布局父視圖S中有三個子視圖A,B,C。要求S的高度和寬度根據三個子視圖的高度和寬度自適應,那么只需要將布局視圖S的約束設置為如下:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return UITableViewAutomaticDimension;
}

1.容器視圖實現尺寸自適應

在MyLayout&TangramKit中的定義出了特殊的布局視圖這個概念。所有為子視圖設置的約束都必須放入到一個布局視圖中才有效。整個布局框架提供了多種布局視圖,每種布局視圖中的子視圖都將按照特定的規則進行排列和布局。當布局視圖這個容器視圖要實現尺寸自適應時就非常簡單,它不需要依賴任何對子視圖的約束依賴,而只需要將布局視圖的尺寸設置為wrap即可。就以上面的圖片例子用MyLayout&TangramKit來實現來說,可以將S視圖定義為一個垂直線性布局視圖,而將A,B,C三個子視圖添加到布局視圖中即可。下面是具體實現的布局部分的代碼:

------------------------------------------------
//OC版本,S是一個垂直線性布局

A.myLeft = 10;
A.myTop = 10;
A.mySize = CGSizeMake(100,30);

B.myLeft = 80;
B.myTop = 20;
B.mySize = CGSizeMake(200, 30);

C.myLeft = 30;
C.myTop = 20;
C.mySize = CGSizeMake(50, 40);

//只需要將布局視圖的相應屬性設置為YES即可,不需要依賴于特定子視圖。
S.wrapContentSize = YES;


------------------------------------------------
//Swift版本,S是一個垂直線性布局
A.tg_origin(x:10,y:10).and().tg_size(width:100,height:30)
B.tg_origin(x:80,y:20).and().tg_size(width:200,height:30)
C.tg_origin(x:30,y:20).and().tg_size(width:50,height:40)
//布局視圖的尺寸根據子視圖自適應。
S.tg_size(width:.wrap, height:.wrap)

因為MyLayout&TangramKit中的尺寸自適應約束不需要明確依賴的某個子視圖,因此當布局視圖中的子視圖有變化時系統會自動重新進行布局視圖的尺寸計算,而不需要做任何調整,這是使用MyLayout&TangramKit的最大的一個優勢!

2.UIScrollView的滾動

MyLayout&TangramKit對于處理和UIScrollView進行結合時進行特殊處理,當將一個布局視圖添加到滾動視圖時,布局系統內部會負責處理滾動視圖的contentSize。要實現UIScrollView滾動時,只需要在一個滾動視圖內添加一個布局視圖,然后將所有其他子視圖都添加到這個布局視圖中去,這個和上面的AutoLayout的處理方式是一樣的,最后將布局視圖的尺寸自適應屬性設置為YES就可以了。具體實現的OC代碼如下:

   //1.創建一個滾動視圖,并設置好約束,這個約束可以是AutoLayout也可以是frame的,這里為了簡單就用frame。
    UIScrollView *scrollView = [UIScrollView new];
    [self.view addSubview:scrollView];
    scrollView.frame = CGRectMake(100, 100, 100, 100);
    
    //2.創建一個容器視圖, 這個容器視圖放入滾動視圖中,保證滾動視圖只有一個容器子視圖。
    MyLinearLayout *containerView = [MyLinearLayout new];
    containerView.backgroundColor = [UIColor orangeColor];
    //設置容器布局視圖的尺寸自適應屬性為YES。
    containerView.wrapContentSize = YES; 
    [scrollView addSubview:containerView];
    
    //3.將所有的子視圖A,B,C都添加到容器視圖中。
    UILabel *A = [UILabel new];
    A.text = @"A";
    A.backgroundColor = [UIColor redColor];
    [containerView addSubview:A];
    
    UILabel *B = [UILabel new];
    B.text = @"B";
    B.backgroundColor = [UIColor greenColor];
    [containerView addSubview:B];
    
    UILabel *C = [UILabel new];
    C.text = @"C";
    C.backgroundColor = [UIColor blueColor];
    [containerView addSubview:C];
    
    //4.分別設置容器視圖中子視圖的約束依賴。
    A.myLeft = 10;
    A.myTop = 10;
    A.mySize = CGSizeMake(100,30);

    B.myLeft = 80;
    B.myTop = 20;
    B.mySize = CGSizeMake(200, 30);

    C.myLeft = 30;
    C.myTop = 20;
    C.mySize = CGSizeMake(50, 40);

   //5.然后就沒有然后了!用MyLayout不需要進行特殊的附加設置??!

因為MyLayout&TangramKit中的尺寸自適應約束不需要明確依賴某個子視圖,因此當布局視圖中的子視圖有變化時系統會自動重新進行布局視圖的尺寸計算,而當布局視圖的尺寸變化時又會調整UIScrollView的contentSize。

3. UITableViewCell的高度自適應

UITableViewCell要實現高度自適應,需要在UITableViewDelegate中的方法:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return UITableViewAutomaticDimension;
}

實現中針對某個cell返回一個特定高度值UITableViewAutomaticDimension。然后在UITableViewCell的派生類中建立一個根布局視圖,這個根布局視圖作為子視圖添加到contentView中代碼如下:

 //假設根布局視圖是一個垂直線性布局視圖。
  self.rootLayout= [MyLinearLayout linearLayoutWithOrientation:MyOrientation_Vert];
  self.rootLayout.cacheEstimatedRect = YES;  //提供緩存加速計算 
  self.rootLayout.myHorzMargin = 0;   //布局寬度等于contentView的寬度
  self.rootLayout.wrapContentHeight = YES; //布局視圖高度自適應。
  [self.contentView addSubview:self.rootLayout]; 

  //這里將所有子視圖都添加到rootLayout中,并設置約束。

然后在UITableViewCell的派生類中重載視圖的方法:

- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority
{
     //系統在對UITableVeiwCell進行布局時會調用systemLayoutSizeFittingSize方法來得到視圖的尺寸,我們把計算cell尺寸的任務交由布局視圖的sizeThatFits方法來完成。
     return [self.rootLayout sizeThatFits:targetSize];  
}

MyLayout&TangramKit和AutoLayout的結合

MyLayout&TangramKit的布局體系是基于原生的frame的計算來實現布局,而AutoLayout則不再依賴frame而是依賴視圖之間的約束來是實現布局。這里只介紹將MyLayout&TangramKit的布局視圖加入到AutoLayout布局體系中去的一些方法。

1.將布局視圖添加到非布局父視圖中

因為布局視圖也是一個視圖,都是從UIView派生。因此要將一個布局視圖添加到采用AutoLayout約束的布局體系時,就像為普通視圖一樣給布局視圖設置約束依賴即可。

2.使用布局視圖的尺寸自適應屬性

因為MyLayout&TangramKit中的布局視圖具有設置尺寸自適應的屬性,為了實現跟AutoLayout結合,最新版本的庫的布局視圖內部重載了intrinsicContentSize的方法。因此如果想使用布局視圖的尺寸自適應功能,那么在將布局視圖的尺寸設置為wrap后,就可以像使用UILabel那樣不用去設置布局視圖的寬度約束和高度約束了。比如有兩個兄弟視圖A,B。A視圖是一個MyLayout&TangramKit布局視圖,其寬度等于父視圖S的寬度,而高度則根據布局視圖里面的子視圖的高度自適應,而B視圖則在A視圖的下方,并且寬度等于A視圖。那么混合約束代碼的實現如下:

//這里只演示OC代碼。
MyLinearLayout *A = [MyLinearLayout new];
A.translatesAutoresizingMaskIntoConstraints = NO;
//這里設置A視圖的高度自適應。
A.wrapContentHeight = YES;
[S addSubView:A];

//往A布局視圖里面添加子視圖的代碼。。

UIView *B = [UIView new];
B.translatesAutoresizingMaskIntoConstraints = NO;
[S addSubView:B];


//A布局視圖的約束設置,這里不需要設置高度約束,因為使用了布局視圖的高度自適應屬性。
[A.leftAnchor  constraintEqualToAnchor:S.leftAnchor].active = YES;
[A.topAnchor  constraintEqualToAnchor:S.topAnchor].active = YES;
[A.widthAnchor constraintEqualToAnchor:S.widthAnchor].active = YES;

//B視圖必須設置完整約束
[B.leftAnchor  constraintEqualToAnchor:S.leftAnchor].active = YES;
[B.topAnchor  constraintEqualToAnchor:A.bottomAnchor].active = YES;
[B.widthAnchor constraintEqualToAnchor:A.widthAnchor].active = YES;
[B.heightAnchor constraintEqualToConstant:30].active = YES;

在布局庫MyLayout中有更加復雜和詳細的對布局視圖如何和AutoLayout相互結合的代碼:AllTest12ViewController。您可以在這個DEMO中看到如何實現父視圖的尺寸和兄弟視圖的尺寸和位置如何依賴尺寸自適應的布局視圖的代碼。

3.MyLayout&TangramKit的UITableViewCell高度自適應實現

如果你的所有視圖都不使用AutoLayout的話則可以通過上面介紹的MyLayout&TangramKit來實現UITableViewCell的高度自適應的解決方案來實現。但是缺點就是要進行特定方法的重載。而這個問題在新版本中都已經得到解決了??!因為布局視圖重載intrinsicContentSize方法,因此當將某個布局視圖作為UITableViewCell的子視圖時如果想使用布局視圖的尺寸自適應的能力,只需要將布局視圖的尺寸設置為wrap即可,然后將布局視圖添加到其他視圖中去,不需要再為布局視圖設置寬度和高度約束了,也不再限制只能將布局視圖添加到contentView中了,也不再需要重載特定的方法了,就相當于將一個布局視圖當做UILabel視圖來使用即可。具體的代碼可以參考布局庫MyLayout中的AllTest1TableViewCellForAutoLayout.m實現。


歡迎大家訪問歐陽大哥2013的github地址

三公棋牌 任八技巧复式稳赚不赔法 内蒙古时时 一分快三精准计划软件 重庆时时彩单双玩法 pk10人工计划网站 二八杠怎么压才能赢钱 双色球开奖数据导出 孔帕尼 正规篮球投注 二八杠游戏下载 安徽快三计划软件 双色球开奖数据统计 大富翁棋牌游戏 3d长期追号稳赚吗 时时彩后三杀号技巧