1. 程式人生 > >高仿iOS的BlurDialog

高仿iOS的BlurDialog

文/BlackSwift(簡書作者)
原文連結:http://www.jianshu.com/p/1e2d68183c3c
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。

本文主要討論Dialog的blur背景與泛談window中的各種view。

Github上有許多仿iOS的dialog,但是都沒有讓背景模糊,第三方的view太坑,不如自己對著iOS模擬器的圖與動畫做出來。耗費了很久時間終於搞定了,如下圖,左邊是仿iOS的dialog,右邊是SupportV7的dialog。


BlurDialog

Dialog 生命週期的簡要分析

在建構函式時,建立了一個window,並設定了相關window屬性。

當呼叫 show()

時,首先呼叫了 onCreateonStart(都是一些無關緊要的方法),最後通過 WindowManager 添加了 window 中的 DecoView,從這裡開始,view就要開始走繪製流程了。

當呼叫 dismiss() 時,通過 WindowManager 移除了 View,並呼叫
onStop() 執行可能的清理任務。

@see #Dialog (setUp window inside)

@see #show()

- @see #onCreate(Bundle) (call setContentView here)

- @see #onStart()

- mWindowManager.addView(mDecor, l);

@see
#dismiss() - mWindowManager.removeViewImmediate(mDecor); - @see #onStop() (clean work)

如果我們需要自定義一個dialog,只需要在 show() 之前配置好 window 的屬性,以及在 onCreate() 中將自定義的 xml 佈局初始化即可。

mWindowManager.addview() 之前,除非指定了 view 的dp高寬,否則輸出的view高度等資料將是0,-1之類的值。為了獲得準確的值,可以使用 view.post() 將訊息入隊,這樣可以獲取到正確的資料。

BlurAlertDialog 的實現

通過post入隊即可,入隊主要是為了準確測量偏移量。

//初始化 BlurDrawable 的樣式
//`mRvFragCard`表示在底層的將要被模糊的View
BlurDrawable drawable = new BlurDrawable(mRvFragCard);

dialog.getWindow().getDecorView().post(new Runnable() {
  @Override public void run() {
      //設定邊緣圓弧的 dp
    drawable.setCornerRadius(xxDp);
    // 設定繪製偏移量
    // 座標為相對整個螢幕,dialog最左邊,最上面的點
    drawable.setDrawOffset(x,y);
    dialog.getWindow().setBackgroundDrawable(drawable);
  }
});
dialog.show();

注意不能用系統自帶的dialog,因為它的內部是不開放的(也就是紅色的程式碼),window建立流程也不透明,可能會丟擲異常。

示例地址

本文專案

本部分完,需要ui的直接clone即可。

以下為冗長的理論資料

Window 與 WindowManager

  • window是對整個view的一套管理,對開發者來說僅僅是介面。PhoneWindow 是對 window 的抽象方法的實現,手機ROM廠商可以修改或者實現它,而開發者只能反射呼叫PhoneWindow的一些功能。

  • WindowManager是系統服務,負責view繪製。它對開發中看來也只是一個介面,通過 getSystemService 即可獲得。

介面由ROM實現

/system/framework/framework.jar

反編譯jar後,可以獲得物件 android.view.WindowManagerGlobal,具體流程可以看這裡,我們只需要知道這個函式涉及到AIDL通訊,並且它是非同步回撥的即可。

在國產ROM常見的狀態列字型變色,懸浮Activity等特色功能,都是由 WindowManager 負責繪製的,通過在 Window 中寫入某些flag,之後重繪的時候就能顯示出獨有的樣式。

Dialog 下的 Window 佈局

此場景適用於最原始的 Activity 與 Dialog ,它們的 ViewTree 佈局如下

DecoView 

DecorContentParent

ContentView(android.R.id.content)

你寫的xml佈局

通過 Dump 工具可以驗證:


Dump

1. DecoView

PhoneWindow 中,它的變數名為 mDecor , 它是 window 中的 top-level 的view,通過 installDecor() 進行構造,它預設的大小是 wrap_content,我們通常所說的 window.setXXX() 本質上呼叫的就是 DecoView(與它的子view)

網上有部分文章說 DecoView 不是top-level 的 View,這個說法是錯的,各位可以手動設定 id 測試

2. DecorContentParent

PhoneWindow 中,它的變數名為 mDecorContentParent,通過它可以設定 Window 中的Title,requestWindowFeature 等功能,它是一個寬泛的層,層次可能是一層(在NoActionBar,notitle的條件下),可能有多個層,上圖就有3層。在實際開發中,我們一般也用不到(如果非要用的話,還是隻能反射呼叫)。

舉個例子,在最新的 AppCompatActivity 中,它重寫的setContentView 中通過 ensureSubDecor() 方法手動填充幾個夾層View,這個算一個應用案例

@Override
public void setContentView(int resId) {
    //對mWindow.setContentView()的一道包裝
    //新增部分View, 比如Actionbar/Title/Toolbar
    ensureSubDecor();
    .....
}

3. ContentView

PhoneWindow 中,它的變數名為 mContentParent。我們日常接觸的就是 ContentView 了,它包裝著我們寫的xml佈局

contentView可以通過ID獲得,這個id是內建的,如下兩個是等價的

android.R.id.content
Window.ID_ANDROID_CONTENT

4. xml佈局

我們自己寫的xml佈局,需要注意 inflate 方法中的boolean值,它控制當前view是否依附到 parent 中。

如果view本身需要被add時(比如dialog,activity),呼叫setContentView()即可,它將生成view並依附到parent中。

如果這個view並沒有parent(比如viewholder), 選擇不依附即可

inflater.inflate(ResId, null, false);

總結

  1. 儘量在setcontentView之前呼叫window屬性
  2. 涉及到長寬等內容時,因為view的顯示是非同步的,所以需要post傳送,以免獲得到的結果為0