iOS開發tips-UIScrollView的Autlayout布局


UIScrollViewj盡管繼承於UIView,但它是一個相對比較特殊的視圖,特別是當它遇到了AutoLayout之后。在UIScrollView中使用AutoLayout的目的除了使用相對約束確定子控件的位置和大小外,更重要的是如何自動計算出UIScrollView的contentSize(關於使用UIScrollView並且最終手動指定contentSize的AutoLayout用法不再今天討論之列,嚴格意義上來說這也不是一種真正的UIScrollView的AutoLayout應用)。

UIScrollView的特殊之處

所謂UIScrollView的特殊之處就在於當它遇到了AutoLayout之后其contentSize的計算規則有些特殊。首先contentSize是根據子視圖的leading/trailing/top/bottom進行確定的,而子視圖的位置約束又必須依賴於UIScrollView來確定。這就有點類似於前面UICollectionView自適應高度文章中提到的:UICollectionViewCell的大小計算就是計算contentView的大小,而contentView的大小計算依賴於子視圖的leading/trailing/top/bottom,子視圖的位置約束又依賴於contentView,此時只要子視圖存在固有尺寸(intrinsicContentSize)或者指定了尺寸又設置了leading/trailing/top/bottom,AutoLayout布局引擎即可計算出contentView的大小。
再回到AutoLayout,其實它的contentSize計算原理和UICollectionViewCell自適應很是類似,只是UIScrollView內部並沒有一個contentView的東西(但是可以想象其存在,方便后面的理解,不過要清楚UIScrollView滾動的本質並非包含一個contentView而是通過bounds和frame坐標體系轉換來實現的),只要設置子視圖的leading/trailing/top/bottom(通常是通過edges=0讓子視圖上下左右間距都為0保證整個視圖都在UIScrollView可視范圍之內),然后通過設置size(width/height)約束確定子視圖大小進而由AutoLayout反向計算出UIScrollView的contentSize。
假設A是UIScrollView(藍色)、B是子視圖1(綠色)、C是子視圖2(綠色)、D是contentSize的計算區域(灰色,事實上它不存在),要想讓cotentSize可以自動計算只需要確定B、C上下左右布局間距,然后再指定B、C間距和尺寸之后AutLayout既可以自動推斷出contentSize的大小,原理如下圖(下圖布局類似於下面Demo3):

demo

demo1 單個子視圖布局

對於單個子視圖布局比較簡單,只要設置leading/trailing/top/bottom,再設置子視圖的size(width/height)即可,當然如果子視圖存在固有尺寸並且想要使用固有尺寸的話,則這一步也可以省略。例如下面demo中演示了一個UIScrollView包含一個UIImageView子視圖的圖片查看界面。在下面的布局中僅僅設置了UIImageView上下左右邊距,而UIImageView存在固有尺寸,因此整個布局就相當簡單了(AutoLayout布局使用SnapKit庫)。

	class ImageViewController: UIViewController {	
	    override func viewDidLoad() {
	        super.viewDidLoad()
	        
	        self.view.addSubview(self.scrollView)
	        self.scrollView.addSubview(self.imageView)
	        self.scrollView.snp.makeConstraints { (make) in
	            make.edges.equalTo(0.0)
	        }
	        
	        self.imageView.snp.makeConstraints { (make) in
	            // 下面的約束用於確定contentSize的邊距約束(leading/trailing/top/bottom)
	            // 而由於UIImageView和UILabel、UIButton一樣存在固有尺寸(intrinsicContentSize),因此不需要其他size約束就可以計算出contentSize大小
	            make.edges.equalTo(0.0)
	        }
	        
	    }
	    
	    // MARK: - 私有屬性
	    private lazy var scrollView:UIScrollView = {
	        let temp = UIScrollView()
	        return temp
	    }()
	    
	    private lazy var imageView:UIImageView = {
	        let image = UIImage(named: "img")
	        let temp = UIImageView(image:image)
	        return temp
	    }()
	
	}

demo2 多個子視圖布局使用containerView

很多UIScrollView的AutoLayout的布局文章中都會提到使用一個容器視圖包含多個子視圖,然后分別完成子視圖布局和容器視圖在UIScrollView中的布局,以此來簡化布局過程。下面的Demo中演示了一個圖片分頁查看的布局情況,containerView作為容器布局時設置上下左右間距,然后設置其高度等於UIScrollView高度(因為要實現左右滾動),而此時並不需要設置寬度,因為寬度的計算依賴於子視圖。在containerView的子視圖中只要設置子視圖與containerView的邊距及各自間距和寬度,之后AutoLayout就可以計算出containerView的寬度。如此一來containerView已經設置完了四周間距和尺寸就可以計算出contentSize。

	class SlideViewController: UIViewController {
	
	    override func viewDidLoad() {
	        super.viewDidLoad()
	        
	        self.automaticallyAdjustsScrollViewInsets = false
	        
	        self.view.addSubview(self.scrollView)
	        self.scrollView.addSubview(self.containerView)
	        self.containerView.addSubview(self.firstImageView)
	        self.containerView.addSubview(self.secondImageView)
	        self.containerView.addSubview(self.thirthImageView)
	        
	        self.scrollView.snp.makeConstraints { (make) in
	            make.top.equalTo(self.topLayoutGuide.snp.bottom)
	            make.left.bottom.right.equalTo(0.0)
	        }
	        
	        // 下面的約束確定了containerView的高度,相當於contentSize.height已經確定,width通過cotnentView的子視圖確定即可
	        self.containerView.snp.makeConstraints { (make) in
	            make.edges.equalTo(0.0)
	            make.height.equalTo(self.scrollView.snp.height)
	        }
	        
	        self.firstImageView.snp.makeConstraints { (make) in
	            make.top.left.bottom.equalTo(0.0)
	            make.width.equalTo(self.scrollView.snp.width)
	        }
	        
	        self.secondImageView.snp.makeConstraints { (make) in
	            make.top.bottom.equalTo(0.0)
	            make.left.equalTo(self.firstImageView.snp.right)
	            make.width.equalTo(self.scrollView.snp.width)
	        }
	        
	        self.thirthImageView.snp.makeConstraints { (make) in
	            make.top.bottom.equalTo(0.0)
	            make.left.equalTo(self.secondImageView.snp.right)
	            make.width.equalTo(self.scrollView.snp.width)
	            make.right.equalTo(0.0) // 確定右邊距
	        }
	        
	    }
	    
	    // MARK: - 私有屬性
	    private lazy var scrollView:UIScrollView = {
	        let temp = UIScrollView()
	        temp.isPagingEnabled = true
	        return temp
	    }()
	    
	    private lazy var containerView:UIView = {
	        let temp = UIView()
	        return temp
	    }()
	    
	    private lazy var firstImageView:UIImageView = {
	        let image = UIImage(named: "1")
	        let temp = UIImageView(image:image)
	        temp.contentMode = .scaleAspectFill
	        temp.clipsToBounds = true
	        return temp
	    }()
	    
	    private lazy var secondImageView:UIImageView = {
	        let image = UIImage(named: "2")
	        let temp = UIImageView(image:image)
	        temp.contentMode = .scaleAspectFill
	        temp.clipsToBounds = true
	        return temp
	    }()
	    
	    private lazy var thirthImageView:UIImageView = {
	        let image = UIImage(named: "3")
	        let temp = UIImageView(image:image)
	        temp.contentMode = .scaleAspectFill
	        temp.clipsToBounds = true
	        return temp
	    }()
	
	}

demo3 多個子視圖不使用containerView布局

demo2的containerView包含多個子視圖的布局方式相對來說好像使用要多一些,但是其實布局原理並沒有任何變化,如果熟悉了UIScrollView的AutoLayout布局原理,用不用containerView大家可以根據情況自行決定,如果僅僅是簡單的幾個子視圖布局沒有特殊的需求那么直接布局可能會更簡單,但是如果子視圖相對較多並且可能所有子視圖有公共的操作需求(例如所有子視圖在鍵盤彈出后需要改變其位置)則更適合使用containerView布局。下面代碼中去掉containerView完成demo2的需求,原理相同,代碼也不難理解。

	class SlideViewController2: UIViewController {
	
	    override func viewDidLoad() {
	        super.viewDidLoad()
	        
	        self.automaticallyAdjustsScrollViewInsets = false
	        
	        self.view.addSubview(self.scrollView)
	        self.scrollView.addSubview(self.firstImageView)
	        self.scrollView.addSubview(self.secondImageView)
	        self.scrollView.addSubview(self.thirthImageView)
	        
	        self.scrollView.snp.makeConstraints { (make) in
	            make.top.equalTo(self.topLayoutGuide.snp.bottom)
	            make.left.bottom.right.equalTo(0.0)
	        }
	
	        
	        self.firstImageView.snp.makeConstraints { (make) in
	            make.top.left.bottom.equalTo(0.0)
	            make.size.equalTo(self.scrollView.snp.size)
	        }
	        
	        self.secondImageView.snp.makeConstraints { (make) in
	            make.top.bottom.equalTo(0.0)
	            make.left.equalTo(self.firstImageView.snp.right)
	            make.size.equalTo(self.scrollView.snp.size)
	        }
	        
	        self.thirthImageView.snp.makeConstraints { (make) in
	            make.top.bottom.equalTo(0.0)
	            make.left.equalTo(self.secondImageView.snp.right)
	            make.size.equalTo(self.scrollView.snp.size)
	            make.right.equalTo(0.0) // 確定右邊距
	        }
	        
	    }
	    
	    // MARK: - 私有屬性
	    private lazy var scrollView:UIScrollView = {
	        let temp = UIScrollView()
	        temp.isPagingEnabled = true
	        return temp
	    }()
	
	    private lazy var firstImageView:UIImageView = {
	        let image = UIImage(named: "1")
	        let temp = UIImageView(image:image)
	        temp.contentMode = .scaleAspectFill
	        temp.clipsToBounds = true
	        return temp
	    }()
	    
	    private lazy var secondImageView:UIImageView = {
	        let image = UIImage(named: "2")
	        let temp = UIImageView(image:image)
	        temp.contentMode = .scaleAspectFill
	        temp.clipsToBounds = true
	        return temp
	    }()
	    
	    private lazy var thirthImageView:UIImageView = {
	        let image = UIImage(named: "3")
	        let temp = UIImageView(image:image)
	        temp.contentMode = .scaleAspectFill
	        temp.clipsToBounds = true
	        return temp
	    }()
	
	}

最終效果

總結

其實概括起來UIScrollView的布局最主要的問題就是解決contentSize的計算問題。而根據UIScrollView的特點contentSize的計算最終就是根據上下左右邊距和子控件自身尺寸來反向推導出來的。在遇到多個子視圖的情況下具體用不用容器視圖根據情況而定,容器視圖僅僅起到輔助作用,整個布局原理是完全相同的。使用UIScrollView的AutoLayout布局優點自不必多說,除了從frame計算中擺脫出來之外(絕對布局和相對布局的區別),天生支持屏幕旋轉(屏幕的旋轉適配只需要在布局時稍加注意即可),例如上面三個demo均支持豎屏和橫屏查看,相對於frame布局代碼簡化了很多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM