從解決一個分享圖片生成的歷史bug出發,詳解LayoutInflater和View.post的工作原理

問題背景

最近在項目中遇到一個問題:在檔口分享功能中,需要動態生成一個分享圖片。代碼是這樣寫的:

// 項目中的代碼
val shareView = LayoutInflater.from(this@StallMainActivityV1).inflate(R.layout.share_header_stall_main_layout, null)

這個寫法本身是正確的,但是在自定義的 AvatarView 中,頭像加載的代碼執行不到:

iv_avatar.post {// 這里的代碼執行不到!val w = if (iv_avatar.width > 0) iv_avatar.width else sizeval h = if (iv_avatar.height > 0) iv_avatar.height else sizeGlideKHelper.loadImageToBitmap(...) { ... }
}

LayoutInflater.inflate() 方法

基本語法

LayoutInflater.inflate() 最常用的重載方法是:

inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View

其中的attchToRoot缺省情況下:

root != null → attachToRoot = true
root == null → attachToRoot = false

參數詳解

1. resource (Int) - 布局資源ID

這個沒什么好說的,就是你要加載的XML布局文件的資源ID,比如 R.layout.my_layout

2. root (ViewGroup?) - 父容器

這個參數很關鍵,很多人容易搞錯:

  • 可以傳 null:創建一個獨立的View
  • 可以傳具體的ViewGroup:為新創建的View提供LayoutParams參數

重點來了:這個參數的作用主要是為了讓新創建的View知道自己的LayoutParams應該是什么樣的。

3. attachToRoot (Boolean) - 是否立即添加到父容器
  • true:立即將新View添加到root中,返回的是root
  • false:不添加到root中,但使用root的LayoutParams,返回的是新創建的View
總結
root參數attachToRoot返回值說明
nullfalse(默認)布局文件根View獨立View,使用XML中定義的LayoutParams
ViewGroupfalse布局文件根View獨立View,但使用parent的LayoutParams
ViewGrouptrue(默認)parent布局文件根View已添加到parent中

常見的幾種用法

用法1:創建獨立View(分享、Dialog等場景)
val view = LayoutInflater.from(context).inflate(R.layout.my_layout, null)

適用場景:分享圖片生成、PopupWindow、Dialog等不需要添加到現有布局的場景。

用法2:為RecyclerView創建ViewHolder
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)

為什么要傳parent:讓item知道自己應該用什么樣的LayoutParams。
為什么是false:因為RecyclerView會自己管理添加時機。

用法3:直接添加到父容器
val view = LayoutInflater.from(context).inflate(R.layout.child_layout, parentView, true)

注意:這種情況下返回的是parentView,不是新創建的View!

我們項目中的用法

項目中的代碼:

val shareView = LayoutInflater.from(this@StallMainActivityV1).inflate(R.layout.share_header_stall_main_layout, null)

這個寫法是正確的

  • 傳入 null 作為parent參數
  • 創建了一個獨立的View,適合分享圖片生成場景
  • 符合分享場景的使用規范

但是,這樣的寫法雖然正確,卻引出了另一個問題:View.post() 執行不到。

View.post() 工作原理詳解

View.post() 是干什么的?

簡單來說,View.post() 就是把一個任務扔到主線程的消息隊列里,等合適的時候再執行。

執行條件

View.post() 要正常工作,需要滿足幾個條件:

  1. View必須attached to window(附加到窗口)
  2. View必須在主線程的消息隊列中
  3. View必須有有效的Handler

為什么分享場景下執行不到?

在我們的分享場景中:

val shareView = LayoutInflater.from(context).inflate(R.layout.share_header_stall_main_layout, null)// shareView沒有被添加到任何父容器中!
// 所以它沒有attached to window
// 因此View.post()不會執行

關鍵點:這個 shareView 只是一個孤立的View對象,它沒有被添加到Activity的視圖層次結構中。

View的生命周期

要理解這個問題,需要了解View的生命周期:

  1. 創建:通過LayoutInflater創建View對象
  2. 測量:measure() - 確定View的大小
  3. 布局:layout() - 確定View的位置
  4. 繪制:draw() - 把View畫出來

只有當View被添加到視圖層次結構中時,才會經歷完整的生命周期。

當前生命周期狀態

在上述分享場景中,通過 LayoutInflater.inflate() 創建的 shareView 僅僅完成了 創建 階段:

  1. ? 創建: View對象已通過 LayoutInflater.inflate() 實例化
  2. ? 測量: 由于沒有添加到視圖層次結構中,measure()未執行,width/height都是0
  3. ? 布局: 沒有父容器,layout()未執行,位置未確定
  4. ? 繪制: 沒有進入視圖層次結構,draw()未執行,無法顯示

這就是為什么此時調用 View.post() 會失效 - View還處于"孤立"狀態,沒有進入完整的生命周期流程。只有將View添加到Activity的視圖層次結構中(比如通過 addView() 方法),才會觸發后續的測量、布局和繪制過程。

實際的問題表現

StallAvatarView 中:

iv_avatar.post {// 這里執行不到的原因:// 1. iv_avatar沒有attached to window// 2. iv_avatar.width 和 iv_avatar.height 都是0val w = if (iv_avatar.width > 0) iv_avatar.width else sizeval h = if (iv_avatar.height > 0) iv_avatar.height else size// ...
}

解決方案

方案1:檢查View狀態(推薦)

使用isAttachedToWindow,也是我最后修復這個歷史問題所使用的解決方案:

isAttachedToWindow 的工作原理

isAttachedToWindow 是 View 類中的一個屬性,用于判斷當前 View 是否已經被添加到視圖層次結構中。它的工作原理如下:

1. 狀態變化時機
  • 當 View 通過 addView() 等方法被添加到 Window 時,會調用 onAttachedToWindow(),此時 isAttachedToWindow = true
  • 當 View 通過 removeView() 等方法從 Window 中移除時,會調用 onDetachedFromWindow(),此時 isAttachedToWindow = false
2. 生命周期流程

View 的生命周期狀態轉換過程如下:

  1. View 對象創建后,初始狀態 isAttachedToWindow = false
  2. 調用 Activity.setContentView()ViewGroup.addView() 時:
    • 觸發 onAttachedToWindow()
    • 設置 isAttachedToWindow = true
    • 開始 measure、layout、draw 等生命周期
  3. 調用 ViewGroup.removeView() 或 Activity 銷毀時:
    • 觸發 onDetachedFromWindow()
    • 設置 isAttachedToWindow = false
    • 停止生命周期,釋放資源
3. 實際應用價值

檢查 isAttachedToWindow 的主要作用:

  1. 避免無效操作 - 在 View 未添加到窗口時,很多操作(如 post())都無法正常執行
  2. 判斷時機 - 可以用來判斷是否可以進行需要 View 完成布局后才能執行的操作
  3. 防止內存泄漏 - 在 onDetachedFromWindow() 時及時釋放資源
  4. 控制生命周期 - 自定義 View 時在合適的時機執行初始化和清理
因此可以這樣調整代碼:
  • 在進行 View 相關異步操作前,先檢查 isAttachedToWindow 狀態
  • 對于未 attached 的情況,可以采用備選方案(如使用預設的默認值)
fun showAvatarOrFirstChar(supply_avatar: String,supply_name: String,// ... 其他參數avatarComplete: (() -> Unit)? = null
) {// ... 前面的代碼if (supply_avatar.isBlank()) {// 處理無頭像情況avatarComplete?.invoke()} else {// 設置View狀態tv_avatar.hide()iv_avatar.view()// 關鍵:檢查View是否已經attachedif (isAttachedToWindow) {// 正常情況:View已經在視圖層次結構中iv_avatar.post {val w = if (iv_avatar.width > 0) iv_avatar.width else sizeval h = if (iv_avatar.height > 0) iv_avatar.height else sizeloadAvatar(supply_avatar, w, h, avatarComplete)}} else {// 特殊情況:View還沒有attached(比如分享場景)// 直接使用傳入的size參數loadAvatar(supply_avatar, size, size, avatarComplete)}}
}private fun loadAvatar(supply_avatar: String,width: Int,height: Int,avatarComplete: (() -> Unit)?
) {GlideKHelper.loadImageToBitmap(context, supply_avatar,R.drawable.shape_circle_solid_f0f0f0,width, height) { bmp ->iv_avatar.setImageBitmap(bmp)avatarComplete?.invoke()}
}

方案2:手動測量View

可以設置為MeasureSpec.UNSPECIFIED之后,手動測量
在Android中,View.MeasureSpec.UNSPECIFIED的作用是告訴View它可以按照自己的意愿設置大小,不受任何限制。這是因為:

  1. MeasureSpec的組成
    MeasureSpec是一個32位的整型值,高2位表示測量模式(mode),低30位表示測量大小(size)。測量模式有三種:
  • UNSPECIFIED(0): 父容器不對View進行任何限制,要多大給多大
  • EXACTLY(1): 父容器已經檢測出View所需要的精確大小
  • AT_MOST(2): 父容器指定了一個最大值,View的大小不能超過這個值

默認情況下,測量模式取決于View的LayoutParams和父容器的MeasureSpec:

  1. 對于match_parent
  • 父容器是EXACTLY: 子View也是EXACTLY,大小為父容器剩余空間
  • 父容器是AT_MOST: 子View是AT_MOST,最大值為父容器剩余空間
  1. 對于wrap_content
  • 父容器是EXACTLY/AT_MOST: 子View是AT_MOST,最大值為父容器剩余空間
  • 父容器是UNSPECIFIED: 子View也是UNSPECIFIED
  1. 對于具體數值(如100dp)
  • 不管父容器是什么模式,子View都是EXACTLY,大小為指定值
  1. 為什么使用UNSPECIFIED
    當我們手動測量一個未添加到視圖層級的View時,使用UNSPECIFIED是最合適的,因為:
  • View此時沒有父容器,不需要考慮父容器的限制
  • 讓View按照自己的wrap_content邏輯來計算實際需要的尺寸
  • 避免其他模式可能帶來的尺寸限制
  1. 測量過程
    當使用UNSPECIFIED時:
  • View會根據自己的內容大小來決定測量結果
  • 對于ViewGroup,它會遞歸測量所有子View
  • 最終得到的measuredWidth和measuredHeight就是View真實需要的尺寸
  1. 實際應用
    在分享圖片生成等場景下,我們需要提前知道View的尺寸,此時使用UNSPECIFIED測量是最佳選擇:
// 對于沒有attached的View,手動觸發測量和布局
val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
shareView.measure(measureSpec, measureSpec)
shareView.layout(0, 0, shareView.measuredWidth, shareView.measuredHeight)// 現在可以獲取到正確的尺寸了

方案3:使用ViewTreeObserver

ViewTreeObserver.OnGlobalLayoutListener 是一個非常有用的回調接口,它會在布局發生變化時被觸發。具體來說,在以下情況下會觸發:(不過在目前的業務環境下,使用這個回調接口不太符合。)

  1. View的尺寸發生變化
  • View的寬高改變
  • View的padding改變
  • View的margin改變
  1. View的位置發生變化
  • View在父容器中的位置改變
  • View的translation屬性改變
  • View的scroll位置改變
  1. View層級發生變化
  • 添加或刪除子View
  • View的可見性改變(VISIBLE/GONE/INVISIBLE)
  1. 特殊時機
  • Activity/Fragment首次布局完成
  • 軟鍵盤彈出或收起
  • 屏幕旋轉
  • 系統窗口(如狀態欄)顯示或隱藏

需要注意的是:

  • 一個布局變化可能會觸發多次回調
  • 建議在獲取到需要的信息后立即移除監聽器
  • 不要在回調中執行耗時操作
  • 如果View已被移除,回調可能不會觸發

示例代碼:

shareView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {override fun onGlobalLayout() {shareView.viewTreeObserver.removeOnGlobalLayoutListener(this)// 現在布局完成了,可以安全地獲取View尺寸}
})

總結

LayoutInflater.inflate() 使用建議

  1. 分享、Dialog場景:傳 null 作為parent
  2. RecyclerView ViewHolder:傳 parent, false
  3. 直接添加到容器:傳 parent, true

View.post() 使用建議

  1. 確保View已經attached:使用 isAttachedToWindow 檢查
  2. 分享等特殊場景:考慮直接執行,不使用post
  3. 需要View尺寸時:確保View已經經過測量和布局

最佳實踐

// ? 正確的分享View創建方式
val shareView = LayoutInflater.from(context).inflate(R.layout.share_layout, null)// ? 安全的View.post使用方式
if (view.isAttachedToWindow) {view.post { /* 執行需要View尺寸的操作 */ }
} else {// 直接執行或使用其他方式
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/82771.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/82771.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/82771.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2.linux目錄切換命令:cd與pwd以及路徑與路徑符

cd 切換當前工作目錄 cd [linux路徑0] cd沒有選項,直接執行,只有參數.如果沒有參數,表示回到用戶的home目錄 pwd 無參,無選項,直接打印當前工作目錄的絕對路徑 路徑 相對路徑 以當前目錄為起點,路徑描述無需使用/開頭 # cd Desktop 絕對路徑 路徑描述需要以/開頭 cd…

摩爾條紋 原理以及matlab 實現

一、簡介 莫爾條紋的形成原理-CSDN博客 “莫爾”一詞源于法文“Moire”,其原本的含義是“波動”或者“起波紋的”。早在古代時期,人們便偶然發現,當把兩塊薄的絲綢織物相互疊加放置時,能夠看到一種呈現不規則形態的花紋。此后&a…

【海康USB相機被HALCON助手連接過后,MVS顯示無法連接故障。】

在Halcon里使用助手調用海康USB相機時,如果這個界面點擊了【是】 那么恭喜你,相機只能被HALCON調用使用,使用MVS或者海康開發庫,將查找不到相機 解決方式: 右鍵桌面【此電腦】圖標 ->選擇【管理】 ->選擇【設備…

數據治理是什么意思?數據治理平臺有哪些?

目錄 一、數據治理的概念 1. 數據治理的定義 2. 數據治理的目標 二、數據治理的實施流程 1. 規劃階段 2. 評估階段 3. 執行階段 4. 監控與評估階段 三、常見的數據治理平臺 1. FineDataLink 2. IBM InfoSphere Information Governance Catalog 四、總結 隨著企業業…

高效工具-tldr

喜歡使用命令操作的小伙伴,肯定會遇到一個問題,查看命令如何使用時,會列出一堆,特別是英文,看的直發懵。前段時間我也是研究git命令,也遇到了類似的問題。好在有大數據,幫我普及相關的知識。 在…

安卓添加設備節點權限和selinux訪問權限

# 1 修改設備節點權限及配置屬性設置節點值 ## 1.1 修改設備節點權限 ### 1.1.1 不會手動卸載的節點 在system/core/rootdir/init.rc中添加節點權限 在on boot下面添加 chown system system /sys/kernel/usb/host chmod 0664 /sys/kernel/usb/host ### 1.1.2 支持熱插拔的…

ssm學習筆記(尚硅谷) day1

創建新項目 maven的聚合 1. 標記父類項目 標簽<packaging>pom</packaging>表示將該項目標記為父類項目&#xff0c;必須添加。 以下是標簽<packing>的常見取值 groupId在pom.xml中&#xff0c;可以從pom.xml直接修改。 2. 通過<modules>添加子項目…

基于Java,SpringBoot,Vue,UniAPP醫院預約掛號買藥就診病例微信小程序系統設計

摘要 隨著醫療信息化的不斷推進以及“互聯網醫療”模式的廣泛普及&#xff0c;傳統醫院掛號流程中存在的排隊時間長、資源分配不均等問題日益凸顯&#xff0c;急需通過數字化手段加以解決。本研究設計并實現了一套基于Java、SpringBoot、Vue與UniAPP技術棧的醫院預約掛號微信小…

Axure項目實戰:運輸統計頁引入echarts實現高保真設計(JS代碼ctrl+c ctrl+v懂得來)

親愛的小伙伴,在您瀏覽之前,煩請關注一下,在此深表感謝!如有幫助請訂閱專欄! Axure產品經理精品視頻課已登錄CSDN可點擊學習https://edu.csdn.net/course/detail/40420 案例視頻: 數據統計引入echarts示例演示 課程主題:運輸統計頁引入echarts實現高保真設計 主要內容…

python打卡day39

圖像數據與顯存 知識點回顧 圖像數據的格式&#xff1a;灰度和彩色數據模型的定義顯存占用的4種地方 模型參數梯度參數優化器參數數據批量所占顯存神經元輸出中間狀態 batchisize和訓練的關系 作業&#xff1a;今日代碼較少&#xff0c;理解內容即可 在 PyTorch 中&#xff0c;…

15.1 【基礎項目】使用 HTML、CSS 和 TypeScript 構建的簡單計數器應用

一個簡單的計數器應用是學習如何集成 HTML、CSS 和 TypeScript 的絕佳項目。該應用允許用戶對計數值進行增加、減少和重置&#xff0c;展示了 TypeScript 中基本的 DOM 操作和事件處理。 我們將構建的內容 我們將創建一個具有以下功能的計數器應用&#xff1a; 增加計數值減…

RT-Thread源碼閱讀(3)——內核對象管理

_object_container對象容器數組 在RT-Thread操作系統中&#xff0c;_object_container數組的作用是按類型分類管理內核對象&#xff0c;提供高效的類型檢查、資源管理和統計功能 struct rt_list_node {struct rt_list_node *next; /**< point to…

《智能醫學》征稿通知:7天可見刊,專科及以上可發表

香港科學出版社(Hong Kong Scientific Publishers Journals)是一家全球獨立高質量的學術出版機構&#xff0c;遵循國際開放獲取的出版(OA)原則。現已與科檢易學術攜手共同征集高質量文章。目前可出版來自高等學校、科研院所和企業的先進科技成果。包括理、工、農、醫、經、管、…

如何利用categraf的exec插件實現對Linux主機系統用戶及密碼有效期進行監控及告警?

需求描述 Categraf作為夜鶯監控平臺的數據采集工具&#xff0c;為了保障Linux主機的安全&#xff0c;需要實現對系統用戶密碼有效期的監控&#xff0c;并在密碼即將到期時及時告警&#xff0c;以提醒運維人員更改密碼。本章將詳細介紹如何利用Categraf的exec插件來實現這一功能…

RV1126-OPENCV 交叉編譯

一.下載opencv-3.4.16.zip到自己想裝的目錄下 二.解壓并且打開 opencv 目錄 先用 unzip opencv-3.4.16.zip 來解壓 opencv 的壓縮包&#xff0c;并且進入 opencv 目錄(cd opencv-3.4.16) 三. 修改 opencv 的 cmake 腳本的內容 先 cd platforms/linux 然后修改 arm-gnueabi.to…

如何加載私鑰為 SecKeyRef

本文介紹如何在 iOS/macOS 下將私鑰加載為 SecKeyRef&#xff0c;涵蓋 PEM 格式的 ECC 密鑰讀取、X9.63 數據構建、以及與 Keychain 的集成。 1. 使用 SecKeyCreateWithData 加載私鑰 Apple 提供的 SecKeyCreateWithData 方法可以直接將密鑰數據加載為 SecKeyRef 對象。 SecK…

Missashe考研日記—Day44-Day50

Missashe考研日記—Day44-Day50 寫在面前 本系列博客用于記錄博主一周的學習進度&#xff0c;具體知識總結在目前已有的筆記中&#xff1a;1.高數強化學習筆記2.計網復習筆記3.新增&#xff1a;線代題型總結 專業課408 這周先是把計網第三章數據鏈路層剩下的局域網以及之后…

Windows下安裝并使用kubectl查看K8S日志

【1】安裝kubectl 官網文檔&#xff1a;https://kubernetes.io/zh-cn/docs/tasks/tools/install-kubectl-windows/ 下載后得到 kubectl.exe&#xff0c;放到一個目錄下&#xff0c;然后配置環境變量。 此時CMD 進入DOS命令窗口 kubectl version【2】配置config文件 其實就是…

攻防世界János-the-Ripper

打開壓縮包是一個文件&#xff0c;用010Editor打開可以發現里面有隱藏文件flag.txt 此時想到分離文件&#xff0c;利用binwalk工具 利用binwalk生成出的是一個壓縮包&#xff0c;解壓縮但是發現竟然解壓需要密碼 這里就可以開始暴力破解密碼了&#xff0c;這里我用的是ARCHPR工…