關(guān)于如何寫UI及屏幕適配的一些技巧(下)

接上篇:關(guān)于如何寫UI及屏幕適配的一些技巧(上)


上篇發(fā)出之后收集了一些反饋, 總結(jié)起來以下幾點:

  1. 沒有demo,代碼沒有全部粘上來。
    我認(rèn)為這篇的內(nèi)容不需要,不能我寫個弱引用的懶加載怎么寫,還一定要把聲明weak屬性的代碼放上來吧。
  2. 關(guān)于xib/strotyboard和純代碼
    我是兩種用過,實際工作中也基本都達(dá)到了熟練的程度,現(xiàn)在使用純代碼也不是公司要求,自己覺得更好,其他的就不說了,這就相當(dāng)于兩條路都行的通,任意一條走成老司機(jī)了另一條也沒問題,但是新人工作中最好純代碼為主。
  3. 關(guān)于懶加載是否一定要
    理解到位想怎么搞怎么搞,蘿卜青菜各有所愛。

提綱:上篇說到第4條
1. 關(guān)于xib/storyboard 與 純代碼的對比  
2. 一條規(guī)范(又提了一點關(guān)于命名的)
3. UI工廠類 與 代碼塊
4. 懶加載, View使用strong還是weak
5. 復(fù)雜界面要會分區(qū),要會障眼法
6. masonry均布View,及其布局時約束依賴關(guān)系
7. 關(guān)于屏幕適配的一點技巧
8. 循環(huán)引用(上篇文章有人對循環(huán)引用不理解,雖然是基礎(chǔ),有人不理解還是說一下吧)
5. 復(fù)雜界面要會分區(qū),要會用障眼法

分區(qū):什么叫分區(qū),其實就是封裝,干啥其實都是一樣,UI網(wǎng)絡(luò)邏輯思路有相同的地方,就包括收拾東西,為啥很多人喜歡把各種東西用各種盒子裝起來,假設(shè)現(xiàn)在要寫一個aView,上面是這樣的

一個View上的元素

這個要怎么寫, 直接挨個創(chuàng)建直接往aView上加嗎, 這以后維護(hù)起來改點東西相信你死的心都會有的,一般這種元素有點多的都要適當(dāng)?shù)姆忠幌聟^(qū)

分區(qū)后

這樣分區(qū)后,如圖所示,寫UI的時候就先依次單獨解決好上中下三部分,然后需要做的就是對上中下三部分的整體進(jìn)行布局,這一級布局的時候就完全可以忽略他們內(nèi)部的東西是什么樣的,全部完成整體微調(diào),該調(diào)里面就里面, 該調(diào)整體就整體

障眼法: 所謂障眼法就是投機(jī)取巧,當(dāng)然可以有各種各樣的方法,把一些復(fù)雜功能簡化,不管用了什么方法,最終看起來像是實現(xiàn)了就可以。
下面舉個例子, 這個例子是項目中的一個界面, 我簡化了一下抽出來, 這是一個消費(fèi)記錄的界面, 有個tableview,每個cell如下所示,可以展開收起

展開狀態(tài)
收起狀態(tài)

看到這樣一個界面,首先不要考慮如何展開收起,就看一下展開的要怎么寫,(演示Demo中的UI因為沒有使用網(wǎng)絡(luò)數(shù)據(jù),也為了演示方便,做了簡化,實際賬單消費(fèi)下面還有一部分如何消費(fèi),可獲得什么等等的區(qū)域),參照上一條,這種一個View里元素較多的時候可以先分區(qū)如下:

按功能或者位置分區(qū)如圖

先假設(shè)展開狀態(tài)已經(jīng)寫好了,下面要考慮如何收起,觀察UI發(fā)現(xiàn)收起狀態(tài)的信息是展開狀態(tài)中的主要信息, 如圖元素其實表達(dá)的是同樣信息

Paste_Image.png

那么難道要打破布局,將這幾個view找到重新布局,其他的隱藏掉嗎?那再點擊回到展開狀態(tài)怎么辦,在重新布局?想想就麻煩

所以,這時再搞一個summaryView,負(fù)責(zé)收起的信息展示,這個View內(nèi)部的時間桌號等控件,跟展開狀態(tài)的時間桌號雖然長的一樣,但是實際是兩個不同的UI對象。

summaryView

所以完成之后,這個View里會有如下幾大塊

  • summaryView (收起的View)
  • expendBgView(展開時的整體View)
    • topView (這樣命名不好)
    • midView

如此布局,在點擊了View要展開/收起的時候,只需要轉(zhuǎn)換summaryView和expendBgView的隱藏狀態(tài),改變一下最外層View的底部約束即可

demo地址:https://github.com/CoderLXWang/LayoutViewDemo

6. masonry均布View,及其布局時約束依賴關(guān)系

均布View: 等間距布局 - 從0開始說一下masonry的使用

約束依賴關(guān)系:這個標(biāo)題其實比較寬泛,也說不好,如何寫約束本身就是比較靈活的,每個人的寫法可能都不一樣,下面舉兩個例子大概說一下,
示例1:

示例1

這個很簡單, 左右間距都是30,第二三四行View的左右約束該怎么寫,都寫下面的嗎?這樣寫如果要改這個30,就瞎了

make.left.equalTo(父視圖).offset(30);
make.right.equalTo(父視圖).offset(-30);

因為這里的設(shè)計就是左右都要對其,所以下面都都依靠第一個布局即可,第二行兩個不是左右都對其

make.left.right.equalTo(父視圖).offset(第一個View);

代碼少了一行是其次,主要是改的話只改一個,也可以透過代碼看到這個地方的設(shè)計
注:這個示例很簡單,勿噴,主要說這種做法,復(fù)雜布局也需要考慮到底依靠那個View布局,具體體況多體會,簡單說就是要選取合適的依賴對象

示例2:
需求:
1.整體居中
2.寬度可變,看文字是否夠一行,最寬左右內(nèi)邊距10
3.內(nèi)部兩個View的centerY對其
4.最小高度為圖片高度,文字高度超度圖片,就以文字高度為準(zhǔn)

直接上代碼,只為說明約束,不要找別的毛病,具體自己看吧,這里L(fēng)abel和ImageView一定要作為一個整體(即放到同一個父視圖中),內(nèi)部因為圖片相對固定,左右尺寸都不變,要先布局圖片才可以,否則Label沒有可以依賴的東西

@interface ViewController ()

@property (nonatomic, strong) UIView *containerView;

@end

@implementation ViewController

- (UIView *)containerView {
    if (!_containerView) {
        _containerView = [[UIView alloc] init];
        _containerView.backgroundColor = [UIColor orangeColor];
       
        UIImageView *imgView = [[UIImageView alloc] init];
        imgView.image = [UIImage imageNamed:@"demo1.jpeg"];
        [_containerView addSubview:imgView];
        [imgView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.right.equalTo(_containerView).offset(0);
            make.size.mas_equalTo(CGSizeMake(100, 100));
            make.centerY.equalTo(_containerView);
        }];
       
        UILabel *label = [[UILabel alloc] init];
        label.numberOfLines = 0;
        label.text = @"這是阿三沖擊紅進(jìn)口付出dsk紅進(jìn)口付出ds口付出dsk紅進(jìn)口付出dskjfhks口付kjfhks口付出dskj紅進(jìn)口付出dskjfhks口付出dskjjfhks口付出dskjfhd付出dsk紅進(jìn)口付出ds口付出dsk紅進(jìn)口付出dskjfhks口付kjfhks口付出dskj紅進(jìn)口付出dskjfhks口付出dskjjfhks口付出dskjfhdjfhdksjhfdk";
        [_containerView addSubview:label];
        [label mas_makeConstraints:^(MASConstraintMaker *make) {
            make.right.equalTo(imgView.mas_left).offset(-20);
            make.left.equalTo(_containerView).offset(0);
            make.bottom.top.equalTo(_containerView).offset(0);
            make.height.mas_greaterThanOrEqualTo(100);
        }];
    }
    return _containerView;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.containerView];
    [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view).offset(200);
        make.centerX.equalTo(self.view);
        make.width.mas_lessThanOrEqualTo([UIScreen mainScreen].bounds.size.width-20);
    }];
}
7. 關(guān)于屏幕適配的一點技巧

首先說一個屏幕適配到底是什么, 工作中很多人,甚至產(chǎn)品都搞錯了
所謂屏幕適配,并不是大屏就要將UI變大,而是要顯示更多的內(nèi)容。
再說一個關(guān)于按鈕的寫UI原則
按鈕設(shè)計的大沒啥可說,如果設(shè)計的按鈕很小,到程序員手里一定要讓它看起來小,點起來大
KRATE :當(dāng)然在這一基本原則下,有的時候大屏上的某些元素和小屏保持同樣大小會有一些難看,這時還是要分別對待,如果以5s屏幕尺寸為基準(zhǔn)(也有用6的尺寸做基準(zhǔn)的,都一樣,習(xí)慣問題),這里一般會定義這樣一個宏

#define KRATE (SCREEN_WIDTH/320.0)

舉個例子


5s效果

不做比例的適配,在6p上如圖

6p效果

其實也沒啥問題,看起來也沒有很不協(xié)調(diào)的地方,但是注意看一下券左右兩條豎直虛線,會發(fā)現(xiàn)大屏上左面券命和右面打印的寬度會比較小,中間區(qū)域顯得過大,應(yīng)該稍微勻一點給左右兩邊,兩邊看起來會不那么擠,同時右側(cè)點擊范圍也會相應(yīng)放大,做法就是將左右約束的值*KRATE

[self.leftLine mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.bgView).offset(80*KRATE);
        make.top.equalTo(self.bgView).offset(15);
        make.bottom.equalTo(self.bgView).offset(-15);
        make.width.mas_equalTo(1);
}];
    
[self.rightLine mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.equalTo(self.bgView).offset(-40*KRATE);
        make.top.bottom.equalTo(self.bgView).offset(0);
        make.width.mas_equalTo(1);
}];

乘上比例之后,效果如圖

*KRATE后的6P效果

為什么會有這種問題產(chǎn)生, 其實也是由于個人的布局習(xí)慣引起的,在布局這個UI時,我是先將左右兩條虛線定位好,內(nèi)部的東西根據(jù)虛線的位置確定,而虛線的位置就是一塊白色背景左右給定值布局的,所以換到大屏?xí)笥覍挾炔蛔?,這種情況乘個KRATE就可以了。
麻煩有些人不要來噴啥你寫就不會有這個問題,按比例分區(qū)怎樣的,那是你的方法,你要是按比例分區(qū)不也要想到底給0.幾的比例嗎,要是豎直方向在scollView里也有這個問題呢

KKRATE:因為KRATE是用不同屏幕的寬度算出的一個系數(shù),假設(shè)某個寬度5s上寬度為10,KRATE后6上則為10375/320=11.7, 也就是屏幕大1號之后原本為10的寬度增大了1.7, 那么這個寬度如果是40呢,40*374/320 = 46.9。
為了解決小寬度 ×KRATE基本沒效果或者寬度大 ×KRATE 又過分了的問題,又定義了這樣一個宏,給KRATE在乘一個自己制定的系數(shù),感覺沒效果就給KKRATE傳個大于1的系數(shù),感覺過分了就KKRATE(0.95),這里注意傳入的系數(shù)小于0.86就反而大屏UI更小了,這里不想在里面繼續(xù)寫個三目運(yùn)算符判斷了,就這樣了

/** 在屏幕比例基礎(chǔ)上再次比例, 大于0.86, 否則反而變小 */
#define KKRATE(rate) (KRATE > 1 ? KRATE*rate : KRATE)

拿一個界面舉個例子,如圖

5s效果

這里左右看起來窄窄的間距用的都是6dp,6dp如果直接*KRATE基本沒用,乘完也就加一個dp,效果基本就是如下,屏幕很大,間距很小氣,也許你會說小屏上也小氣,設(shè)計說了,你不懂,正好

6P效果

如果將各處左右間距設(shè)置為

make.left.equalTo(ws.view).offset(6*KKRATE(1.8));
make.right.equalTo(ws.view).offset(-6*KKRATE(1.8));

效果如下

*KKRATE后6P效果

明顯大氣了許多。。。

8. 循環(huán)引用(上篇文章有人對循環(huán)引用不理解,雖然是基礎(chǔ),有人不理解還是說一下吧)

這部分是計劃外的,因為上篇有不少同學(xué)問起這個東西,發(fā)現(xiàn)不少人對看似簡單的循環(huán)引用概念還是比較模糊,所以我就拿出來說一下,我會分別解釋一下常見的循環(huán)引用,以及代理,block中的循環(huán)引用問題,這里只做理解解釋,沒有深入研究,大神直接略過吧。
先說一下內(nèi)存管理,大家都知道內(nèi)存管理在MRC下要手動寫retain,release等代碼,操作一個對象的引用計數(shù),以此控制對象持有及釋放,ARC下編譯器會自動添加retain/release等代碼,ARC的一個基本規(guī)則就是,只要某個對象被任一strong指針指向,那么它將不會被銷毀。如果對象沒有被任何strong指針指向,那么就將被銷毀。
所以當(dāng)前我們的代碼基本都是ARC,當(dāng)我們研究一個對象是否循環(huán)引用時,也就不考去考慮計數(shù)到底為幾,什么時候retain,什么時候release,我們只需要按照ARC的基本原則關(guān)心指向這個對象的strong指針。
下面就按這個基本原則解釋一下循環(huán)引用,觀察是否釋放在控制器打印dealloc方法即可
示例1:簡單粗暴無邏輯演示
有一個控制器SampleRetainCycleController *retainVC,retainVC.view上面有個SampleRetainCycleView *testView,testView有個強(qiáng)引用指針,指向retainVC,看起來貌似循環(huán)成一個圈了,這就是循環(huán)引用嗎?貌似怪怪的,因為少了一個引用

簡單的循環(huán)引用?

實際上少了一個引用關(guān)系,沒有考慮retainVC是那里來的,retainVC被創(chuàng)建之后加載nav導(dǎo)航棧里是被navController強(qiáng)引用的,這是我們就可以按ARC的原則分析了,就是看線,找實線,這里我們關(guān)心的是控制器會不會被正常釋放,那我們就看控制器有幾根實線,這時就會發(fā)現(xiàn)有兩根,pop出去的時候,上面的那條nav的線斷了,但是還有一條View的線,所以控制器就不會被釋放


實際循環(huán)引用圖示1

示例2:代理為什么用weak聲明
如圖就是代理為什么用弱引用,如果用強(qiáng)引用就變成示例1的情況了


代理用弱引用原因

示例3:一般使用block為什么注意循環(huán)引用,使用weakSelf
先說為什么block一定要用copy,既然會循環(huán)引用,那么就像代理一樣,使用弱引用的指針不行嗎?
詳細(xì)看這篇文章吧 Block為什么使用copy修飾,
更詳細(xì)可以看這篇談Objective-C block的實現(xiàn)
總結(jié)起來就是為了使其存放在堆中,如果不copy一下,block是存放在棧中的,出了創(chuàng)建它的作用域,就可能被釋放掉,但是用了copy,對這個block就是強(qiáng)引用,所以需要注意循環(huán)引用,使用weakSelf。
那什么是weakSelf,block默認(rèn)對內(nèi)部引用的外部變量是強(qiáng)引用,所以如果直接使用了self,則相當(dāng)于block有一條實線(強(qiáng)指針)指向self,則self又有兩條實線了

block為什么需要weakSelf

示例4:什么樣的block不會造成循環(huán)引用
最常見的就是系統(tǒng)的一些block與masonry,系統(tǒng)的比如:

    [UIView animateWithDuration: animations:^{
        
    }];

首先是self(假設(shè)是當(dāng)前的控制器)并沒有copy(強(qiáng)引用)這個block,其次這還是個類方法,類方法里不能對屬性進(jìn)行復(fù)制,即也不能強(qiáng)引用這個block,所以直接用self(即block對self強(qiáng)引用)也不會形成循環(huán)引用

再比如masonry

[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {

}];

看一下mas_makeConstraints是怎么寫的

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

只有第三行執(zhí)行了一下這個block,并沒有任何引用的代碼(即類似self.xxblock = block),所以根據(jù)上面的幾篇文章,這種block是存放在棧上的,出了作用域(即這個方法)就會被釋放掉,既然block都被釋放掉了,自然不會循環(huán)引用。

demo地址:https://github.com/CoderLXWang/RetainCycleDemo

上篇地址:關(guān)于如何寫UI及屏幕適配的一些技巧(上)


碼字不易,共同進(jìn)步,歡迎提意見。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容