【轉載請注明出處:http://blog.csdn.net/feiduclear_up CSDN 廢墟的樹】
1.前言
在Android系統中窗口(Window)分3種類型:利用窗口,子窗口,系統窗口。上1篇博客分析了Android利用窗口Window的創建進程,接下來這篇博客來學習其他兩種窗口類型的實現機制。Android開發中常常會使用到Dialog,PopupWindow,Toast等對話框來作為提示信息或和用戶交互。但是這些對話框其實都是窗口,它們的創建和移除機制也就是Android系統對窗口的添加和刪除的進程了。
這篇博客從源碼角度來分析Dialog,PopupWindow,Toast的實現原理。
2.Dialog實現的機制
在Android系統中Dialog對話框是子窗口,也就是Dialog對話框窗口必須要有1個父窗口,那末Dialog對話框窗口的父窗口是誰呢?我不說相信大家也知道了吧!沒錯就是Activity利用窗口,為何呢?這篇博客來為你解答!
相信很多人平日里用的最多的對話框還是AlertDialog,不過今天它可不是主角,Dialog才是我們今天的重點。其實AlertDialog只是Google官方定制了很多不同主題不同布局的Dialog而已,AlertDialog繼承自Dialog類。因此我們只分析Dialog的實現機制。使用對話框都是在Activity中,因此在Activity中創建最簡單的Dialog對話框代碼以下:
Dialog dialog = new Dialog(MainActivity.this) dialog.setContentView(R.layout.dialog) dialog.show() //取消對話框
dialog.cancel()
以上是最簡單的對話框使用示例,先創建1個Dialog對象實例,然后Dialog加載布局,最后調用show方法來顯示該對話框,當用戶按“back”鍵時系統會自動調用cancel方法來移除Dialog對話框窗口?,F在我們就就從以上幾個進程來詳細分析Dialog創建進程。
2.1Dialog對話框創建
來看看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) { if (createContextThemeWrapper) { if (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;
} 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);
}
}
分析:
在Dilaog的構造方法中主要做了以下工作:
-
根據參數createContextThemeWrapper的值來決定是使用參數theme指定的主題還是使用其父窗口Activity的主題。
-
調用Context#getSystemService方法取得當前利用的窗口管理器WindowManager對象,有上1篇博客知道:1個利用不管有多少個Activity都只有1個WindowManager對象用于管應當前利用中的所有窗口。
-
為Dialog對話框創建1個窗口Window對象,Window是個抽象類,其實現指向PhoneWindow類。
-
給窗口設置事件回調監聽,由于在Dialog類中實現了Window#Callback接口類,該接口類目的是讓Dialog對話框的窗口具有處理響應按鍵觸摸事件的能力,這也就是為何用戶默許創建的Dialog對話框可以響應“Back”回退按鍵事件和點擊對話框窗口之外的地方Dialog對話框會自動消失隱藏。由此可知,Dialog和Activity都實現了消息處理。
-
設置Window類的內部成員變量值WindowManager,由此知道Window的WindowManager和Dilaog的WindowManager指向同1個對象。
-
設置當前Dialog窗口的對齊方式為居中,這就是為何我們默許的對話框都是居中顯示了吧。
-
創建對話框的事件監聽對象,用于對話框顯示,消失,取消時的1些監聽操作。
Dialog內部創建了1個Window對象,窗口是1個抽象的東西,和Activity利用窗口1樣,需要往窗口Window中添加視圖View來顯示內容。因此調用setContentView方法來加載對話框的布局視圖。
2.2Dialog加載布局
Dialog#setContentView源碼以下:
public void setContentView(int layoutResID) {
mWindow.setContentView(layoutResID);
}
分析:
該方法將操作轉發給Window類中的setContentView方法,但是mWindow對象是指向PhoneWindow類的,也就是調用PhoneWindow類中的setContentView方法。到此處我們發現Dialog加載布局的流程和Activity加載布局的流程是1樣的。因此這里就不仔細分析了,可以參考上1篇博客。到此,Dialog對話框窗口Window內部就已添加了視圖DecorView了。那末剩下的事就是Dilaog對話框怎樣顯示在手機屏幕上了。
2.3 Dialog的顯示
在創建完Dialog對話框以后我們僅僅調用Dialog#show方法就能夠讓該對話框顯示在當前Activity上。
Dilaog#show源碼以下:
public void show() { if (mShowing) { if (mDecor != null) { if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
} return;
} mCanceled = false; if (!mCreated) {
dispatchOnCreate(null);
} onStart(); mDecor = mWindow.getDecorView(); if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) { final ApplicationInfo info = mContext.getApplicationInfo();
mWindow.setDefaultIcon(info.icon);
mWindow.setDefaultLogo(info.logo);
mActionBar = new WindowDecorActionBar(this);
}
WindowManager.LayoutParams l = mWindow.getAttributes(); if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
} try { mWindowManager.addView(mDecor, l); mShowing = true; sendShowMessage();
} finally {
}
}
分析:
在show方法里主要做了以下幾件工作:
-
判斷當前Dialog對話框窗口是存在,如果存在直接讓其顯示便可;如果當前窗口不存在,則調用Dialog的回調方法onCreate方法,用戶可以在onCreate回調方法中創建1個新的Dialog對話框。
-
取得Dialog對話框的頂層視圖DecorView對象賦值給成員變量mDecor用于addView方法的參數。
-
根據條件為當前對話框窗口設置導航欄logo圖標等。
-
取得當前窗口的參數屬性賦值給l,用于addView方法的參數。
-
調用WindowManager#addView方法添加Dialog對話框窗口。
自此Dialog對話框的添加進程已完成了,回過頭來會發現,其實Dialog對話框窗口的創建添加進程和Activity利用窗口進程是1樣1樣的。
2.4 移除Dialog對話框
移除或隱藏對話框的代碼也很簡單。用戶僅僅調用Dialog#cancel方法就能夠移除當前Activity之上的對話框了。
public void cancel() { if (!mCanceled && mCancelMessage != null) {
mCanceled = true; Message.obtain(mCancelMessage).sendToTarget();
}
dismiss();
}
該方法也很簡單,先發送移除Dialog時的監聽事件,以后將操作轉發到dismiss方法中。
/**
* Dismiss this dialog, removing it from the screen. This method can be
* invoked safely from any thread. Note that you should not override this
* method to do cleanup when the dialog is dismissed, instead implement
* that in {@link #onStop}.
*/ @Override public void dismiss() { if (Looper.myLooper() == mHandler.getLooper()) { dismissDialog();
} else { mHandler.post(mDismissAction);
}
}
分析:
注釋解釋的很清楚了:該方法可以安全的在任何線程中調用,也就是說可以在子線程中移除對話框而不報錯。Looper.myLooper()方法取得的Looper對象是當前線程的Looper,而mHandler.getLooper()方法取得的Looper對象是mHandler所在線程的Looper。由于Android系統規定只要有關UI操作都必須在主線程中,而我們在創建Dialog是在主線程中,mHandler對象是在主線程中創建的,因此mHandler.getLooper()就是主線程的Looper。
以上代碼:如果當前線程為主線程,則調用dismissDialog方法,如果是子線程,則利用Handler將此操作發送到UI線程中操作。
1.在主線程中移除對話框
void dismissDialog() { if (mDecor == null || !mShowing) { return;
} if (mWindow.isDestroyed()) {
Log.e(TAG, "Tried to dismissDialog() but the Dialogs window was already destroyed!"); return;
} try { mWindowManager.removeViewImmediate(mDecor);
} finally { if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels(); onStop(); mShowing = false; sendDismissMessage();
}
}
分析:
-
如果當前Dialog窗口的視圖DecorView為空或當前窗口不存在,則不做任何處理,直接退出當前方法便可。
-
如果當前Dialog窗口已被燒毀了也不做任何處理。
-
調用WindowManager#removeView方法來移除當前對話框窗口。
該方法主要作用就是從Activity的窗口管理器mWindowManager中移除對話框窗口的視圖,也就是完成了該對話框的移除操作。
2.在子線程中調用Dialog#cancel
當子線程調用時就會履行 mHandler.post(mDismissAction)代碼。該代碼的作用就是將操作轉發到主線程中。我們看看mDismissAction的實現以下:
private final Runnable mDismissAction = new Runnable() { public void run() {
dismissDialog();
}
};
該類很簡單,僅僅實現了run回調方法,然后調用了dismissDialog方法。
2.5 Dialog 觸摸事件處理
我們知道Dialog默許是響應“Back”返回鍵當前對話框消失事件和點擊Dialog對話框視圖之外的地方當前對話框也會消失,而默許的PopupWindow對話框是不支持以上兩種事件操作的。那末為何會是這樣呢?此處先分析Dialog對觸摸事件的處理,下1節分PopupWindow不支持事件處理的緣由。
響應“Back”返回鍵
public class Dialog implements DialogInterface, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback { ........ public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
&& !event.isCanceled()) {
onBackPressed(); return true;
} return false;
}
........ public void onBackPressed() { if (mCancelable) {
cancel();
}
}
}
分析:
在Dialog類中實現了按鍵事件KeyEvent.Callback接口類,因此當有用戶按鍵輸入事件產生時就會調用KeyEvent.Callback接口類中的相應方法。當按鍵操作有“抬起”的操作行動時,系統會調用onKeyUp方法。而Dialog類中的onKeyUp方法中會檢查當前按鍵事件是不是為“KeyEvent.KEYCODE_BACK”事件,且當前輸入事件沒有被取消,那末會調用onBackPressed,而該方法中判斷如果當前對話框可以被取消則調用cancel方法來取消或隱藏當前對話框。因此Dialog也就響應了“Back”按鍵事件以后對話框消失。
Dialog點擊對話框視圖之外的地方消失
public class Dialog implements DialogInterface, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback { ........ public boolean dispatchTouchEvent(MotionEvent ev) { if (mWindow.superDispatchTouchEvent(ev)) { return true;
} return onTouchEvent(ev);
}
........
}
分析:
Dialog類一樣也實現了Window.Callback接口事件,同時調用Window#setCallback方法設置了該事件的回調,因此Dialog也一樣具有響應觸摸事件的功能。當用戶點擊手機屏幕時,就系統就會自動調用dispatchTouchEvent方法來分發當前窗口的觸摸事件。該方法前后做了兩件事情:
-
先調用Dialog的窗口Window對象的方法Window#superDispatchTouchEvent來處理觸摸按鍵事件。
-
如果Window窗口的觸摸按鍵事件處理返回為false,則調用Dialog#onTouchEvent方法來繼續處理觸摸按鍵事件。
有關觸摸事件傳遞機制請參考這篇博客:Android事件分發機制完全解析,帶你從源碼的角度完全理解(上)。
當用戶點擊Dialog窗口視圖之外的地方時,最后時會履行Dialog#onTouchEvent方法的,感興趣的同學可以自行研究下!那末我們來看看Dialog#onTouchEvent方法源碼以下:
public boolean onTouchEvent(MotionEvent event) { if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
cancel(); return true;
} return false;
}
分析:
該方法也很簡單,如果if條件滿足,則直接調用cancel方法來取消當前對話框,if條件不滿足時不做任何處理直接返回。那末我們來看看甚么情況下if添加滿足致使了調用cancel方法取消對話框。必須滿足3個條件:當前對話框可以被取消,對話框正在顯示,和Window.shouldCloseOnTouch方法返回true。前兩個條件默許都滿足,那末來看看第3個條件甚么情況下滿足吧!
Window.shouldCloseOnTouch源碼以下:
/** @hide */ public boolean shouldCloseOnTouch(Context context, MotionEvent event) { if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) { return true;
} return false;
}
分析:
該方法需要滿足4個條件才會返回true。
-
布爾變量mCloseOnTouchOutside:表示是不是支持點擊窗口之外的地方窗口可消失。Dialog對話框的窗口默許支持,也就是該條件滿足。如果想修改該條件,你可以調用Dialog#setCanceledOnTouchOutside(false)方法來到達點擊窗口之外的地方Dialog消失,其實終究是設置mCloseOnTouchOutside變量為false,然后致使shouldCloseOnTouch方法返回false。
-
當前觸摸事件是不是為“MotionEvent.ACTION_DOWN”手指按下事件,自然滿足。
-
調用isOutOfBounds方法判斷當前手指導擊的坐標是不是在Dialog對話框窗口視圖以外?
-
當前Dialog對話框窗口是不是添加了視圖DecorView?如果對話框顯示出來了,自然窗口DecorView對象不為空。
因此有上面4個條件分析我們得知:只有當isOutOfBounds方法返回true時,條件才成立,shouldCloseOnTouch方法返回值才為true,手指導擊Dialog窗口以外的地方Dialog才會消失。所以主要看isOutOfBounds方法的實現了。
Window#isOutOfBounds源碼以下:
private boolean isOutOfBounds(Context context, MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop(); final View decorView = getDecorView(); return (x < -slop) || (y < -slop) || (x > (decorView.getWidth()+slop))
|| (y > (decorView.getHeight()+slop));
}
此方法實現也很簡單,判斷當前手指按下點擊屏幕的坐標x,y是不是在Window窗口的視圖DecorView寬度高度以外,如果是,則返回true,否則返回false。
至此:有關Dialog響應“Back”返回按鍵事件和點擊Dialog窗口以外的地方Dialog自動消失事件分析完成了。其實這1塊的原理和Activity處理“Back”返回鍵當前Activity會調用finish方法1樣。
Dialog總結:
Dialog對話框窗口Window的實現機制和Activity1樣。Dialog有1個Window對象,該對象屬于PhoneWindow類型用于描寫Dialog對話框窗口;PhoneWindow類有1個內部類DecorView,用于描寫當前窗口的頂層視圖。一樣Dialog也實現了Window.Callback接口回調,以便Dialog也能夠處理用戶的觸摸和按鍵事件。
Dialog窗口Window視圖View層次關系圖以下:
開發中用的最多的對話框AlertDialog,如果需要定制自己的對話框風格或AlertDialog沒法滿足你的需求時,就能夠斟酌下PopupWindow對話框了。彈出式對話框PopupWinsow的使用也很簡單,僅僅調用已下幾行代碼就可以實現最簡單的對話框了!
//取得父窗口視圖中的某個View對象
View parentView = findViewById(R.id.main) //加載popupWindow對話框布局
View popWindow = LayoutInflater.from(MainActivity.this).inflat(R.layout.dialog, null) //創建對話框
PopupWindow pw = new PopupWindow(popWindow,ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, true) //顯示對話框
pw.showAtLocation(parentView, Gravity.CENTER, 0, 0) //移除對話框
pw.dismiss()
分析:
使用PopupWindow彈出式對話框主要以下幾個步驟:
1. 取得父窗口中的某1個View對象
2. 加載對話框視圖布局文件
3. 創建對話框實例
4. 顯示該對話框
5. 移除對話框
我們從PopupWindow類中的構造方法開始分析
PopupWindow構造方法源碼以下:
public PopupWindow(View contentView, int width, int height, boolean focusable) { if (contentView != null) { mContext = contentView.getContext(); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
} setContentView(contentView); setWidth(width); setHeight(height); setFocusable(focusable);
}
分析:
PopupWindow構造方法中主要做了以下幾個工作:
-
取得父窗口的context對象,也就是當前Activity的Context對象,然后有context對象取得全部利用的WindowManager對象,從上1篇博客知道:1個利用只有唯逐一個WindowManager對象用于管理全部利用的窗口。
-
設置對話框布局,該操作主要是將對話框視圖賦值給PopupWindow類的成員變量mContentView。
-
分別設置對話框布局的寬度,高度,和取得 焦點的能力。這3個方法的主要操作還是對PopupWindow類中的成員變量mWidth,mHeight,mFocusable賦值,以便對話框顯示的時候使用。
PopupWindow對話框創建完成,接下來看看怎樣來顯示它。
PopupWindow對話框顯示的方法有兩種:
-
showAtLocation
-
showAsDropWown
其實這兩種方法實現的原理是相同的,僅僅是顯示的位置控制不1樣而已,因此這里就分析其中1個方法showAtLocation實現的原理。
PopupWindow#showAtLocation源碼以下:
public void showAtLocation(View parent, int gravity, int x, int y) {
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
該方法僅僅將操作轉發給同名方法,只是利用第1個參數parent來取得父窗口的標識符token對象,但是,父窗口Window視圖中的任何1個View得到的標識符都是同1個對象。因此在構建parent參數的時候只要滿足1個條件就能夠了:那就是參數parent只要是對話框所依賴的父類窗口中的其中1個子View便可,也就是Activity布局中的任何1個子View都可以作為PopupWindow類中showAtLocation方法的第1個參數。
同名方法showAtLocation源碼以下:
public void showAtLocation(IBinder token, int gravity, int x, int y) { if (isShowing() || mContentView == null) { return;
}
unregisterForScrollChanged(); mIsShowing = true;
mIsDropdown = false; WindowManager.LayoutParams p = createPopupLayout(token); p.windowAnimations = computeAnimationResource(); preparePopup(p); if (gravity == Gravity.NO_GRAVITY) {
gravity = Gravity.TOP | Gravity.START;
}
p.gravity = gravity; p.x = x;
p.y = y; if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; if (mWidthMode < 0) p.width = mLastWidth = mWidthMode; invokePopup(p);
}
以上方法主要做了3件事:
-
創建對話框窗口布局參數
-
創建對話框窗口的視圖
-
添加對話框窗口的進程
我們順次來分析以上3步:
1.創建窗口參數:
PopupWindow#createPopupLayoutParams源碼以下:
private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) { final WindowManager.LayoutParams p = new WindowManager.LayoutParams(); p.gravity = Gravity.START | Gravity.TOP; p.flags = computeFlags(p.flags); p.type = mWindowLayoutType; p.token = token; p.softInputMode = mSoftInputMode; p.windowAnimations = computeAnimationResource(); if (mBackground != null) {
p.format = mBackground.getOpacity();
} else {
p.format = PixelFormat.TRANSLUCENT;
} if (mHeightMode < 0) {
p.height = mLastHeight = mHeightMode;
} else {
p.height = mLastHeight = mHeight;
} if (mWidthMode < 0) {
p.width = mLastWidth = mWidthMode;
} else {
p.width = mLastWidth = mWidth;
} return p;
}
分析:
該方法主要是設置對話框的 gravity(對齊方式),flag(窗口特點),type(窗口類型),softInputMode (軟輸入法模式),windowAnimations(窗口相干動畫),width,height等參數。
2.創建對話框視圖
API22 PopupWindow#preparePopup源碼以下:
private void preparePopup(WindowManager.LayoutParams p) { if (mContentView == null || mContext == null || mWindowManager == null) { throw new IllegalStateException("You must specify a valid content view by " + "calling setContentView() before attempting to show the popup.");
} if (mBackground != null) { final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); int height = ViewGroup.LayoutParams.MATCH_PARENT; if (layoutParams != null &&
layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
height = ViewGroup.LayoutParams.WRAP_CONTENT;
}
PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, height
);
popupViewContainer.setBackground(mBackground);
popupViewContainer.addView(mContentView, listParams);
mPopupView = popupViewContainer;
} else { mPopupView = mContentView;
} mPopupView.setElevation(mElevation);
mPopupViewInitialLayoutDirectionInherited =
(mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
mPopupWidth = p.width;
mPopupHeight = p.height;
}
分析:
有以上代碼我們發現在創建對話框窗口視圖進程中有兩種情況
-
窗口背景mBackground不為空。
-
窗口背景mBackground為空。
而我們知道在PopupWindow類中成員變量mBackground默許是為空的,只有調用setBackgroundDrawable方法才能修改mBackground成員變量的值,也就是為PopupWindow對話框設置背景。
當窗口背景mBackground不為空時,if條件滿足,先創建PopupViewContainer對象,該對象是FrameLayout類型。然后將窗口布局視圖mContentView添加到popupViewContainer視圖上,也就是PopupViewContainer類作為父類視圖來添加窗口的布局視圖mContentView。也就是當前對話框窗口視圖mContentView外面還包裹著1成PopupViewContainer。那末我們來看看PopupViewContainer做了甚么工作。
private class PopupViewContainer extends FrameLayout { private static final String TAG = "PopupWindow.PopupViewContainer"; public PopupViewContainer(Context context) { super(context);
} @Override protected int[] onCreateDrawableState(int extraSpace) { if (mAboveAnchor) { final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET); return drawableState;
} else { return super.onCreateDrawableState(extraSpace);
}
} @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (getKeyDispatcherState() == null) { return super.dispatchKeyEvent(event);
} if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null) {
state.startTracking(event, this);
} return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null && state.isTracking(event) && !event.isCanceled()) { dismiss(); return true;
}
} return super.dispatchKeyEvent(event);
} else { return super.dispatchKeyEvent(event);
}
} @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) { return true;
} return super.dispatchTouchEvent(ev);
} @Override public boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); if ((event.getAction() == MotionEvent.ACTION_DOWN)
&& ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
dismiss(); return true;
} else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
dismiss(); return true;
} else { return super.onTouchEvent(event);
}
} @Override public void sendAccessibilityEvent(int eventType) { if (mContentView != null) {
mContentView.sendAccessibilityEvent(eventType);
} else { super.sendAccessibilityEvent(eventType);
}
}
}
}
PopupViewContainer 繼承自FrameLayout是1個ViewGroup是圖組,然后你會發現其實該類里面并沒有實現甚么邏輯處理,僅僅是重寫了dispatchKeyEvent和dispatchTouch按鍵和觸摸事件分發而已。而你會發現在按鍵和觸摸事件方法里面處理了點擊PopupWindow對話框以外的像素位置時,對話框調用了dismiss方法,也就是移除對話框。并且處理了按返回鍵時對話框移除的事件,一樣當用戶按back鍵時也調用了dismiss方法。這就是為何PopupWindow在默許情況下是不響應back事件和點擊對話框以外的地方PopupWindow是不消失的。所以,如果你想要你的PopupWindow類型的對話框能像Dialog1樣響應back和點擊對話框之外的地方消失,你就能夠調用PopupWindow#setBackgroundDrawable方法來實現了。
當用戶沒有設置窗口背景也就是沒有調用PopupWindow#setBackgroundDrawable方法時mBackground為空,那末當前窗口的視圖就直接是mContentView了。但是所有View默許的按鍵和觸摸事件是沒有處理back事件和點擊對話框以外的地方對話框消失的處理的。因此,使用PopupWindow對話框不設置對話框背景時是不響應“back”返回按鍵和點擊窗口以外的地方消失的。
3.3添加對話框窗口進程
PopupWindow#invokePopup源碼以下:
private void invokePopup(WindowManager.LayoutParams p) { if (mContext != null) {
p.packageName = mContext.getPackageName();
}
mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
mWindowManager.addView(mPopupView, p);
}
分析:
該方法也很簡單,主要是調用了WindowManager#addView方法來添加對話框視圖。從而PopupWindow對話框顯示在Activity利用窗口之上了。
3.4移除對話框
public void dismiss() { if (isShowing() && mPopupView != null) { mIsShowing = false;
unregisterForScrollChanged(); try {
從Activity上移除對話框視圖
mWindowManager.removeViewImmediate(mPopupView);
} finally { if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { ((ViewGroup) mPopupView).removeView(mContentView);
}
mPopupView = null; if (mOnDismissListener != null) {
mOnDismissListener.onDismiss();
}
}
}
}
分析:
移除對話框的進程和Dialog移除對話框相識,這里不仔細分析了。
自此PopupWindow對話框的創建,添加,移除的進程已分析完成了。其主要流程就是取得當前利用程序的WindowManager對象,然后將對話框的視圖添加到WindowManager上來顯示PopupWindow對話框,調用WindowManager#remove方法移除對話框視圖來到達移除當前對話框。所以PopupWindow類型的對話框必須要依附在某1個Activity之上,也就是PopupWindow是1個子窗口。
Dialog對話框和PopupWindow對話框最主要的區分就是Dialog窗口內部具有1個PhoneWindow對象來處理了輸入事件,而PopupWindow窗口內部沒有PhoneWindow對象來理輸入事件。這也就致使了Dialog能響應“Back”返回鍵對話框消失和點擊對話框以外的地方對話框消失而PopupWindow不能的緣由。
PopupWindow對話框窗口視圖關系以下:
4.Toast顯示的機制
Toast也常常使用,而且使用簡單,僅僅需要以下1行代碼便可實現吐司效果
Toast.makeText(MainActivity.this, "Toast", Toast.LENGTH_SHORT).show()
其實分兩步調用,Toast#makeText,Toast#show。
4.1Toast#makeText
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 }
分析:首先調用Toast的構造方法,然后加載Toast布局視圖,將布局視圖和Toast顯示時間參數賦值給Toast類的成員變量mNextView和mDuration。
4.2Toast構造方法
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) }
Toast構造方法也很簡單,new了1個內部類TN,然后給TN類中的成員變量mY和mGravity賦值。那末主要的操作就在內部類TN的構造方法了。
Toast#TN構造方法
private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams() ......
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 //Toast高度
params.height = WindowManager.LayoutParams.WRAP_CONTENT //Toast寬度
params.width = WindowManager.LayoutParams.WRAP_CONTENT params.format = PixelFormat.TRANSLUCENT //Toast入場動畫
params.windowAnimations = com.android.internal.R.style.Animation_Toast //Toast窗口類型
params.type = WindowManager.LayoutParams.TYPE_TOAST //標題
params.setTitle("Toast") //窗口特點標記符
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE }
}
TN類的構造方也很簡單,僅僅是創建了布局參數mParams并且賦值操作。
4.3Toast#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) { }
}
......... private static INotificationManager sService; static private INotificationManager getService() { if (sService != null) { return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService;
}
分析:在該方法中主要作用就是調用遠程服務NotificationManagerService中的enqueueToast方法將Toast的內部類TN對象入隊列的1個進程。進入NotificationManagerService類的enqueueToast方法看看
public class NotificationManagerService extends SystemService { .............
private final IBinder mService =
new INotificationManager.Stub() {
@Override
public void enqueueToast(String pkg, ITransientNotification callback,
int duration)
{
if (pkg ==
null || callback ==
null) {
Slog.e(TAG,
"Not doing toast. pkg=" + pkg +
" callback=" + callback);
return ;
}
final boolean isSystemToast = isCallerSystem() || (
"android".equals(pkg));
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG,
"Suppressing toast from package " + pkg +
" by user request.");
return;
}
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
if (
index >=
0) {
record = mToastQueue.get(
index);
record.update(duration);
}
else {
if (!isSystemToast) {
int count =
0;
final int N = mToastQueue.size();
for (
int i=
0; i
final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return;
}
}
}
} record = new ToastRecord(callingPid, pkg, callback, duration); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveLocked(callingPid);
} if (index == 0) {
showNextToastLocked();
}
} fina
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈
------分隔線----------------------------
------分隔線----------------------------