1. 程式人生 > >Android視窗機制(五)最終章:WindowManager.LayoutParams和Token以及其他視窗Dialog,Toast

Android視窗機制(五)最終章:WindowManager.LayoutParams和Token以及其他視窗Dialog,Toast

Android視窗機制系列

Android視窗機制(一)初識Android的視窗結構
Android視窗機制(二)Window,PhoneWindow,DecorView,setContentView原始碼理解
Android視窗機制(三)Window和WindowManager的建立與Activity
Android視窗機制(四)ViewRootImpl與View和WindowManager
Android視窗機制(五)最終章:WindowManager.LayoutParams和Token以及其他視窗Dialog,Toast

前面幾篇文章基本介紹完Activity上的視窗機制,但是我們常見的視窗就還有Dialog,Toast這些,本篇文章就來介紹這兩個的視窗機制以及WindowManager.LayoutParams和Token

WindowManager.LayoutParams

首先,先跟大家介紹這個WindowManager.LayoutParams,在前面幾篇文章中,都有出現過這個LayoutParams,我們看下具體的原始碼。
翻譯參考

  public static class LayoutParams extends ViewGroup.LayoutParams
            implements Parcelable {
        //視窗的絕對XY位置,需要考慮gravity屬性
        public int x;
        public int y;
        //在橫縱方向上為相關的View預留多少擴充套件畫素,如果是0則此view不能被拉伸,其他情況下擴充套件畫素被widget均分
        public float horizontalWeight;
        public float verticalWeight;
        //視窗型別
        //有3種主要型別如下:
        //ApplicationWindows取值在FIRST_APPLICATION_WINDOW與LAST_APPLICATION_WINDOW之間,是常用的頂層應用程式視窗,須將token設定成Activity的token;
        //SubWindows取值在FIRST_SUB_WINDOW和LAST_SUB_WINDOW之間,與頂層視窗相關聯,需將token設定成它所附著宿主視窗的token;
        //SystemWindows取值在FIRST_SYSTEM_WINDOW和LAST_SYSTEM_WINDOW之間,不能用於應用程式,使用時需要有特殊許可權,它是特定的系統功能才能使用;
        public int type;

        //WindowType:開始應用程式視窗
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //WindowType:所有程式視窗的base視窗,其他應用程式視窗都顯示在它上面
        public static final int TYPE_BASE_APPLICATION   = 1;
        //WindowType:普通應用程式視窗,token必須設定為Activity的token來指定視窗屬於誰
        public static final int TYPE_APPLICATION        = 2;
        //WindowType:應用程式啟動時所顯示的視窗,應用自己不要使用這種型別,它被系統用來顯示一些資訊,直到應用程式可以開啟自己的視窗為止
        public static final int TYPE_APPLICATION_STARTING = 3;
        //WindowType:結束應用程式視窗
        public static final int LAST_APPLICATION_WINDOW = 99;

        //WindowType:SubWindows子視窗,子視窗的Z序和座標空間都依賴於他們的宿主視窗
        public static final int FIRST_SUB_WINDOW        = 1000;
        //WindowType: 面板視窗,顯示於宿主視窗的上層
        public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
        //WindowType:媒體視窗(例如視訊),顯示於宿主視窗下層
        public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
        //WindowType:應用程式視窗的子面板,顯示於所有面板視窗的上層
        public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
        //WindowType:對話方塊,類似於面板視窗,繪製類似於頂層視窗,而不是宿主的子視窗
        public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
        //WindowType:媒體資訊,顯示在媒體層和程式視窗之間,需要實現半透明效果
        public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
        //WindowType:子視窗結束
        public static final int LAST_SUB_WINDOW         = 1999;

        //WindowType:系統視窗,非應用程式建立
        public static final int FIRST_SYSTEM_WINDOW     = 2000;
        //WindowType:狀態列,只能有一個狀態列,位於螢幕頂端,其他視窗都位於它下方
        public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
        //WindowType:搜尋欄,只能有一個搜尋欄,位於螢幕上方
        public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
        //WindowType:電話視窗,它用於電話互動(特別是呼入),置於所有應用程式之上,狀態列之下
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        //WindowType:系統提示,出現在應用程式視窗之上
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
        //WindowType:鎖屏視窗
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //WindowType:資訊視窗,用於顯示Toast
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //WindowType:系統頂層視窗,顯示在其他一切內容之上,此視窗不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
        //WindowType:電話優先,當鎖屏時顯示,此視窗不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        //WindowType:系統對話方塊
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //WindowType:鎖屏時顯示的對話方塊
        public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
        //WindowType:系統內部錯誤提示,顯示於所有內容之上
        public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
        //WindowType:內部輸入法視窗,顯示於普通UI之上,應用程式可重新佈局以免被此視窗覆蓋
        public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
        //WindowType:內部輸入法對話方塊,顯示於當前輸入法視窗之上
        public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
        //WindowType:牆紙視窗
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //WindowType:狀態列的滑動面板
        public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
        //WindowType:安全系統覆蓋視窗,這些窗戶必須不帶輸入焦點,否則會干擾鍵盤
        public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
        //WindowType:拖放偽視窗,只有一個阻力層(最多),它被放置在所有其他視窗上面
        public static final int TYPE_DRAG               = FIRST_SYSTEM_WINDOW+16;
        //WindowType:狀態列下拉麵板
        public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
        //WindowType:滑鼠指標
        public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
        //WindowType:導航欄(有別於狀態列時)
        public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
        //WindowType:音量級別的覆蓋對話方塊,顯示當用戶更改系統音量大小
        public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
        //WindowType:起機進度框,在一切之上
        public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
        //WindowType:假窗,消費導航欄隱藏時觸控事件
        public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
        //WindowType:夢想(屏保)視窗,略高於鍵盤
        public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
        //WindowType:導航欄面板(不同於狀態列的導航欄)
        public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
        //WindowType:universe背後真正的窗戶
        public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
        //WindowType:顯示視窗覆蓋,用於模擬輔助顯示裝置
        public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
        //WindowType:放大視窗覆蓋,用於突出顯示的放大部分可訪問性放大時啟用
        public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
        //WindowType:......
        public static final int TYPE_KEYGUARD_SCRIM           = FIRST_SYSTEM_WINDOW+29;
        public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
        public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
        public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
        //WindowType:系統視窗結束
        public static final int LAST_SYSTEM_WINDOW      = 2999;

        //MemoryType:視窗緩衝位於主記憶體
        public static final int MEMORY_TYPE_NORMAL = 0;
        //MemoryType:視窗緩衝位於可以被DMA訪問,或者硬體加速的記憶體區域
        public static final int MEMORY_TYPE_HARDWARE = 1;
        //MemoryType:視窗緩衝位於可被圖形加速器訪問的區域
        public static final int MEMORY_TYPE_GPU = 2;
        //MemoryType:視窗緩衝不擁有自己的緩衝區,不能被鎖定,緩衝區由本地方法提供
        public static final int MEMORY_TYPE_PUSH_BUFFERS = 3;

        //指出視窗所使用的記憶體緩衝型別,預設為NORMAL 
        public int memoryType;

        //Flag:當該window對使用者可見的時候,允許鎖屏
        public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;
        //Flag:讓該window後所有的東西都成暗淡
        public static final int FLAG_DIM_BEHIND        = 0x00000002;
        //Flag:讓該window後所有東西都模糊(4.0以上已經放棄這種毛玻璃效果)
        public static final int FLAG_BLUR_BEHIND        = 0x00000004;
        //Flag:讓window不能獲得焦點,這樣使用者快就不能向該window傳送按鍵事
        public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
        //Flag:讓該window不接受觸控式螢幕事件
        public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
        //Flag:即使在該window在可獲得焦點情況下,依舊把該window之外的任何event傳送到該window之後的其他window
        public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
        //Flag:當手機處於睡眠狀態時,如果螢幕被按下,那麼該window將第一個收到
        public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
        //Flag:當該window對使用者可見時,讓裝置螢幕處於高亮(bright)狀態
        public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
        //Flag:讓window佔滿整個手機螢幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
        //Flag:window大小不再不受手機螢幕大小限制,即window可能超出螢幕之外
        public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;
        //Flag:window全屏顯示
        public static final int FLAG_FULLSCREEN      = 0x00000400;
        //Flag:恢復window非全屏顯示
        public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;
        //Flag:開啟抖動(dithering)
        public static final int FLAG_DITHER             = 0x00001000;
        //Flag:當該window在進行顯示的時候,不允許截圖
        public static final int FLAG_SECURE             = 0x00002000;
        //Flag:一個特殊模式的佈局引數用於執行擴充套件表面合成時到螢幕上
        public static final int FLAG_SCALED             = 0x00004000;
        //Flag:用於windows時,經常會使用螢幕使用者持有反對他們的臉,它將積極過濾事件流,以防止意外按在這種情況下,可能不需要為特定的視窗,在檢測到這樣一個事件流時,應用程式將接收取消運動事件表明,這樣應用程式可以處理這相應地採取任何行動的事件,直到手指釋放
        public static final int FLAG_IGNORE_CHEEK_PRESSES    = 0x00008000;
        //Flag:一個特殊的選項只用於結合FLAG_LAYOUT_IN_SC
        public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000;
        //Flag:轉化的狀態FLAG_NOT_FOCUSABLE對這個視窗當前如何進行互動的方法
        public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;
        //Flag:如果你設定了該flag,那麼在你FLAG_NOT_TOUNCH_MODAL的情況下,即使觸控式螢幕事件傳送在該window之外,其事件被髮送到了後面的window,那麼該window仍然將以MotionEvent.ACTION_OUTSIDE形式收到該觸控式螢幕事件
        public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
        //Flag:當鎖屏的時候,顯示該window
        public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
        //Flag:在該window後顯示系統的牆紙
        public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
        //Flag:當window被顯示的時候,系統將把它當做一個使用者活動事件,以點亮手機螢幕
        public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
        //Flag:消失鍵盤
        public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
        //Flag:當該window在可以接受觸控式螢幕情況下,讓因在該window之外,而傳送到後面的window的觸控式螢幕可以支援split touch
        public static final int FLAG_SPLIT_TOUCH = 0x00800000;
        //Flag:對該window進行硬體加速,該flag必須在Activity或Dialog的Content View之前進行設定
        public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
        //Flag:讓window佔滿整個手機螢幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
        //Flag:請求一個半透明的狀態列背景以最小的系統提供保護
        public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
        //Flag:請求一個半透明的導航欄背景以最小的系統提供保護
        public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;
        //Flag:......
        public static final int FLAG_LOCAL_FOCUS_MODE = 0x10000000;
        public static final int FLAG_SLIPPERY = 0x20000000;
        public static final int FLAG_LAYOUT_ATTACHED_IN_DECOR = 0x40000000;
        public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;

        //行為選項標記
        public int flags;

        //PrivateFlags:......
        public static final int PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED = 0x00000001;
        public static final int PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED = 0x00000002;
        public static final int PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS = 0x00000004;
        public static final int PRIVATE_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;
        public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 0x00000040;
        public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 0x00000080;
        public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100;
        public static final int PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR = 0x00000200;
        public static final int PRIVATE_FLAG_KEYGUARD = 0x00000400;
        public static final int PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS = 0x00000800;

        //私有的行為選項標記
        public int privateFlags;

        public static final int NEEDS_MENU_UNSET = 0;
        public static final int NEEDS_MENU_SET_TRUE = 1;
        public static final int NEEDS_MENU_SET_FALSE = 2;
        public int needsMenuKey = NEEDS_MENU_UNSET;

        public static boolean mayUseInputMethod(int flags) {
            ......
        }

        //SOFT_INPUT:用於描述軟鍵盤顯示規則的bite的mask
        public static final int SOFT_INPUT_MASK_STATE = 0x0f;
        //SOFT_INPUT:沒有軟鍵盤顯示的約定規則
        public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
        //SOFT_INPUT:可見性狀態softInputMode,請不要改變軟輸入區域的狀態
        public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
        //SOFT_INPUT:使用者導航(navigate)到你的視窗時隱藏軟鍵盤
        public static final int SOFT_INPUT_STATE_HIDDEN = 2;
        //SOFT_INPUT:總是隱藏軟鍵盤
        public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
        //SOFT_INPUT:使用者導航(navigate)到你的視窗時顯示軟鍵盤
        public static final int SOFT_INPUT_STATE_VISIBLE = 4;
        //SOFT_INPUT:總是顯示軟鍵盤
        public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
        //SOFT_INPUT:顯示軟鍵盤時用於表示window調整方式的bite的mask
        public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
        //SOFT_INPUT:不指定顯示軟體盤時,window的調整方式
        public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
        //SOFT_INPUT:當顯示軟鍵盤時,調整window內的控制元件大小以便顯示軟鍵盤
        public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
        //SOFT_INPUT:當顯示軟鍵盤時,調整window的空白區域來顯示軟鍵盤,即使調整空白區域,軟鍵盤還是有可能遮擋一些有內容區域,這時使用者就只有退出軟鍵盤才能看到這些被遮擋區域並進行
        public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
        //SOFT_INPUT:當顯示軟鍵盤時,不調整window的佈局
        public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
        //SOFT_INPUT:使用者導航(navigate)到了你的window
        public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;

        //軟輸入法模式選項
        public int softInputMode;

        //視窗如何停靠
        public int gravity;
        //水平邊距,容器與widget之間的距離,佔容器寬度的百分率
        public float horizontalMargin;
        //縱向邊距
        public float verticalMargin;
        //積極的insets繪圖表面和視窗之間的內容
        public final Rect surfaceInsets = new Rect();
        //期望的點陣圖格式,預設為不透明,參考android.graphics.PixelFormat
        public int format;
        //視窗所使用的動畫設定,它必須是一個系統資源而不是應用程式資源,因為視窗管理器不能訪問應用程式
        public int windowAnimations;
        //整個視窗的半透明值,1.0表示不透明,0.0表示全透明
        public float alpha = 1.0f;
        //當FLAG_DIM_BEHIND設定後生效,該變數指示後面的視窗變暗的程度,1.0表示完全不透明,0.0表示沒有變暗
        public float dimAmount = 1.0f;

        public static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f;
        public static final float BRIGHTNESS_OVERRIDE_OFF = 0.0f;
        public static final float BRIGHTNESS_OVERRIDE_FULL = 1.0f;
        public float screenBrightness = BRIGHTNESS_OVERRIDE_NONE;
        //用來覆蓋使用者設定的螢幕亮度,表示應用使用者設定的螢幕亮度,從0到1調整亮度從暗到最亮發生變化
        public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE;

        public static final int ROTATION_ANIMATION_ROTATE = 0;
        public static final int ROTATION_ANIMATION_CROSSFADE = 1;
        public static final int ROTATION_ANIMATION_JUMPCUT = 2;
        //定義出入境動畫在這個視窗旋轉裝置時使用
        public int rotationAnimation = ROTATION_ANIMATION_ROTATE;

        //視窗的標示符
        public IBinder token = null;
        //此視窗所在的包名
        public String packageName = null;
        //螢幕方向
        public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
        //首選的重新整理率的視窗
        public float preferredRefreshRate;
        //控制status bar是否顯示
        public int systemUiVisibility;
        //ui能見度所請求的檢視層次結構
        public int subtreeSystemUiVisibility;
        //得到關於系統ui能見度變化的回撥
        public boolean hasSystemUiListeners;

        public static final int INPUT_FEATURE_DISABLE_POINTER_GESTURES = 0x00000001;
        public static final int INPUT_FEATURE_NO_INPUT_CHANNEL = 0x00000002;
        public static final int INPUT_FEATURE_DISABLE_USER_ACTIVITY = 0x00000004;
        public int inputFeatures;
        public long userActivityTimeout = -1;

        ......
        public final int copyFrom(LayoutParams o) {
            ......
        }

        ......
        public void scale(float scale) {
            ......
        }

        ......
    }

可以看到在WindowManager.LayoutParams上有三種視窗型別type,對應為

  • **應用程式視窗 : **type值在 FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW 須將token設定成Activity的token
    eg: 前面介紹的Activity視窗,Dialog
  • 子視窗: type值在 FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW SubWindows與頂層視窗相關聯,需將token設定成它所附著宿主視窗的token
    eg: PopupWindow(想要依附在Activity上需要將token設定成Activity的token)
  • **系統視窗: type值在 FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW ** SystemWindows不能用於應用程式,使用時需要有特殊許可權,它是特定的系統功能才能使用。
    eg: Toast,輸入法等。

WindowManager.LayoutParams原始碼中也講到輸入法的問題,裡面有很多種模式,通過設定softInputMode來調整輸入法。這裡舉個常見例子吧,平時我們在Activity的底部放置EditText的時候,輸入法的彈出可能會遮擋住介面。
這裡通過設定相應的softInputMode就可以解決這個問題

<activity  
     android:name=".TestActivity"  
     android:windowSoftInputMode="stateVisible|adjustResize" >  
     <intent-filter>  
          <action android:name="android.intent.action.MAIN" />  
          <category android:name="android.intent.category.LAUNCHER" />  
     </intent-filter>  
</activity>  

或者

public class TestActivity extends AppCompatActivity {      
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);                 
        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
        setContentView(R.layout.activity_test);  
    }
}  

另外,三種類型裡面出現了個概念,就是token問題。
在應用程式視窗中,token是用來標識Activity的,一個Activity就對應一個token令牌
而在子視窗中,某個子視窗想要依附在對應的宿主視窗上設定要將token設定為對應宿主視窗的token。

token

token是用來表示視窗的一個令牌,只有符合條件的token才能被WMS通過新增到應用上。
我們來看下token的傳遞過程

首先對於Activity裡面的token,它的建立則是在AMS啟動Activity開始的,之後儲存在ActivityRecord.appToken中。而對於Activity中的token繫結到對應的Window上
我們知道,應用程式視窗的Activity視窗Window是在Activity建立過程中建立的,具體是在activity.attach方法中建立的。

 final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        ...
        mWindow = new PhoneWindow(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        //設定軟鍵盤
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        ...
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ...
        mWindowManager = mWindow.getWindowManager();
    }

追蹤token可看到最後傳遞到window.setWindowManager中

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

在setWindowManager中,appToken賦值到Window上,同時在當前Window上建立了WindowManager。

在將DecorView新增到WindowManager時候,會呼叫到windowManagerGlobal.addView方法

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
       ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
           ...
        }
       ...
    }

parentWindow.adjustLayoutParamsForSubWindow(wparams);方法裡面的重要一步就是給token設定值。不過在這以前要判斷parentWindow是否為null。

  • 如果是應用程式視窗的話,這個parentWindow就是activity的window
  • 如果是子視窗的話,這個parentWindow就是activity的window
  • 如果是系統視窗的話,那個parentWindow就是null

這個parentWindow則是在建立WindowManagerImpl的時候被賦值的

 private WindowManagerImpl(Display display, Window parentWindow) {
        mDisplay = display;
        mParentWindow = parentWindow;
    }

為什麼說子視窗中的parentWindow是Activity的window,因為子視窗中用到的是Activity的WindowManager,這裡會在下面分析到Dialog的時候說。
在Window.adjustLayoutParamsForSubWindow方法中

void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            if (wp.token == null) {
                View decor = peekDecorView();
                if (decor != null) {
                    wp.token = decor.getWindowToken();
                }
            }
            ...
        } else {
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
           ...
        }
     ...
    }

可以看到在adjustLayoutParamsForSubWindow通過wp.type來判斷當前視窗的型別,如果是子視窗型別,則wp.token = decor.getWindowToken();這裡賦值的是父視窗的W物件。關於W物件在下面講解。
如果是應用程式視窗,則走分支。一般應用程式視窗的話,mContainer為null,也就是mAppToken,就是Activity的mToken物件。

獲取到Token後就儲存在了LayoutParams裡面,接著到WindowManagerGlobal.addView中去。

   root = new ViewRootImpl(view.getContext(), display);

   view.setLayoutParams(wparams);

   mViews.add(view);
   mRoots.add(root);
   mParams.add(wparams);
   ...
   root.setView(view, wparams, panelParentView);</pre>

可以看到token儲存在WindowManager.LayoutParams中,之後再傳到了ViewRootImpl.setView

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
        
        final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
        final IWindowSession mWindowSession;
        ...
        public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ...
                mWindowAttributes.copyFrom(attrs);
                ...
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
            }
        }
    }
}

可以看到從WindowManagerGlobal中傳遞過來的params賦值到了ViewRootImpl中的mWindowAttributes中,之後呼叫到了ViewRootImpl.setView方法中的mWindowSession的addToDisplay方法,該方法用來請求WMS新增Window
mWindowSession的型別是IWindowSession它的實現類是Session,用來與WMS通訊

 final class Session extends IWindowSession.Stub
        implements IBinder.DeathRecipient {
      final WindowManagerService mService;
      ...
    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }
 }

我們看下WindowManagerService中是如何判斷這個token的

 public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
        int[] appOp = new int[1];
        //判斷許可權
        int res = mPolicy.checkAddPermission(attrs, appOp);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }
        ...
        final int type = attrs.type;
        synchronized(mWindowMap) {
            ...
            boolean addToken = false;
            WindowToken token = mTokenMap.get(attrs.token);
            if (token == null) {
                if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                    Slog.w(TAG, "Attempted to add application window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_INPUT_METHOD) {
                    Slog.w(TAG, "Attempted to add input method window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_VOICE_INTERACTION) {
                    Slog.w(TAG, "Attempted to add voice interaction window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_WALLPAPER) {
                    Slog.w(TAG, "Attempted to add wallpaper window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_DREAM) {
                    Slog.w(TAG, "Attempted to add Dream window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                if (type == TYPE_ACCESSIBILITY_OVERLAY) {
                    Slog.w(TAG, "Attempted to add Accessibility overlay window with unknown token "
                            + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
                token = new WindowToken(this, attrs.token, -1, false);
                addToken = true;
            } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                AppWindowToken atoken = token.appWindowToken;
                if (atoken == null) {
                    Slog.w(TAG, "Attempted to add window with non-application token "
                          + token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
                } else if (atoken.removed) {
                    Slog.w(TAG, "Attempted to add window with exiting application token "
                          + token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_APP_EXITING;
                }
                if (type == TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn) {
                    // No need for this guy!
                    if (localLOGV) Slog.v(
                            TAG, "**** NO NEED TO START: " + attrs.getTitle());
                    return WindowManagerGlobal.ADD_STARTING_NOT_NEEDED;
                }
            } else if (type == TYPE_INPUT_METHOD) {
                if (token.windowType != TYPE_INPUT_METHOD) {
                    Slog.w(TAG, "Attempted to add input method window with bad token "
                            + attrs.token + ".  Aborting.");
                      return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
            } else if (type == TYPE_VOICE_INTERACTION) {
                if (token.windowType != TYPE_VOICE_INTERACTION) {
                    Slog.w(TAG, "Attempted to add voice interaction window with bad token "
                            + attrs.token + ".  Aborting.");
                      return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
            } else if (type == TYPE_WALLPAPER) {
                if (token.windowType != TYPE_WALLPAPER) {
                    Slog.w(TAG, "Attempted to add wallpaper window with bad token "
                            + attrs.token + ".  Aborting.");
                      return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
            } else if (type == TYPE_DREAM) {
                if (token.windowType != TYPE_DREAM) {
                    Slog.w(TAG, "Attempted to add Dream window with bad token "
                            + attrs.token + ".  Aborting.");
                      return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
            } else if (type == TYPE_ACCESSIBILITY_OVERLAY) {
                if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
                    Slog.w(TAG, "Attempted to add Accessibility overlay window with bad token "
                            + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                }
            } else if (token.appWindowToken != null) {
                Slog.w(TAG, "Non-null appWindowToken for system window of type=" + type);
                // It is not valid to use an app token with other system types; we will
                // instead make a new token for it (as if null had been passed in for the token).
                attrs.token = null;
                token = new WindowToken(this, null, -1, false);
                addToken = true;
            }
        ...
        return res;
    }

可以看到在WMS中,做了很多的判斷,顯示判斷對應的許可權,如果不滿足則直接return到ViewRootImpl,如果滿足許可權,則在mWindowMap中去匹配params.token值,如果不滿足,則return對應的錯誤。都沒問題則開始新增Window。
而在ViewRootImpl則有判斷對應的返回值來報錯

  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
      ...
      res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
      if (res < WindowManagerGlobal.ADD_OKAY) {
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    switch (res) {
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
                        case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not for an application");
                        case WindowManagerGlobal.ADD_APP_EXITING:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- app for token " + attrs.token
                                    + " is exiting");
                       ...
                }

可以看到ViewRootImpl會根據WMS檢測token返回對應的情況,再去判斷是否報錯。

ViewRootImpl 和View的mAttachInfo

token與View的繫結,前面講到的token則是繫結在對應的Window,而對於View而言,它的所有繫結資訊都是存在一個靜態內部類AttachInfo中
在ViewRootImpl的建立中,可以看到

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        ...
        mWindow = new W(this);
        ...
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
        ...
    }

可以看到,這裡傳遞了mWindow和mWindowSession,而這裡賦值的mWindow物件,是通過new W(ViewRootImpl v)創建出來的,有留意的話,會發現在向WMS請求新增視窗,也就是在addToDisplay中,傳遞了mWindow這個引數

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);

這個mWindow也可以說是token,可以通過mWindow.asBinder()拿到。它是WMS回撥的介面。

 static class W extends IWindow.Stub {
        private final WeakReference<ViewRootImpl> mViewAncestor;
        private final IWindowSession mWindowSession;

        W(ViewRootImpl viewAncestor) {
            mViewAncestor = new WeakReference<ViewRootImpl>(viewAncestor);
            mWindowSession = viewAncestor.mWindowSession;
        }
        ...
 }

在AttachInfo的構造引數中

final static class AttachInfo {
        final IWindowSession mSession;
        final IWindow mWindow;
        final IBinder mWindowToken;
        AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
            mSession = session;
            mWindow = window;
            mWindowToken = window.asBinder();
            mDisplay = display;
            mViewRootImpl = viewRootImpl;
            mHandler = handler;
            mRootCallbacks = effectPlayer;
        }
        ....
}

IWindow mWindow:WMS回撥應用程式的Binder介面
WindowSession mSession : 就是訪問wms的Binder介面
IBinder mWindowToken : 它的賦值是window.asBinder,代表的是W物件,IWindow是通過new W建立的。mWindowToken也是WMS和應用程式互動的Binder介面。獲取到後就可以通過view.getWindowToken獲取

可以說AttachInfo代表了一系列繫結的狀態資訊,接著通過ViewRootImpl賦值到每個Window上的View上,如何賦值呢?
在ViewRootImpl的setView過程中,呼叫到了View的繪製performTraversals,這些前幾篇有講過,在這個方法中

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
        ...
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        ...
}

而在View這個方法中

 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        //System.out.println("Attached! " + this);
        mAttachInfo = info;
        if (mOverlay != null) {
            mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
        }
        ...
 }

會判斷是否是ViewGroup,如果是,則呼叫到ViewGroup裡面的方法

  void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
        super.dispatchAttachedToWindow(info, visibility);
        mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            child.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, child.getVisibility()));
        }
       ...
  }

可以看到在這個方法中遍歷呼叫了dispatchAttachedToWindow去賦值AttachInfo,而這些AttachInfo在同一個ViewGroup則是相同的值。之後View就獲得了這些繫結資訊。

Dialog

好了,做了那麼多鋪墊,可以開始看其他視窗了。
先看下Dialog的建立

 Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == 0) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }

        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

我們知道,關於Dialog的建立過程中要傳入引數Activity,主要是Dialog的建立過程與Activity相似,它同時也需要一些主題資源也就是ContextThemeWrapper,但是Dialog只是一個類,它並沒有繼承於ContextThemeWrapper,顧也就需要繼承於ContextThemeWrapper的Activity來結合使用。

可以看到,在上面的構造方法中,傳入的Context的是Activity的Context,接著獲取了一個WindowManager,這裡的WindowManager是通過context.getSystemService(Context.WINDOW_SERVICE),而這裡的Context是Activity,我們看下Activity裡面這個方法

 @Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        ...
        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }

可以看到這裡返回的WindowManager也就是Activity的WindowManager。
接著建立了一個新的Window,型別是PhoneWindow,與Activity的Window相比,是不同的物件。Dialog與Activity,同個WindowManager,不同Window。接著設定了Callback介面回撥,這也是Dialog能夠接受到按鍵事件的原因。接著呼叫setWindowManager設定到Window中。注意這個方法引數:這裡第二個引數傳遞的是null,也就是token為null

public void setWindowManager(WindowManager wm, IBinder appToken, String appName){
    ...
}

token居然為null,那麼Dialog到底是如何依附在Activity上的,我們看下show方法

public void show() {
        ...
        if (!mCreated) {
            dispatchOnCreate(null);
        }

        onStart();
        mDecor = mWindow.getDecorView();
        ...
        WindowManager.LayoutParams l = mWindow.getAttributes();
        ...
        try {
            mWindowManager.addView(mDecor, l);
            mShowing = true;
   
            sendShowMessage();
        } finally {
        }
    }

可以看到,show方法會先呼叫dispatchOnCreate來建立,最後會呼叫到onCreate

/**
     * Similar to {@link Activity#onCreate}, you should initialize your dialog
     * in this method, including calling {@link #setContentView}.
     * @param savedInstanceState If this dialog is being reinitalized after a
     *     the hosting activity was previously shut down, holds the result from
     *     the most recent call to {@link #onSaveInstanceState}, or null if this
     *     is the first time.
     */
    protected void onCreate(Bundle savedInstanceState) {
    }

可以看到,這個跟Activity相似,只不過在這裡Dialog是空的,但是它的子類,AlertDialog這些都是重寫了它,既然與Activity相似,那也就需要setContentView(這個很重要)了。
接著呼叫到

mDecor = mWindow.getDecorView();

這個mWindow就是前面建立的PhoneWindow例項,前面明明沒建立DecorView啊,為啥這裡Window能得到DecorView啊。咦,沒錯,你可能會猜到是在setContentView中建立的,因為這點跟Activity很像。

 public void setContentView(@LayoutRes int layoutResID) {
        mWindow.setContentView(layoutResID);
    }

可以看到這裡呼叫到了window.setContentView,而在第二篇文章也講過,Activity.setContentView,實際上也是呼叫到了window.setContentView,在它的實現類PhoneWindow.setContentView中就會建立DecorView了。

接著到了

 WindowManager.LayoutParams l = mWindow.getAttributes();

看下getAttributes

 public final WindowManager.LayoutParams getAttributes() {
        return mWindowAttributes;
    }

mWindowAttributes則是Window的一個成員變數

private final WindowManager.LayoutParams mWindowAttributes =
        new WindowManager.LayoutParams();

可以看到這裡是個預設建立

public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;
            format = PixelFormat.OPAQUE;
        }

可以看到這裡的型別是應用程式型別,對應WindowManager.LayoutParams說明是

//WindowType:普通應用程式視窗,token必須設定為Activity的token來指定視窗屬於誰
public static final int TYPE_APPLICATION        = 2;

接著呼叫到了 mWindowManager.addView(mDecor, l); 不過這裡呼叫到的是Activity的WindowManager,之後就到了WindowGlobal.addView,ViewRootImpl.setView,addToDisplay。和Activity的新增一致。也就是說因為傳入的引數是Activity的context,使得在新增視窗的時呼叫的是Activity的WindowManager,而Activity的WindowManager則儲存了對應的token,所以Dialog才可以被新增。如果此時傳遞的是getApplication或者是Service,則在ViewRootImpl.setView中會報錯,找不到對應的token,這也就是我們設定Dialog的時候要傳遞Activity的原因。

非Activity報錯

Toast

講完了Dialog就來講講一個系統視窗Toast,它與Dialog,Activity不同。我們從平常用法開始

Toast.makeText(MainActivity.this , "Hohohong" , Toast.LENGTH_SHORT);

在Toast的makeText方法中

/**
     * Make a standard toast that just contains a text view.
     *
     * @param context  The context to use.  Usually your {@link android.app.Application}
     *                 or {@link android.app.Activity} object.
     * @param text     The text to show.  Can be formatted text.
     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
     *                 {@link #LENGTH_LONG}
     *
     */
    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        Toast result = new Toast(context);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);
        
        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

可以看到這裡Context的說明允許Application或者Activity了,接著建立Toast物件,例項化預設的佈局。
我們看下構造方法

   public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }

在構造方法中,建立了TN物件,這個TN又是什麼

 private static class TN extends ITransientNotification.Stub {
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };

        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
        ...

        WindowManager mWM;
        TN() {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            final WindowManager.LayoutParams params = mParams;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            ...
        }

        /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }
       /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }
        ....
 }

可以看到TN是一個Binder物件,用來跨程序呼叫的,裡面封裝了show,hide方法,可以說,Toast的show,hide實際上就是呼叫了TN裡面的方法。為什麼這麼說的,我們看下Toast的show方法。此外,注意WindowManager.LayoutParams.type,這裡的Type是WindowManager.LayoutParams.TYPE_TOAST,表示系統視窗。

Toast.show()

 public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

Toast的show方法中,會先判斷mNextView是否為空,這個mNextView是在Toast.makeText的時候建立賦值出來的。接著通過getService拿到NotificationManagerService的訪問介面,接著把TN,包資訊,以及設定時常傳送到NotificationManagerService.enqueueToast中,我們看下這個方法

 @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
           ...
            synchronized (mToastQueue) {
                ...
                try {
                    ToastRecord record;
                    //從當前佇列檢查是否已經新增過了
                    int index = indexOfToastLocked(pkg, callback);
                    if (index >= 0) {
                        //新增過,則直接獲取
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        ...
                        //否則重新建立個新增到佇列
                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveLocked(callingPid);
                    }
                    ...
                    if (index == 0) {
                        //開始顯示
                        showNextToastLocked();
                    }
                }
               ...
            }

可以看到,在enqueueToast中,首先會呼叫indexOfToastLocked來判斷當前的TN也就是callback是否在佇列中,如果有,則在mToastQueue中直接獲取,更新時間。否則,則重新建立一個帶有TN,時間,包資訊的ToastRecord,再新增到佇列。最後呼叫了showNextToastLocked顯示

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            ...
            try {
                record.callback.show();
                scheduleTimeoutLocked(record);
                return;
            } 
            ...
    }

在showNextToastLocked中,先獲取當前佇列最前的ToastRecord,再呼叫recoed.callback.show(),這裡的callback,就是前面我們傳入的TN物件,也就是說呼叫到我們Toast中TN的show方法。

  @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }
  final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

最終執行方法則是handleShow

 public void handleShow() {
            ...
            if (mView != mNextView) {
                //移除之前的View
                handleHide();
                mView = mNextView;
                ...
                mWM =(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ...
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

在handleHide中,會先移除之前的View,然後把mNextView的值賦值給mView,前面也說到了,這個mNextView就是我們要顯示的內容。接著獲取WindowManager,呼叫addView請求WMS新增到視窗上,而因為是系統視窗,所以token為Null也是可以顯示。

執行完show方法後,Toast已經顯示出來了,後面還呼叫了scheduleTimeoutLocked方法,沒錯,這個就是來限制顯示時間的。

    private void scheduleTimeoutLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }

可以看到它把ToastRecode封裝到Message,而你設定你時間則設定為Handler的delay時間,到達指定時間,則傳送帶Handler去,我們看下mHandler裡面what為MESSAGE_TIMEOUT的執行情況。

        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MESSAGE_TIMEOUT:
                    handleTimeout((ToastRecord)msg.obj);
                    break;
                ...
            }
        }

可以看到到達指定時間後呼叫了handleTimeout,接下來呼叫的方法肯定是來取消Toast顯示的。

    private void handleTimeout(ToastRecord record)
    {
        if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

繼續看

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } 
        ...
        mToastQueue.remove(index);
        keepProcessAliveLocked(record.pid);
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            showNextToastLocked();
        }
    }

可以看到,到達指定時間後,最終呼叫了TN的hide方法,然後移除佇列,如果佇列中還有其他的,則繼續顯示其他的。

同理,看下hide方法

 @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

  final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };


  public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }

                mView = null;
            }
        }

可以看到hide方法最終呼叫了windowManager.removeView來取消顯示。這也從另一方面看到看WindowManager的重要性。

小結

  • WindowManager.LayoutParams中有三種類型,分別為

    • **應用程式視窗 : **type值在 FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW 須將token設定成Activity的token
      eg: 前面介紹的Activity視窗,Dialog

    • 子視窗: type值在 FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW SubWindows與頂層視窗相關聯,需將token設定成它所附著宿主視窗的token
      eg: PopupWindow(想要依附在Activity上需要將token設定成Activity的token)

    • **系統視窗: type值在 FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW ** SystemWindows不能用於應用程式,使用時需要有特殊許可權,它是特定的系統功能才能使用。
      eg: Toast,輸入法等。

  • 對於Activity裡面ActivityRecord的token,它間接標識了一個Activity。想要依附在Activity上需要將token設定成Activity的token,接著傳到WMS中判斷返回ViewRootImpl去判斷報錯。

  • View的繫結資訊通過它的靜態內部類AttachInfo在ViewRootImpl中繫結

  • Dialog中,與Activity共用同個WindowManager,但是他們兩者的Window並不相同。可以說一個Window可以對應一個Activity,但一個Activity不一定對應一個Window,它也有可能對應Dialog

五篇視窗機制總結

  • 瞭解掌握了Android中的視窗分類
  • 懂得Window,PhoneWindow,WindowManager,WindowManagerGlobal它們的分類區別作用
  • 掌握View的真正建立繪製
  • 熟悉了ViewRoot和View樹機制
  • View的繫結資訊,token

小感悟

五篇文章寫了一個星期了吧,挺久的了。一開始從一個小例子一步一步摸索,順蔓摸瓜出這麼多知識,對於自己來說掌握的也相對全面了,也逐漸對原始碼越來越感興趣了。以前對於View繪製視窗這些只懂怎麼用,但不知道為什麼?可以說,現在自己在用的時候,不妨多問自己個為什麼,再去摸索摸索,才能更快的成長也說不定哦。

 



作者:Hohohong
連結:https://www.jianshu.com/p/bac61386d9bf
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授