Android应用Activity、Dialog、PopWindow、Toast窗体加入机制及源代码分析
【工匠若水 http://blog.csdn.net/yanbober 轉載煩請注明出處。尊重勞動成果】
1 背景
之所以寫這一篇博客的原因是由于之前有寫過一篇《Android應用setContentView與LayoutInflater載入解析機制源代碼分析》。然后有人在文章以下評論和微博私信中問我關于Android應用Activity、Dialog、PopWindow載入顯示機制是咋回事,所以我就寫一篇文章來分析分析吧(本文以Android5.1.1 (API 22)源代碼為基礎分析),以便大家在應用層開發時不再迷糊。
PS一句:不僅有人微博私信我這個問題,還有人問博客插圖這些是用啥畫的,這里告訴大家。就是我,快來猛戳我
還記得之前《Android應用setContentView與LayoutInflater載入解析機制源代碼分析》這篇文章的最后分析結果嗎?就是例如以下這幅圖:
在那篇文章里我們當時重點是Activity的View載入解析xml機制分析,當時說到了Window的東西,但僅僅是皮毛的分析了Activity相關的一些邏輯。(PS:看到這不清楚上面啥意思的建議先移步到《Android應用setContentView與LayoutInflater載入解析機制源代碼分析》,完事再回頭繼續看這篇文章。)當時給大家承諾過我們要從應用控件一點一點往下慢慢深入分析,所以如今開始深入,可是本篇的深入也僅僅是僅限Window相關的東東。之后文章還會繼續慢慢深入。
【工匠若水 http://blog.csdn.net/yanbober 轉載煩請注明出處。尊重勞動成果】
2 淺析Window與WindowManager相關關系及源代碼
通過上面那幅圖能夠非常直觀的看見,Android屏幕顯示的就是Window和各種View,Activity在當中的作用主要是管理生命周期、建立窗體等。也就是說Window相關的東西對于Android屏幕來說是至關重要的(盡管前面分析Activity的setContentView等原理時說過一點Window。但那僅僅是皮毛。),所以有必要在分析Android應用Activity、Dialog、PopWindow載入顯示機制前再看看Window相關的一些東西。
2-1 Window與WindowManager基礎關系
在分析Window與WindowManager之前我們先看一張圖:
接下來看一點代碼,例如以下:
/** Interface to let you add and remove child views to an Activity. To get an instance* of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.*/ public interface ViewManager {public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view); }能夠看見,ViewManager接口定義了一組規則。也就是add、update、remove的操作View接口。也就是說ViewManager是用來加入和移除activity中View的接口。
繼續往下看:
public interface WindowManager extends ViewManager {......public Display getDefaultDisplay();public void removeViewImmediate(View view);......public static class LayoutParams extends ViewGroup.LayoutParamsimplements Parcelable {......} }看見沒有。WindowManager繼承自ViewManager,然后自己還是一個接口,同一時候又定義了一個靜態內部類LayoutParams(這個類比較重要。后面會分析。提前透漏下。假設你在APP做過相似360助手屏幕的那個懸浮窗或者做過那種相似IOS的小白圓點,點擊展開菜單功能,你或多或少就能猜到這個類的重要性。)。
WindowManager用來在應用與Window之間的接口、窗體順序、消息等的管理。繼續看下ViewManager的還有一個實現子類ViewGroup。例如以下:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {//protected ViewParent mParent;//這個成員是View定義的,ViewGroup繼承自View,所以也能夠擁有。//這個變量就是前面我們一系列文章分析View向上傳遞的父節點,相似于一個鏈表Node的next一樣//終于指向了ViewRoot......public void addView(View child, LayoutParams params) {addView(child, -1, params);}......public void addView(View child, int index, LayoutParams params) {......// addViewInner() will call child.requestLayout() when setting the new LayoutParams// therefore, we call requestLayout() on ourselves before, so that the child's request// will be blocked at our levelrequestLayout();invalidate(true);addViewInner(child, index, params, false);}...... }這下理解上面那幅圖了吧,所以說View通過ViewGroup的addView方法加入到ViewGroup中。而ViewGroup層層嵌套到最頂級都會顯示在在一個窗體Window中(正如上面背景介紹中《Android應用setContentView與LayoutInflater載入解析機制源代碼分析》的示意圖一樣)。當中每一個View都有一個ViewParent類型的父節點mParent,最頂上的節點也是一個viewGroup,也即前面文章分析的Window的內部類DecorView(從《Android應用setContentView與LayoutInflater載入解析機制源代碼分析》的總結部分或者《Android應用層View繪制流程與源代碼分析》的5-1小節都能夠驗證這個結論)對象。同一時候通過上面背景中那幅圖能夠看出來。對于一個Activity僅僅有一個DecorView(ViewRoot)。也僅僅有一個Window。
2-2 Activity窗體加入流程拓展
前面文章說過。ActivityThread類的performLaunchActivity方法中調運了activity.attach(…)方法進行初始化。例如以下是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) {......//創建Window類型的mWindow對象。實際為PhoneWindow類實現了抽象Window類mWindow = PolicyManager.makeNewWindow(this);......//通過抽象Window類的setWindowManager方法給Window類的成員變量WindowManager賦值實例化mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);......//把抽象Window類相關的WindowManager對象拿出來關聯到Activity的WindowManager類型成員變量mWindowManagermWindowManager = mWindow.getWindowManager();......}看見沒有。Activity類中的attach方法又創建了Window類型的新成員變量mWindow(PhoneWindow實現類)與Activity相關聯,接著在Activity類的attach方法最后又通過mWindow.setWindowManager(…)方法創建了與Window相關聯的WindowManager對象,最后又通過mWindow.getWindowManager()將Window的WindowManager成員變量賦值給Activity的WindowManager成員變量mWindowManager。
接下來我們看下上面代碼中的mWindow.setWindowManager(…)方法源代碼(PhoneWindow沒有重寫抽象Window的setWindowManager方法,所以直接看Window類的該方法源代碼),例如以下:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,boolean hardwareAccelerated) {......if (wm == null) {wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);}//實例化Window類的WindowManager類型成員mWindowManagermWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);}能夠看見,Window的setWindowManager方法中通過WindowManagerImpl實例的createLocalWindowManager方法獲取了WindowManager實例。例如以下:
public final class WindowManagerImpl implements WindowManager {......private WindowManagerImpl(Display display, Window parentWindow) {mDisplay = display;mParentWindow = parentWindow;}......public WindowManagerImpl createLocalWindowManager(Window parentWindow) {return new WindowManagerImpl(mDisplay, parentWindow);}...... }看見沒有?這樣就把Activity的Window與WindowManager關聯起來了。Activity類的Window類型成員變量mWindow及WindowManager類型成員變量mWindowManager就是這么來的。
回過頭繼續看上面剛剛貼的Activity的attach方法代碼,看見mWindow.setWindowManager方法傳遞的第一個參數沒?有人會想(WindowManager)context.getSystemService(Context.WINDOW_SERVICE)這行代碼是什么意思。如今告訴你。
《Android應用Context具體解釋及源代碼解析》一文中第三部分以前說過ActivityThread中創建了Acitivty(運行attach等方法)等東東,在創建這個Activity之前得到了Context的實例。
記不記得當時說Context的實現類就是ContextImpl嗎?以下我們看下ContextImpl類的靜態方法塊,例如以下:
class ContextImpl extends Context {......//靜態代碼塊。類載入時運行一次static {......//這里有一堆相似的XXX_SERVICE的注冊......registerService(WINDOW_SERVICE, new ServiceFetcher() {Display mDefaultDisplay;public Object getService(ContextImpl ctx) {//搞一個Display實例Display display = ctx.mDisplay;if (display == null) {if (mDefaultDisplay == null) {DisplayManager dm = (DisplayManager)ctx.getOuterContext().getSystemService(Context.DISPLAY_SERVICE);mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);}display = mDefaultDisplay;}//返回一個WindowManagerImpl實例return new WindowManagerImpl(display);}});......}//這就是你在外面調運Context的getSystemService獲取到的WindowManagerImpl實例@Overridepublic Object getSystemService(String name) {ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);return fetcher == null ? null : fetcher.getService(this);}//上面static代碼塊創建WindowManagerImpl實例用到的方法private static void registerService(String serviceName, ServiceFetcher fetcher) {if (!(fetcher instanceof StaticServiceFetcher)) {fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;}SYSTEM_SERVICE_MAP.put(serviceName, fetcher);} }看見沒有,我們都知道Java的靜態代碼塊是類載入是運行一次的。也就相當于一個全局的,這樣就相當于每一個Application僅僅有一個WindowManagerImpl(display)實例。
還記不記得《Android應用setContentView與LayoutInflater載入解析機制源代碼分析》一文2-6小節中說的,setContentView的實質顯示是觸發了Activity的resume狀態。也就是觸發了makeVisible方法。那我們再來看下這種方法。例如以下:
void makeVisible() {if (!mWindowAdded) {//也就是獲取Activity的mWindowManager//這個mWindowManager是在Activity的attach中通過mWindow.getWindowManager()獲得ViewManager wm = getWindowManager();//調運的實質就是ViewManager接口的addView方法,傳入的是mDecorViewwm.addView(mDecor, getWindow().getAttributes());mWindowAdded = true;}mDecor.setVisibility(View.VISIBLE);}特別注意,看見makeVisible方法的wm變量沒。這個變量就是Window類中通過調運WindowManagerImpl的createLocalWindowManager創建的實例,也就是說每一個Activity都會新創建這么一個WindowManager實例來顯示Activity的界面的,有點和上面分析的ContextImpl中static塊創建的WindowManager不太一樣的地方就在于Context的WindowManager對每一個APP來說是一個全局單例的,而Activity的WindowManager是每一個Activity都會新創建一個的(事實上你從上面分析的兩個實例化WindowManagerImpl的構造函數參數傳遞就能夠看出來,Activity中Window的WindowManager成員在構造實例化時傳入給WindowManagerImpl中mParentWindow成員的是當前Window對象,而ContextImpl的static塊中單例實例化WindowManagerImpl時傳入給WindowManagerImpl中mParentWindow成員的是null值)。
繼續看makeVisible中調運的WindowManagerImpl的addView方法例如以下:
public final class WindowManagerImpl implements WindowManager {//繼承自Object的單例類private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();......public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);//mParentWindow是上面分析的在Activity中獲取WindowManagerImpl實例化時傳入的當前Window//view是Activity中最頂層的mDecormGlobal.addView(view, params, mDisplay, mParentWindow);}...... }這里當前傳入的view是mDecor。LayoutParams呢?能夠看見是getWindow().getAttributes()。那我們進去看看Window類的這個屬性。例如以下:
// The current window attributes.private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();原來是WindowManager的靜態內部類LayoutParams的默認構造函數:
public LayoutParams() {super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);type = TYPE_APPLICATION;format = PixelFormat.OPAQUE; }看見沒有。Activity窗體的WindowManager.LayoutParams類型是TYPE_APPLICATION的。
繼續回到WindowManagerImpl的addView方法。分析能夠看見WindowManagerImpl中有一個單例模式的WindowManagerGlobal成員mGlobal。addView終于調運了WindowManagerGlobal的addView,源代碼例如以下:
public final class WindowManagerGlobal {......private final ArrayList<View> mViews = new ArrayList<View>();private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();private final ArrayList<WindowManager.LayoutParams> mParams =new ArrayList<WindowManager.LayoutParams>();private final ArraySet<View> mDyingViews = new ArraySet<View>();......public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {......//獲取Activity的Window的getWindow().getAttributes()的LayoutParams final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;//假設是Activity中調運的,parentWindow=Window。假設不是Activity的。譬如是Context的靜態代碼塊的實例化則parentWindow為nullif (parentWindow != null) {//根據當前Activity的Window調節sub Window的LayoutParamsparentWindow.adjustLayoutParamsForSubWindow(wparams);} else {......}ViewRootImpl root;......synchronized (mLock) {......//為當前Window創建ViewRootroot = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);//把當前Window相關的東西存入各自的List中,在remove中會刪掉mViews.add(view);mRoots.add(root);mParams.add(wparams);}// do this last because it fires off messages to start doing thingstry {//把View和ViewRoot關聯起來。非常重要!!! root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { ...... } } ...... }能夠看見,在addView方法中會利用LayoutParams獲得Window的屬性,然后為每一個Window創建ViewRootImpl。最后通過ViewRootImpl的setView方法通過mSession向WindowManagerService發送加入窗體請求把窗體加入到WindowManager中。并且由WindowManager來管理窗體的view、事件、消息收集處理等(ViewRootImpl的這一加入過程后面會寫文章分析,這里先記住這個概念就可以)。
至此我們對上面背景中那幅圖,也就是《Android應用setContentView與LayoutInflater載入解析機制源代碼分析》這篇文章總結部分的那幅圖又進行了更深入的一點分析。其目的也就是為了以下分析Android應用Dialog、PopWindow、Toast載入顯示機制做鋪墊準備。
2-3 繼續順藤摸瓜WindowManager.LayoutParams類的源代碼
上面2-1分析Window與WindowManager基礎關系時提到了WindowManager有一個靜態內部類LayoutParams。它繼承于ViewGroup.LayoutParams。用于向WindowManager描寫敘述Window的管理策略。如今我們來看下這個類(PS:在AD上也能夠看見,自備梯子點我看AD的),例如以下:
public static class LayoutParams extends ViewGroup.LayoutParamsimplements 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:信息窗體。用于顯示Toastpublic 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之后的其它windowpublic 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_SCpublic 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:當鎖屏的時候,顯示該windowpublic 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 touchpublic 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的maskpublic 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的maskpublic 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)到了你的windowpublic 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.PixelFormatpublic 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) {......}......}看見沒有。從上面類能夠看出,Android窗體類型主要分成了三大類:
一般應用程序的窗體,比方我們應用程序的Activity的窗體。
同一時候還能夠看見,WindowManager.LayoutParams里面窗體的type類型值定義是一個遞增保留的連續增大數值,從凝視能夠看出來事實上就是窗體的Z-ORDER序列(值越大顯示的位置越在上面,你須要將屏幕想成三維坐標模式)。創建不同類型的窗體須要設置不同的type值,譬如上面拓展Activity窗體載入時分析的makeVisible方法中的Window默認屬性的type=TYPE_APPLICATION。
既然說這個類非常重要。那總得感性的體驗一下重要性吧,所以我們先來看幾個實例。
2-4 通過上面WindowManager.LayoutParams分析引出的應用層開發經常使用經典實例
有了上面分析相信你一定覺得WindowManager.LayoutParams還是蠻熟悉的,不信我們來看下。
Part1:開發APP時設置Activity全屏常亮的一種辦法(設置Activity也就是Activity的Window):
public class MainActivity extends ActionBarActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);//設置Activity的Window為全屏。當然也能夠在xml中設置Window window = getWindow();WindowManager.LayoutParams windowAttributes = window.getAttributes();windowAttributes.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN | windowAttributes.flags;window.setAttributes(windowAttributes);//設置Activity的Window為保持屏幕亮window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);setContentView(R.layout.activity_main);} }這是運行結果:
Part2:App開發中彈出軟鍵盤時以下的輸入框被軟件盤擋住問題的解決的方法:
在Activity中的onCreate中setContentView之前寫例如以下代碼:
//你也能夠在xml文件里設置。一樣的效果 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);Part3:創建懸浮窗體(仿IPhone的小圓點或者魅族的小白點或者360手機衛士的小浮標)。退出當前Activity依然可見的一種實現方法:
省略了Activity的start與stop Service的按鈕代碼,直接給出了核心代碼例如以下:
/*** Author : yanbo* Time : 14:47* Description : 手機屏幕懸浮窗,仿IPhone小圓點* (未全然實現。僅僅提供思路,如需請自行實現)* Notice : <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />*/ public class WindowService extends Service {private WindowManager mWindowManager;private ImageView mImageView;@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();//創建懸浮窗createFloatWindow();}private void createFloatWindow() {//這里的參數設置上面剛剛講過。不再說明WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();mWindowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);//設置window的typelayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;//設置效果為背景透明layoutParams.format = PixelFormat.RGBA_8888;//設置浮動窗體不可聚焦layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;layoutParams.gravity = Gravity.BOTTOM | Gravity.RIGHT;layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;layoutParams.x = -50;layoutParams.y = -50;mImageView = new ImageView(this);mImageView.setImageResource(android.R.drawable.ic_menu_add);//加入到WindowmWindowManager.addView(mImageView, layoutParams);//設置監聽mImageView.setOnTouchListener(touchListener);}@Overridepublic void onDestroy() {super.onDestroy();if (mImageView != null) {//講WindowManager時說過。add,remove成對出現,所以須要removemWindowManager.removeView(mImageView);}}private View.OnTouchListener touchListener = new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {//模擬觸摸觸發的事件Intent intent = new Intent(Intent.ACTION_VIEW);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);return false;}}; }例如以下是運行過程模擬,特別留意屏幕右下角的變化:
怎么樣,通過最后這個樣例你是不是就能體會到WindowManager.LayoutParams的Z-ORDER序列類型,值越大顯示的位置越在上面。
2-5 總結Activity的窗體加入機制
有了上面這么多分析和前幾篇的分析,我們對Activity的窗體載入再次深入分析總結例如以下:
能夠看見Context的WindowManager對每一個APP來說是一個全局單例的。而Activity的WindowManager是每一個Activity都會新創建一個的(事實上你從上面分析的兩個實例化WindowManagerImpl的構造函數參數傳遞就能夠看出來,Activity中Window的WindowManager成員在構造實例化時傳入給WindowManagerImpl中mParentWindow成員的是當前Window對象。而ContextImpl的static塊中單例實例化WindowManagerImpl時傳入給WindowManagerImpl中mParentWindow成員的是null值)。所以上面模擬蘋果浮動小圖標使用了Application的WindowManager而不是Activity的,原因就在于這里;使用Activity的WindowManager時當Activity結束時WindowManager就無效了。所以使用Activity的getSysytemService(WINDOW_SERVICE)獲取的是Local的WindowManager。同一時候能夠看出來Activity中的WindowManager.LayoutParams的type為TYPE_APPLICATION。
好了。上面也說了不少了,有了上面這些知識點以后我們就來開始分析Android應用Activity、Dialog、PopWindow窗體顯示機制。
【工匠若水 http://blog.csdn.net/yanbober 轉載煩請注明出處,尊重勞動成果】
3 Android應用Dialog窗體加入顯示機制源代碼
3-1 Dialog窗體源代碼分析
寫過APP都知道,Dialog是一系列XXXDialog的基類,我們能夠new隨意Dialog或者通過Activity提供的onCreateDialog(……)、onPrepareDialog(……)和showDialog(……)等方法來管理我們的Dialog,可是究事實上質都是來源于Dialog基類,所以我們對于各種XXXDialog來說僅僅用分析Dialog的窗體載入就能夠了。
例如以下從Dialog的構造函數開始分析:
public class Dialog implements DialogInterface, Window.Callback,KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {......public Dialog(Context context) {this(context, 0, true);}//構造函數終于都調運了這個默認的構造函數Dialog(Context context, int theme, boolean createContextThemeWrapper) {//默認構造函數的createContextThemeWrapper為trueif (createContextThemeWrapper) {//默認構造函數的theme為0if (theme == 0) {TypedValue outValue = new TypedValue();context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,outValue, true);theme = outValue.resourceId;}mContext = new ContextThemeWrapper(context, theme);} else {mContext = context;}//mContext已經從外部傳入的context對象獲得值(通常是個Activity)。!。非常重要。先記住!!。//獲取WindowManager對象mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);//為Dialog創建新的WindowWindow w = PolicyManager.makeNewWindow(mContext);mWindow = w;//Dialog能夠接受到按鍵事件的原因w.setCallback(this);w.setOnWindowDismissedCallback(this);//關聯WindowManager與新Window。特別注意第二個參數token為null。也就是說Dialog沒有自己的token//一個Window屬于Dialog的話。那么該Window的mAppToken對象是nullw.setWindowManager(mWindowManager, null, null);w.setGravity(Gravity.CENTER);mListenersHandler = new ListenersHandler(this);}...... }能夠看到。Dialog構造函數首先把外部傳入的參數context對象賦值給了當前類的成員(我們的Dialog一般都是在Activity中啟動的。所以這個context通常是個Activity),然后調用context.getSystemService(Context.WINDOW_SERVICE)獲取WindowManager。這個WindowManager是哪來的呢?先依照上面說的context通常是個Activity來看待,能夠發現這句實質就是Activity的getSystemService方法,我們看下源代碼,例如以下:
@Overridepublic Object getSystemService(@ServiceName @NonNull String name) {if (getBaseContext() == null) {throw new IllegalStateException("System services not available to Activities before onCreate()");}//我們Dialog中獲得的WindowManager對象就是這個分支if (WINDOW_SERVICE.equals(name)) {//Activity的WindowManagerreturn mWindowManager;} else if (SEARCH_SERVICE.equals(name)) {ensureSearchManager();return mSearchManager;}return super.getSystemService(name);}看見沒有。Dialog中的WindowManager成員實質和Activity里面是一樣的,也就是共用了一個WindowManager。
回到Dialog的構造函數繼續分析,在得到了WindowManager之后,程序又新建了一個Window對象(類型是PhoneWindow類型,和Activity的Window新建過程相似)。接著通過w.setCallback(this)設置Dialog為當前window的回調接口。這樣Dialog就能夠接收事件處理了;接著把從Activity拿到的WindowManager對象關聯到新創建的Window中。
至此Dialog的創建過程Window處理已經完成,非常easy。所以接下來我們繼續看看Dialog的show與cancel方法。例如以下:
public void show() {......if (!mCreated) {//回調Dialog的onCreate方法dispatchOnCreate(null);}//回調Dialog的onStart方法onStart();//相似于Activity,獲取當前新Window的DecorView對象,所以有一種自己定義Dialog布局的方式就是重寫Dialog的onCreate方法,使用setContentView傳入布局。就像前面文章分析Activity相似mDecor = mWindow.getDecorView();......//獲取新Window的WindowManager.LayoutParams參數。和上面分析的Activity一樣type為TYPE_APPLICATIONWindowManager.LayoutParams l = mWindow.getAttributes();......try {//把一個View加入到Activity共用的windowManager里面去mWindowManager.addView(mDecor, l);......} finally {}}能夠看見Dialog的新Window與Activity的Window的type相同都為TYPE_APPLICATION,上面介紹WindowManager.LayoutParams時TYPE_APPLICATION的凝視明白說過,普通應用程序窗體TYPE_APPLICATION的token必須設置為Activity的token來指定窗體屬于誰。所以能夠看見,既然Dialog和Activity共享同一個WindowManager(也就是上面分析的WindowManagerImpl),而WindowManagerImpl里面有個Window類型的mParentWindow變量。這個變量在Activity的attach中創建WindowManagerImpl時傳入的為當前Activity的Window,而當前Activity的Window里面的mAppToken值又為當前Activity的token。所以Activity與Dialog共享了同一個mAppToken值,僅僅是Dialog和Activity的Window對象不同。
3-2 Dialog窗體載入總結
通過上面分析Dialog的窗體載入原理,我們總結例如以下圖:
從圖中能夠看出。Activity和Dialog共用了一個Token對象,Dialog必須依賴于Activity而顯示(通過別的context搞完之后token都為null,終于會在ViewRootImpl的setView方法中載入時由于token為null拋出異常)。所以Dialog的Context傳入參數通常是一個存在的Activity,假設Dialog彈出來之前Activity已經被銷毀了,則這個Dialog在彈出的時候就會拋出異常。由于token不可用了。在Dialog的構造函數中我們關聯了新Window的callback事件監聽處理,所以當Dialog顯示時Activity無法消費當前的事件。
到此Dialog的窗體載入機制就分析完成了,接下來我們說說應用開發中常見的一個詭異問題。
3-3 從Dialog窗體載入分析引出的應用開發問題
有了上面的分析我們接下來看下平時開發App剛開始學習的人easy犯的幾個錯誤。
實如今一個Activity中顯示一個Dialog,例如以下代碼:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {setContentView(R.layout.activity_main);//重點關注構造函數的參數,創建一個Dialog然后顯示出來Dialog dialog = new ProgressDialog(this);dialog.setTitle("TestDialogContext");dialog.show();} }分析:使用了Activity為context,也即和Activity共用token,符合上面的分析。所以不會報錯,正常運行。
實如今一個Activity中顯示一個Dialog,例如以下代碼:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {setContentView(R.layout.activity_main);//重點關注構造函數的參數,創建一個Dialog然后顯示出來Dialog dialog = new ProgressDialog(getApplicationContext());dialog.setTitle("TestDialogContext");dialog.show();} }分析:傳入的是Application的Context,導致TYPE_APPLICATION類型Dialog的token為null。所以拋出例如以下異常,無法顯示對話框。
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an applicationat android.view.ViewRootImpl.setView(ViewRootImpl.java:566)at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)at android.app.Dialog.show(Dialog.java:298)實如今一個Service中顯示一個Dialog。例如以下代碼:
public class WindowService extends Service {@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();//重點關注構造函數的參數Dialog dialog = new ProgressDialog(this);dialog.setTitle("TestDialogContext");dialog.show();} }分析:傳入的Context是一個Service,相似上面傳入ApplicationContext一樣的后果,一樣的原因。拋出例如以下異常:
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an applicationat android.view.ViewRootImpl.setView(ViewRootImpl.java:566)at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)at android.app.Dialog.show(Dialog.java:298)至此通過我們平時使用最多的Dialog也驗證了Dialog成功顯示的必要條件,同一時候也讓大家避免了再次使用Dialog不當出現異常的情況,或者出現相似異常后知道真實的背后原因是什么的問題。
能夠看見。Dialog的實質無非也是使用WindowManager的addView、updateViewLayout、removeView進行一些操作展示。
【工匠若水 http://blog.csdn.net/yanbober 轉載煩請注明出處,尊重勞動成果】
4 Android應用PopWindow窗體加入顯示機制源代碼
PopWindow實質就是彈出式菜單。它與Dialag不同的地方是不會使依賴的Activity組件失去焦點(PopupWindow彈出后能夠繼續與依賴的Activity進行交互)。Dialog卻不能這樣。同一時候PopupWindow與Dialog還有一個不同點是PopupWindow是一個堵塞的對話框。假設你直接在Activity的onCreate等方法中顯示它則會報錯,所以PopupWindow必須在某個事件中顯示地或者是開啟一個新線程去調用。
說這么多還是直接看代碼吧。
4-1 PopWindow窗體源代碼分析
根據PopWindow的使用,我們選擇最經常使用的方式來分析。例如以下先看當中經常使用的一種構造函數:
public class PopupWindow {......//我們僅僅分析最經常使用的一種構造函數public PopupWindow(View contentView, int width, int height, boolean focusable) {if (contentView != null) {//獲取mContext。contentView實質是View,View的mContext都是構造函數傳入的,View又層級傳遞,所以終于這個mContext實質是Activity。!!非常重要
mContext = contentView.getContext(); //獲取Activity的getSystemService的WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } //進行一些Window類的成員變量初始化賦值操作 setContentView(contentView); setWidth(width); setHeight(height); setFocusable(focusable); } ...... }能夠看見,構造函數僅僅是初始化了一些變量,看完構造函數繼續看下PopWindow的展示函數。例如以下:
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {......//anchor是Activity中PopWindow準備依附的View,這個View的token實質也是Activity的Window中的token,也即Activity的token//第一步 初始化WindowManager.LayoutParamsWindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());//第二步preparePopup(p);......//第三步invokePopup(p);}能夠看見,當我們想將PopWindow展示在anchor的下方向(Z軸是在anchor的上面)旁邊時經理了上面三步。我們一步一步來分析。先看第一步。源代碼例如以下:
private WindowManager.LayoutParams createPopupLayout(IBinder token) {//實例化一個默認的WindowManager.LayoutParams。當中type=TYPE_APPLICATIONWindowManager.LayoutParams p = new WindowManager.LayoutParams();//設置Gravityp.gravity = Gravity.START | Gravity.TOP;//設置寬高p.width = mLastWidth = mWidth;p.height = mLastHeight = mHeight;//根據背景設置formatif (mBackground != null) {p.format = mBackground.getOpacity();} else {p.format = PixelFormat.TRANSLUCENT;}//設置flagsp.flags = computeFlags(p.flags);//改動type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,mWindowLayoutType有初始值,type類型為子窗體p.type = mWindowLayoutType;//設置token為Activity的tokenp.token = token;......return p;}接著回到showAsDropDown方法看看第二步,例如以下源代碼:
private void preparePopup(WindowManager.LayoutParams p) {......//有無設置PopWindow的background差別if (mBackground != null) {......//假設有背景則創建一個PopupViewContainer對象的ViewGroupPopupViewContainer popupViewContainer = new PopupViewContainer(mContext);PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height);//把背景設置給PopupViewContainer的ViewGrouppopupViewContainer.setBackground(mBackground);//把我們構造函數傳入的View加入到這個ViewGrouppopupViewContainer.addView(mContentView, listParams);//返回這個ViewGroupmPopupView = popupViewContainer;} else {//假設沒有通過PopWindow的setBackgroundDrawable設置背景則直接賦值當前傳入的View為PopWindow的ViewmPopupView = mContentView;}......}能夠看見preparePopup方法的作用就是推斷設置View。假設有背景則會在傳入的contentView外面包一層PopupViewContainer(實質是一個重寫了事件處理的FrameLayout)之后作為mPopupView。假設沒有背景則直接用contentView作為mPopupView。
我們再來看下這里的PopupViewContainer類。例如以下源代碼:
private class PopupViewContainer extends FrameLayout {......@Overrideprotected int[] onCreateDrawableState(int extraSpace) {......}@Overridepublic boolean dispatchKeyEvent(KeyEvent event) {......}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {return true;}return super.dispatchTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {......if(xxx) {dismiss();}......}@Overridepublic void sendAccessibilityEvent(int eventType) {......}}能夠看見,這個PopupViewContainer是一個PopWindow的內部私有類,它繼承了FrameLayout。在當中重寫了Key和Touch事件的分發處理邏輯。同一時候查閱PopupView能夠發現,PopupView類自身沒有重寫Key和Touch事件的處理,所以假設沒有將傳入的View對象放入封裝的ViewGroup中。則點擊Back鍵或者PopWindow以外的區域PopWindow是不會消失的(事實上PopWindow中沒有向Activity及Dialog一樣new新的Window,所以不會有新的callback設置。也就沒法處理事件消費了)。
接著繼續回到showAsDropDown方法看看第三步。例如以下源代碼:
private void invokePopup(WindowManager.LayoutParams p) {if (mContext != null) {p.packageName = mContext.getPackageName();}mPopupView.setFitsSystemWindows(mLayoutInsetDecor);setLayoutDirectionFromAnchor();mWindowManager.addView(mPopupView, p);}能夠看見。這里使用了Activity的WindowManager將我們的PopWindow進行了顯示。
到此能夠發現。PopWindow的實質無非也是使用WindowManager的addView、updateViewLayout、removeView進行一些操作展示。與Dialog不同的地方是沒有新new Window而已(也就沒法設置callback。無法消費事件,也就是前面說的PopupWindow彈出后能夠繼續與依賴的Activity進行交互的原因)。
到此PopWindw的窗體載入顯示機制就分析完成了,接下來進行總結與應用開發技巧提示。
4-2 PopWindow窗體源代碼分析總結及應用開發技巧提示
通過上面分析能夠發現總結例如以下圖:
能夠看見,PopWindow全然使用了Activity的Window與WindowManager,相對來說比較簡單easy記理解。
再來看一個開發技巧:
假設設置了PopupWindow的background。則點擊Back鍵或者點擊PopupWindow以外的區域時PopupWindow就會dismiss;假設不設置PopupWindow的background。則點擊Back鍵或者點擊PopupWindow以外的區域PopupWindow不會消失。
【工匠若水 http://blog.csdn.net/yanbober 轉載煩請注明出處,尊重勞動成果】
5 Android應用Toast窗體加入顯示機制源代碼
5-1 基礎知識準備
在開始分析這幾個窗體之前須要腦補一點東東,我們從應用層開發來直觀腦補,這樣以下分析源代碼時就不蛋疼了。
例如以下是一個我們寫的兩個應用實現Service跨進程調用服務ADIL的樣例,client調運遠程Service的start與stop方法控制遠程Service的操作。
Android系統中的應用程序都運行在各自的進程中,進程之間是無法直接交換數據的。可是Android為開發人員提供了AIDL跨進程調用Service的功能。事實上AIDL就相當于兩方約定的一個規則而已。
先看下在Android Studio中AIDL開發的project文件夾結構,例如以下:
由于AIDL文件里不能出現訪問修飾符(如public),同一時候AIDL文件在兩個項目中要全然一致并且僅僅支持基本類型。所以我們定義的AIDL文件例如以下:
ITestService.aidl
package io.github.yanbober.myapplication;interface ITestService {void start(int id);void stop(int id); }再來看下根據aidl文件自己主動生成的ITestService.java文件吧。例如以下:
/** This file is auto-generated. DO NOT MODIFY.*/ package io.github.yanbober.myapplication; public interface ITestService extends android.os.IInterface {//Stub類是ITestService接口的內部靜態抽象類,該類繼承了Binder類public static abstract class Stub extends android.os.Binder implements io.github.yanbober.myapplication.ITestService{......//這是抽象靜態Stub類中的asInterface方法,該方法負責將service返回至client的對象轉換為ITestService.Stub//把遠程Service的Binder對象傳遞進去,得到的是遠程服務的本地代理public static io.github.yanbober.myapplication.ITestService asInterface(android.os.IBinder obj){......}......//遠程服務的本地代理,也會繼承自ITestServiceprivate static class Proxy implements io.github.yanbober.myapplication.ITestService{......@Overridepublic void start(int id) throws android.os.RemoteException{......}@Overridepublic void stop(int id) throws android.os.RemoteException{......}}......}//兩個方法是aidl文件里定義的方法public void start(int id) throws android.os.RemoteException;public void stop(int id) throws android.os.RemoteException; }這就是自己主動生成的java文件,接下來我們看看服務端的Service源代碼,例如以下:
//記得在AndroidManifet.xml中注冊Service的<action android:name="io.github.yanbober.myapplication.aidl" />public class TestService extends Service {private TestBinder mTestBinder;//該類繼承ITestService.Stub類而不是Binder類,由于ITestService.Stub是Binder的子類//進程內的Service定義TestBinder內部類是繼承Binder類public class TestBinder extends ITestService.Stub {@Overridepublic void start(int id) throws RemoteException {Log.i(null, "Server Service is start!");}@Overridepublic void stop(int id) throws RemoteException {Log.i(null, "Server Service is stop!");}}@Overridepublic IBinder onBind(Intent intent) {//返回Binderreturn mTestBinder;}@Overridepublic void onCreate() {super.onCreate();//實例化BindermTestBinder = new TestBinder();} }如今服務端App的代碼已經OK,我們來看下client的代碼。client首先也要像上面的project結構一樣,把AIDL文件放好。接著在client使用遠程服務端的Service代碼例如以下:
public class MainActivity extends Activity {private static final String REMOT_SERVICE_ACTION = "io.github.yanbober.myapplication.aidl";private Button mStart, mStop;private ITestService mBinder;private ServiceConnection connection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {//獲得還有一個進程中的Service傳遞過來的IBinder對象//用IMyService.Stub.asInterface方法轉換該對象mBinder = ITestService.Stub.asInterface(service);}@Overridepublic void onServiceDisconnected(ComponentName name) {}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mStart = (Button) this.findViewById(R.id.start);mStop = (Button) this.findViewById(R.id.stop);mStart.setOnClickListener(clickListener);mStop.setOnClickListener(clickListener);//綁定遠程跨進程ServicebindService(new Intent(REMOT_SERVICE_ACTION), connection, BIND_AUTO_CREATE);}@Overrideprotected void onDestroy() {super.onDestroy();//取消綁定遠程跨進程ServiceunbindService(connection);}private View.OnClickListener clickListener = new View.OnClickListener() {@Overridepublic void onClick(View v) {調用遠程Service中的start與stop方法switch (v.getId()) {case R.id.start:try {mBinder.start(0x110);} catch (RemoteException e) {e.printStackTrace();}break;case R.id.stop:try {mBinder.stop(0x120);} catch (RemoteException e) {e.printStackTrace();}break;}}}; }到此你相應用層通過AIDL使用遠程Service的形式已經非常熟悉了,至于實質的通信使用Binder的機制我們后面會寫文章一步一步往下分析。到此的準備知識已經足夠用來理解以下我們的源代碼分析了。
5-2 Toast窗體源代碼分析
我們經常使用的Toast窗體事實上和前面分析的Activity、Dialog、PopWindow都是不同的。由于它和輸入法、墻紙相似。都是系統窗體。
我們還是依照最經常使用的方式來分析源代碼吧。
我們先看下Toast的靜態makeText方法吧,例如以下:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {//new一個Toast對象Toast result = new Toast(context);//獲取前面有篇文章分析的LayoutInflaterLayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);//載入解析Toast的布局。實質transient_notification.xml是一個LinearLayout中套了一個@android:id/message的TextView而已View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);//取出布局中的TextViewTextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);//把我們的文字設置到TextView上tv.setText(text);//設置一些屬性result.mNextView = v;result.mDuration = duration;//返回新建的Toastreturn result;}能夠看見。這種方法構造了一個Toast,然后把要顯示的文本放到這個View的TextView中,然后初始化相關屬性后返回這個新的Toast對象。
當我們有了這個Toast對象之后。能夠通過show方法來顯示出來。例如以下看下show方法源代碼:
public void show() {......//通過AIDL(Binder)通信拿到NotificationManagerService的服務訪問接口。當前Toast類相當于上面樣例的client!!。相當重要!!!
INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { //把TN對象和一些參數傳遞到遠程NotificationManagerService中去 service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }我們看看show方法中調運的getService方法,例如以下:
//遠程NotificationManagerService的服務訪問接口private static INotificationManager sService;static private INotificationManager getService() {//單例模式if (sService != null) {return sService;}//通過AIDL(Binder)通信拿到NotificationManagerService的服務訪問接口sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));return sService;}通過上面我們的基礎腦補實例你也能看懂這個getService方法了吧。那接著我們來看mTN吧,好像mTN在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);}能夠看見mTN確實是在構造函數中實例化的,那我們就來看看這個TN類,例如以下:
//相似于上面樣例的服務端實例化的Service內部類Binderprivate static class TN extends ITransientNotification.Stub {......//實現了AIDL的show與hide方法@Overridepublic void show() {if (localLOGV) Log.v(TAG, "SHOW: " + this);mHandler.post(mShow);}@Overridepublic void hide() {if (localLOGV) Log.v(TAG, "HIDE: " + this);mHandler.post(mHide);}......}看見沒有,TN是Toast內部的一個私有靜態類,繼承自ITransientNotification.Stub。你這時指定好奇ITransientNotification.Stub是個啥玩意,對吧?事實上你在上面的腦補實例中見過它的,他出如今服務端實現的Service中,就是一個Binder對象。也就是對一個aidl文件的實現而已,我們看下這個ITransientNotification.aidl文件。例如以下:
package android.app;/** @hide */ oneway interface ITransientNotification {void show();void hide(); }看見沒有,和我們上面的樣例非常相似吧。
再回到上面分析的show()方法中能夠看到。我們的Toast是傳給遠程的NotificationManagerService管理的,為了NotificationManagerService回到我們的應用程序(回調)。我們須要告訴NotificationManagerService我們當前程序的Binder引用是什么(也就是TN)。是不是覺得和上面樣例有些不同,這里感覺Toast又充當client。又充當服務端的樣子,實質就是一個回調過程而已。
繼續來看Toast中的show方法的service.enqueueToast(pkg, tn, mDuration);語句。service實質是遠程的NotificationManagerService。所以enqueueToast方法就是NotificationManagerService類的。例如以下:
private final IBinder mService = new INotificationManager.Stub() {// Toasts// ============================================================================@Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration){......synchronized (mToastQueue) {int callingPid = Binder.getCallingPid();long callingId = Binder.clearCallingIdentity();try {ToastRecord record;//查看該Toast是否已經在隊列當中int index = indexOfToastLocked(pkg, callback);// If it's already in the queue, we update it in place, we don't// move it to the end of the queue.//凝視說了,已經存在則直接取出updateif (index >= 0) {record = mToastQueue.get(index);record.update(duration);} else {// Limit the number of toasts that any given package except the android// package can enqueue. Prevents DOS attacks and deals with leaks.......//將Toast封裝成ToastRecord對象,放入mToastQueue中record = new ToastRecord(callingPid, pkg, callback, duration);//把他加入到ToastQueue隊列中mToastQueue.add(record);index = mToastQueue.size() - 1;//將當前Toast所在的進程設置為前臺進程keepProcessAliveLocked(callingPid);}//假設index為0,說明當前入隊的Toast在隊頭,須要調用showNextToastLocked方法直接顯示if (index == 0) {showNextToastLocked();}} finally {Binder.restoreCallingIdentity(callingId);}}}}繼續看下該方法中調運的showNextToastLocked方法,例如以下:
void showNextToastLocked() {//取出ToastQueue中隊列最前面的ToastRecordToastRecord record = mToastQueue.get(0);while (record != null) {try {//Toast類中實現的ITransientNotification.Stub的Binder接口TN,調運了那個類的show方法record.callback.show();scheduleTimeoutLocked(record);return;} catch (RemoteException e) {......}}}繼續先看下該方法中調運的scheduleTimeoutLocked方法。例如以下:
private void scheduleTimeoutLocked(ToastRecord r){//移除上一條消息mHandler.removeCallbacksAndMessages(r);//根據Toast傳入的duration參數LENGTH_LONG=1來推斷決定多久發送消息Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);long delay = r.duration == Toast.LENGTH_LONG ?LONG_DELAY : SHORT_DELAY; //根據設置的MESSAGE_TIMEOUT后發送消息 mHandler.sendMessageDelayed(m, delay); }
能夠看見這里先回調了Toast的TN的show,以下timeout可能就是hide了。接著還在該類的mHandler處理了這條消息。然后調運了例如以下處理方法:
private void handleTimeout(ToastRecord record){......synchronized (mToastQueue) {int index = indexOfToastLocked(record.pkg, record.callback);if (index >= 0) {cancelToastLocked(index);}}}我們繼續看cancelToastLocked方法,例如以下:
void cancelToastLocked(int index) {ToastRecord record = mToastQueue.get(index);try {//回調Toast的TN中實現的hide方法record.callback.hide();} catch (RemoteException e) {......}//從隊列移除當前顯示的ToastmToastQueue.remove(index);keepProcessAliveLocked(record.pid);if (mToastQueue.size() > 0) {//假設當前的Toast顯示完成隊列里還有其它的Toast則顯示其它的ToastshowNextToastLocked();}}到此能夠發現,Toast的遠程管理NotificationManagerService類的處理實質是通過Handler發送延時消息顯示取消Toast的,并且在遠程NotificationManagerService類中又遠程回調了Toast的TN類實現的show與hide方法。
如今我們就回到Toast的TN類再看看這個show與hide方法,例如以下:
```javaprivate static class TN extends ITransientNotification.Stub {......//僅僅是實例化了一個Handler。非常重要!!!!。!
!!
final Handler mHandler = new Handler(); ...... 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; } }; ...... //實現了AIDL的show與hide方法 @Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); } @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); } ...... }能夠看見。這里實現aidl接口的方法實質是通過handler的post來運行的一個方法,而這個Handler僅僅僅僅是new了一下。也就是說。假設我們寫APP時使用Toast在子線程中則須要自行準備Looper對象。僅僅有主線程Activity創建時幫忙準備了Looper(關于Handler與Looper假設整不明白請閱讀《Android異步消息處理機制具體解釋及源代碼分析》)。
那我們重點關注一下handleShow與handleHide方法。例如以下:
public void handleShow() {if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView+ " mNextView=" + mNextView);if (mView != mNextView) {// remove the old view if necessary//假設有必要就通過WindowManager的remove刪掉舊的handleHide();mView = mNextView;Context context = mView.getContext().getApplicationContext();String packageName = mView.getContext().getOpPackageName();if (context == null) {context = mView.getContext();}//通過得到的context(通常是ContextImpl的context)獲取WindowManager對象(上一篇文章分析的單例的WindowManager)mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);......//在把Toast的View加入之前發現Toast的View已經被加入過(有partent)則刪掉if (mView.getParent() != null) {......mWM.removeView(mView);}......//把Toast的View加入到窗體,當中mParams.type在構造函數中賦值為TYPE_TOAST!!!。!!
特別重要
mWM.addView(mView, mParams); ...... } } public void handleHide() {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.//凝視說得非常清楚了,不解釋,就是removeif (mView.getParent() != null) {mWM.removeView(mView);}mView = null;}}到此Toast的窗體加入原理就分析完成了,接下來我們進行總結。
5-3 Toast窗體源代碼分析總結及應用開發技巧
經過上面的分析我們總結例如以下:
通過上面分析及上圖直觀描寫敘述能夠發現,之所以Toast的顯示交由遠程的NotificationManagerService管理是由于Toast是每一個應用程序都會彈出的,并且位置和UI風格都差點兒相同。所以假設我們不統一管理就會出現覆蓋疊加現象,同一時候導致不好控制。所以Google把Toast設計成為了系統級的窗體類型,由NotificationManagerService統一隊列管理。
在我們開發應用程序時使用Toast注意事項:
通過分析TN類的handler能夠發現,假設想在非UI線程使用Toast須要自行聲明Looper。否則運行會拋出Looper相關的異常;UI線程不須要,由于系統已經幫忙聲明。
在使用Toast時context參數盡量使用getApplicationContext()。能夠有效的防止靜態引用導致的內存泄漏。
有時候我們會發現Toast彈出過多就會延遲顯示。由于上面源代碼分析能夠看見Toast.makeText是一個靜態工廠方法,每次調用這種方法都會產生一個新的Toast對象,當我們在這個新new的對象上調用show方法就會使這個對象加入到NotificationManagerService管理的mToastQueue消息顯示隊列里排隊等候顯示;所以假設我們不每次都產生一個新的Toast對象(使用單例來處理)就不須要排隊。也就能及時更新了。
6 Android應用Activity、Dialog、PopWindow、Toast窗體顯示機制總結
能夠看見上面不管Acitivty、Dialog、PopWindow、Toast的實質事實上都是例如以下接口提供的方法操作:
public interface ViewManager {public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view); }整個應用各種窗體的顯示都離不開這三個方法而已。僅僅是token及type與Window是否共用的問題。
【工匠若水 http://blog.csdn.net/yanbober 轉載煩請注明出處,尊重勞動成果】
總結
以上是生活随笔為你收集整理的Android应用Activity、Dialog、PopWindow、Toast窗体加入机制及源代码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软发布 Win10 Build 190
- 下一篇: android sina oauth2.