
移動開發(fā)UI組件【免費下載鏈接】TextureSmooth asynchronous user interfaces for iOS apps.項目地址https://gitcode.com/gh_mirrors/te/Texture點擊查看免費下載導(dǎo)讀本篇技術(shù)指南以 TextureAsyncDisplayKit官方開發(fā)文檔《Layout specs》為骨架系統(tǒng)講解 Texture 的默認(rèn)布局引擎Layout Spec 體系如何描述節(jié)點尺寸與排列、如何通過layoutSpecThatFits:遞歸計算布局以及布局結(jié)果最終如何被 UIKit 消費并驅(qū)動渲染。全文以examples/LayoutSpecExamples示例項目中的PhotoWithOutsetIconOverlay為實戰(zhàn)案例并結(jié)合Source/Layout與Source/ASDisplayNodeLayout*.mm的源碼實現(xiàn)幫助讀者掌握布局計算的觸發(fā)時機(jī)、調(diào)用棧結(jié)構(gòu)、pending layout 機(jī)制與四階段渲染流程。一、布局是什么尺寸、順序與層級在 Texture 中布局Layout描述節(jié)點應(yīng)該如何被設(shè)定尺寸以便顯示。docs/_docs/development/layout-specs.md明確指出布局信息不僅定義了節(jié)點的尺寸size還定義了節(jié)點之間的水平或垂直排列順序ordering當(dāng)結(jié)合節(jié)點層級hierarchy來解讀時節(jié)點的 frame 才最終確定。Texture 提供了兩套可選的布局引擎默認(rèn)的ASLayoutSpec系統(tǒng)本文的主體基于布局描述Layout Spec對象遞歸計算Yoga可選集成通過ASDisplayNodeYoga相關(guān)源碼與#if YOGA編譯分支接入。布局計算的入口是ASLayoutElement協(xié)議——任何可以參與布局的對象ASDisplayNode與ASLayoutSpec都遵循該協(xié)議。Source/Layout/ASLayoutElement.h中對其職責(zé)的定義是一個布局必須指定 1) 該 layoutElement 對象的尺寸而非位置以及 2) 其所有直接子對象的尺寸與位置。樹形遞歸由父節(jié)點向子節(jié)點請求布局驅(qū)動父節(jié)點先獲知子節(jié)點尺寸隨后在尺寸已知后設(shè)置子節(jié)點位置。協(xié)議的核心方法layoutThatFits:會緩存約束與結(jié)果源碼位置因此重復(fù)調(diào)用不會重復(fù)計算。二、實戰(zhàn)案例PhotoWithOutsetIconOverlay官方文檔推薦的學(xué)習(xí)方式是打開 Texture 的示例工程在子類化的ASDisplayNode各函數(shù)上打斷點逐步觀察 Texture 期望使用者如何與其 API 交互并步進(jìn)查看暫停的調(diào)用棧。文檔選取的示例來自examples/LayoutSpecExamples工程對應(yīng)實現(xiàn)位于 examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m。該節(jié)點創(chuàng)建兩個ASNetworkImageNode子節(jié)點照片與圖標(biāo)并用ASAbsoluteLayoutSpec描述它們的擺放位置- (instancetype)init { self [super init]; if (self) { _photoNode [[ASNetworkImageNode alloc] init]; _photoNode.URL [NSURL URLWithString:http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png]; _iconNode [[ASNetworkImageNode alloc] init]; _iconNode.URL [NSURL URLWithString:http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png]; [_iconNode setImageModificationBlock:^UIImage *(UIImage *image, ASPrimitiveTraitCollection traitCollection) { CGSize profileImageSize CGSizeMake(60, 60); return [image makeCircularImageWithSize:profileImageSize withBorderWidth:10]; }]; } return self; } - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { _iconNode.style.preferredSize CGSizeMake(40, 40); _iconNode.style.layoutPosition CGPointMake(150, 0); _photoNode.style.preferredSize CGSizeMake(150, 150); _photoNode.style.layoutPosition CGPointMake(40 / 2.0, 40 / 2.0); ASAbsoluteLayoutSpec *absoluteSpec [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:[_photoNode, _iconNode]]; // ASAbsoluteLayoutSpecs .sizing property recreates the behavior of ASDK Layout API 1.0s ASStaticLayoutSpec absoluteSpec.sizing ASAbsoluteLayoutSpecSizingSizeToFit; return absoluteSpec; }幾個關(guān)鍵點style.preferredSize與style.layoutPosition這些是ASLayoutElementStyle提供的樣式屬性。ASLayoutElementStyle通過ASStackLayoutElement、ASAbsoluteLayoutElement等協(xié)議簇對外暴露見 Source/Layout/ASLayoutElement.h。preferredSize聲明節(jié)點期望尺寸layoutPosition聲明節(jié)點在其父坐標(biāo)系中的固定位置供絕對布局使用。ASAbsoluteLayoutSpec一種將子節(jié)點放置在固定位置的布局描述A layout spec that positions children at fixed positions見 Source/Layout/ASAbsoluteLayoutSpec.h。其sizing枚舉有兩種取值A(chǔ)SAbsoluteLayoutSpecSizingDefault規(guī)格占據(jù)可能的最大尺寸ASAbsoluteLayoutSpecSizingSizeToFit規(guī)格尺寸取所有子節(jié)點 frame 的并集用以復(fù)刻舊版 ASDK Layout API 1.0 中ASStaticLayoutSpec的行為枚舉定義。三、布局流程觸發(fā)、遞歸與調(diào)用棧3.1 遞歸計算入口布局計算是遞歸進(jìn)行的并有多個起始觸發(fā)點。其中一個觸發(fā)點是父節(jié)點 frame 改變時另一個是節(jié)點樹首次創(chuàng)建時——此時會發(fā)起一次布局展平layout flattening計算。遞歸計算的核心方法是layoutThatFits:- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize { return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; }這正是ASDisplayNode在 Source/ASDisplayNodeLayout.mm 中的實現(xiàn)它把約束尺寸的max作為父尺寸隨后遞歸調(diào)用到其底層的整棵節(jié)點樹。3.2 典型的調(diào)用棧下圖展示了在一個包含簡單視圖層級、由ASDKViewController承載的布局中點擊 Photo with outset icon overlay 示例并打斷點在-[PhotoWithOutsetIconOverlay layoutSpecThatFits:]時看到的典型調(diào)用棧調(diào)用棧的第一個顯著分支是頂層的-[ASDisplayNode calculateLayoutThatFits]它負(fù)責(zé)在 Texture 布局與 Yoga 引擎之間做選擇- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize { __ASDisplayNodeCheckForLayoutMethodOverrides; switch (self.layoutEngineType) { case ASLayoutEngineTypeLayoutSpec: return [self calculateLayoutLayoutSpec:constrainedSize]; #if YOGA case ASLayoutEngineTypeYoga: return [self calculateLayoutYoga:constrainedSize]; #endif // If YOGA is not defined but for some reason the layout type engine is Yoga // we explicitly fallthrough here default: break; } // If this case is reached a layout type engine was defined for a node that is currently // not supported. ASDisplayNodeAssert(NO, No layout type determined); return nil; }layoutEngineType的判定邏輯同樣在 Source/ASDisplayNodeLayout.mm 中只有在啟用 YOGA 且節(jié)點擁有 Yoga 父/子節(jié)點時才返回ASLayoutEngineTypeYoga否則一律走 Layout Spec 引擎。調(diào)用棧隨后穿過各個調(diào)用者最終到達(dá)節(jié)點樹中的各個節(jié)點。整個棧依賴ASDisplayNode子類實現(xiàn)layoutSpecThatFits:并返回一個非空non-empty的布局規(guī)格?;惖哪J(rèn)實現(xiàn)會直接斷言失敗- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { __ASDisplayNodeCheckForLayoutMethodOverrides; ASDisplayNodeAssert(NO, -[ASDisplayNode layoutSpecThatFits:] should never return an empty value. One way this is caused is by calling -[super layoutSpecThatFits:] which is not currently supported.); return [[ASLayoutSpec alloc] init]; }這段代碼位于 Source/ASDisplayNodeLayoutSpec.mm。它有兩層含義一是子類必須覆寫此方法二是千萬不要在覆寫中調(diào)用[super layoutSpecThatFits:]否則會觸發(fā)斷言。3.3 兩種聲明布局的方式除了覆寫layoutSpecThatFits:ASDisplayNode還提供了layoutSpecBlock屬性允許以 block 方式聲明布局而不必創(chuàng)建子類Source/ASDisplayNodeLayoutSpec.hnode.layoutSpecBlock ^ASLayoutSpec *(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { // 返回一個包含全部子節(jié)點位置的 ASLayoutSpec };源碼中有兩條約束值得注意二者不可混用實現(xiàn)layoutSpecThatFits:的節(jié)點不能再設(shè)置layoutSpecBlock反之亦然否則觸發(fā)異常斷言見 Source/ASDisplayNodeLayoutSpec.mm。調(diào)用時機(jī)layoutSpecThatFits:可能在非主線程被調(diào)用因此該方法內(nèi)不應(yīng)執(zhí)行 UI 線程相關(guān)操作但可在此處完成昂貴的預(yù)計算并用 ivar 緩存中間結(jié)果接口注釋。calculateLayoutLayoutSpec:的實際執(zhí)行流程Source/ASDisplayNodeLayoutSpec.mm還包括若節(jié)點既無layoutSpecBlock也未覆寫layoutSpecThatFits:則退化為調(diào)用calculateSizeThatFits:做手工尺寸計算將 layout spec 標(biāo)記為不可變isMutable NO——layout spec 在layoutSpecThatFits:中創(chuàng)建并允許修改一旦交回框架便禁止再改向 spec 子樹傳播 trait collection對返回的 layout 執(zhí)行filteredNodeLayoutTree展平并支持 RTL從右到左鏡像翻轉(zhuǎn)。3.4 Pending Layout計算結(jié)果先緩存繪制時再消費此時尚未發(fā)生任何繪制。layout spec 此刻被貢獻(xiàn)到一個名為_pendingDisplayNodeLayout的數(shù)據(jù)存儲中ASDisplayNode基類上的實例變量等到下一次繪制操作時才被真正實現(xiàn)??梢栽?[ASDisplayNode(Layout) layoutThatFits:parentSize:]中看到 pending layout 的捕獲邏輯ASLayout *layout [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; as_log_verbose(ASLayoutLog(), Established pending layout for % in %s, self, sel_getName(_cmd)); _pendingDisplayNodeLayout ASDisplayNodeLayout(layout, constrainedSize, parentSize, version);對應(yīng)源碼在 Source/ASDisplayNodeLayout.mm。該方法的完整邏輯是三級緩存查找若_calculatedDisplayNodeLayout對當(dāng)前約束、父尺寸與 layout version 有效直接復(fù)用否則若_pendingDisplayNodeLayout有效復(fù)用 pending否則重新計算并把結(jié)果寫入_pendingDisplayNodeLayout。四、從布局計算到渲染四階段流程布局計算與繪制之間隔著一次重要的中間步驟ASDisplayNode背后的 UIKit 對象已被放入 UIKit 層級——表現(xiàn)為UIView或折疊collapsed成CALayer。這正是 Texture 布局 spec 準(zhǔn)備就緒、到它們真正用于給 UIKit 對象設(shè)定尺寸之間的銜接階段。layout spec 真正參與渲染的位置是 UIKit 的layoutIfNeeded階段對應(yīng)-[ASDisplayNode(UIViewBridge) layoutIfNeeded]實現(xiàn)見 Source/Private/ASDisplayNodeUIViewBridge.mm。在該階段pending layout 被消費用于確定即將顯示節(jié)點的 frame 與 bounds。尺寸計算與擺放過程包含多個步驟核心方法之一是-[ASDisplayNode(Layout) _layoutSublayouts]好奇的讀者可以順藤摸瓜查看其調(diào)用方。下圖展示的是布局應(yīng)用rendering階段pending layout 被消費、_layoutSublayouts等核心方法參與執(zhí)行的調(diào)用棧綜上Texture 松散地遵循 UIKit 系統(tǒng)、以不同階段推進(jìn)布局與渲染ASDisplayNode與ASLayout初始化兩者可以完全獨立于 UIKit 存在。此時創(chuàng)建一次 pending layout 計算。UIView/CALayer初始化遵循 UIKit 慣例在設(shè)定尺寸之前先創(chuàng)建用于顯示的 UIKit 對象。此時它們已處于 UIKit 層級中從而觸發(fā)后續(xù)的布局樹流程。UIView/CALayer布局遵循 UIKit 的遞歸操作。這一步與第 1 步的布局計算截然不同——它純粹是消費已準(zhǔn)備好的 pending layout并應(yīng)用到UIView/CALayer上完成尺寸設(shè)定。渲染必要時對CALayer進(jìn)行柵格化rasterizeUIKit 層級隨后被 UIKit 繪制到屏幕。五、數(shù)據(jù)結(jié)構(gòu)支撐ASLayout 與 ASLayoutSpec5.1 ASLayout布局樹的節(jié)點ASLayout是布局樹中的一個節(jié)點代表創(chuàng)建它的ASLayoutElement的尺寸與位置Source/Layout/ASLayout.h。核心屬性size當(dāng)前布局的尺寸position在父布局中的位置默認(rèn)是ASPointNull{NAN, NAN}作為子布局時必須為有效值sublayouts子布局?jǐn)?shù)組其中每個子布局都必須有非空 positionframe由size與position計算出的有效 frame。幾個值得注意的實現(xiàn)細(xì)節(jié)布局的size會被向上取整到像素值A(chǔ)SCeilPixelValue非法尺寸在 Release 配置下會被強制歸零Source/Layout/ASLayout.mmASCalculateLayout與ASCalculateRootLayout是面向layoutElement的安全布局計算入口會針對 nil 節(jié)點做防護(hù)Source/Layout/ASLayout.h**布局展平flattening**由filteredNodeLayoutTree完成如果布局已經(jīng)展平則直接復(fù)用否則通過 DFS 把 layout spec 節(jié)點攤平僅保留ASDisplayNode類型的子布局并把 spec 子節(jié)點的絕對位置逐級累加Source/Layout/ASLayout.mm。這也是布局結(jié)果能夠直接指導(dǎo) UIKit 對象擺放的原因。5.2 ASLayoutSpec不可變的布局描述ASLayoutSpec是一個不可變對象用于描述一種布局設(shè)計上松散地受到 React 啟發(fā)Source/Layout/ASLayoutSpec.h。其子類覆蓋了常見的布局需求例如ASStackLayoutSpec水平/垂直堆疊ASInsetLayoutSpec內(nèi)邊距ASAbsoluteLayoutSpec固定位置ASRatioLayoutSpec寬高比ASBackgroundLayoutSpec/ASOverlayLayoutSpec背景與覆蓋層ASWrapperLayoutSpec包裝一個或多個子元素Source/Layout/ASLayoutSpec.h。每個ASLayoutSpec必須作用于至少一個子節(jié)點?;愗?fù)責(zé)持有子節(jié)點child屬性適用于單子節(jié)點的 spec如ASInsetLayoutSpecchildren數(shù)組適用于多子節(jié)點的 spec如ASStackLayoutSpec。setChild:atIndex:用于設(shè)定多子節(jié)點 spec 中的附加子節(jié)點——例如ASBackgroundLayoutSpec對外暴露background屬性內(nèi)部實則是調(diào)用setChild:atIndex:接口注釋??勺冃约s束在實現(xiàn)中強制執(zhí)行l(wèi)ayout spec 創(chuàng)建時isMutable YES交回框架后被置為NO此后任何屬性修改都會觸發(fā)斷言Source/Layout/ASLayoutSpec.mm。此外ASLayoutSpec還提供 ASCII 藝術(shù)風(fēng)格的調(diào)試輸出asciiArtString用于在控制臺可視化布局樹。5.3 樣式屬性style 與自動失效ASLayoutElementStyle承載節(jié)點的尺寸與布局選項包括width/height、minWidth/maxWidth、preferredSize、layoutPosition、flex 系列屬性flexGrow、flexShrink、flexBasis、alignSelf、spacingBefore/After等屬性常量定義見 Source/Layout/ASLayoutElement.h。一個關(guān)鍵的聯(lián)動機(jī)制當(dāng)樣式屬性變化時ASLayoutElementStyleDelegate的回調(diào)propertyDidChange:會被觸發(fā)進(jìn)而調(diào)用[self setNeedsLayout]Source/ASDisplayNodeLayout.mm。這保證了對style的任何修改都會自動使相關(guān)布局失效并在下一次布局傳遞中重算無需手動觸發(fā)。六、小結(jié)與進(jìn)一步探索理解 Texture 布局體系的核心脈絡(luò)可以濃縮為一條鏈路layoutSpecThatFits:聲明布局 →calculateLayoutThatFits:選擇引擎 → 遞歸計算生成ASLayout樹 → 展平并緩存為_pendingDisplayNodeLayout→ UIKitlayoutIfNeeded階段消費 pending layout →_layoutSublayouts將尺寸與位置應(yīng)用到 UIView/CALayer → 渲染。想要進(jìn)一步深入可以在當(dāng)前倉庫中查看以下材料官方文檔原文docs/_docs/development/layout-specs.md示例工程含PhotoWithOutsetIconOverlay等十余個布局案例examples/LayoutSpecExamples布局核心協(xié)議Source/Layout/ASLayoutElement.h布局 spec 基類Source/Layout/ASLayoutSpec.h 與 Source/Layout/ASLayoutSpec.mm布局結(jié)果對象Source/Layout/ASLayout.h 與 Source/Layout/ASLayout.mm節(jié)點布局計算與緩存Source/ASDisplayNodeLayout.mm 與 Source/ASDisplayNodeLayoutSpec.mm各 spec 的單元測試與快照測試位于 Tests/例如 Tests/ASLayoutSpecTests.mm、Tests/ASAbsoluteLayoutSpecSnapshotTests.mm、Tests/ASStackLayoutSpecSnapshotTests.mm可對照參考每種布局規(guī)格的預(yù)期行為。贊分享移動開發(fā)UI組件【免費下載鏈接】TextureSmooth asynchronous user interfaces for iOS apps.項目地址https://gitcode.com/gh_mirrors/te/Texture點擊查看免費下載相關(guān)推薦LingBot-Video-MoE-30B-A3B高級配置詳解Flow UniPC調(diào)度器與參數(shù)調(diào)優(yōu)LingBot Video MoE 30B A3B高級配置詳解Flow UniPC調(diào)度器與參數(shù)調(diào)優(yōu) 想要充分發(fā)揮LingBot Video MoE 30B A終極指南Azul布局引擎從TAFFY到WebRender的完整渲染流程終極指南Azul布局引擎從TAFFY到WebRender的完整渲染流程 Azul是一個基于Rust構(gòu)建的現(xiàn)代桌面GUI框架它采用 功能響應(yīng)式編程模型 和 硬桌面應(yīng)用前端跨平臺Texture 布局組合實戰(zhàn)深入理解 layoutSpecThatFits: 方法與 ASLayoutSpec 聲明式布局Texture 布局組合實戰(zhàn)深入理解 layoutSpecThatFits: 方法與 ASLayoutSpec 聲明式布局 本文是 TextureAsync移動開發(fā)UI組件上一篇Git Credential Manager 應(yīng)對 GitHub 令牌認(rèn)證新規(guī)升級遷移指南與 PAT 手動配置方案下一篇如何平穩(wěn)升級PySOT從舊版本遷移到新版本的完整指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考