FrameWork层源码分析之popupWindow
继上文讲述了dialog的创建流程之后,接下来讲一下popwindow的创建以及和dialog的不同之处
首先看任何代码都要带着疑问去看,不然很容易一头雾水,先说几个问题
1.popwindow的显示分2种,一种是showAsDropDown和showAtLocation
其各自实现的原理
2.popwindow的动画是和dialog一样是window的动画么
3.popwindow的点击是如何处理的?
4.popwindow中和dialog一样新建了一个phonewindow,同时又用附属的activity的windowManager添加的么?
首先简单看下显示的代码
View mPopView = getLayoutInflater().inflate(R.layout.popwindow_layout, null);
// 将转换的View放置到 新建一个popuwindow对象中
mPopupWindow = new PopupWindow(mPopView,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT);
// 点击popuwindow外让其消失
mPopupWindow.setOutsideTouchable(true);
mPopupWindow.showAsDropDown(mCenterTx, Gravity.CENTER, 0, 0);
可以看到用法和dialog如出一辙,主要看下其构造和show的逻辑吧
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);
}
可以看到用的还是依附的activity的windowManager,然后设置了几个属性。
接下来看下关键的showAsDropDown方法
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
if (isShowing() || mContentView == null) {
return;
}
TransitionManager.endTransitions(mDecorView);
registerForScrollChanged(anchor, xoff, yoff, gravity);
mIsShowing = true;
mIsDropdown = true;
//关键点一
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
preparePopup(p);关键点一
final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
updateAboveAnchor(aboveAnchor);
//关键点二
invokePopup(p);
}
关键点一:
public IBinder getWindowToken() {
return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
}
无论是popwindow还是dialog时,show的时候都要绑定所属activity的token,而mAttachInfo这个对象是viewRootImp初始化的时候新建的
而初始化的时机就是WindowManagerGlobal.addview时候所创建的
所以这个token是为null的,在onCreate时候
接下来看下createPopupLayoutParams方法
private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
// These gravity settings put the view at the top left corner of the
// screen. The view is then positioned to the appropriate location by
// setting the x and y offsets to match the anchor's bottom-left
// corner.
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;
}
// Used for debugging.
p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));
return p;
}
很显然,用的动画也是windowManger里属性的动画
来看下关键点二
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.");
}
// The old decor view may be transitioning out. Make sure it finishes
// and cleans up before we try to create another one.
if (mDecorView != null) {
mDecorView.cancelTransitions();
}
// When a background is available, we embed the content view within
// another view that owns the background drawable.
if (mBackground != null) {
mBackgroundView = createBackgroundView(mContentView);
mBackgroundView.setBackground(mBackground);
} else {
mBackgroundView = mContentView;
}
mDecorView = createDecorView(mBackgroundView);
// The background owner should be elevated so that it casts a shadow.
mBackgroundView.setElevation(mElevation);
// We may wrap that in another view, so we'll need to manually specify
// the surface insets.
final int surfaceInset = (int) Math.ceil(mBackgroundView.getZ() * 2);
p.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset);
p.hasManualSurfaceInsets = true;
mPopupViewInitialLayoutDirectionInherited =
(mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
mPopupWidth = p.width;
mPopupHeight = p.height;
}
可以看到popwindow并没有像dialog一样,新建了一个phonewindow,那它是window么?肯定是,只不过它的windowManager.layoutParams的属性要自己配置,而且它也没有常规setContentView的这么多的嵌套
那既然没有新建phonewindow,那它的点击事件的回掉又从那里来呢?
private class PopupDecorView extends FrameLayout {
private TransitionListenerAdapter mPendingExitListener;
public PopupDecorView(Context context) {
super(context);
}
@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) {
final KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
final KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null && state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
} else {
return super.dispatchKeyEvent(event);
}
}
....
可以看到它的点击事件全部有自己的PopupDecorView重写了,并没有遵循传统的decorview的事件的传递方式,所以也没有了回掉
关键点三
private void invokePopup(WindowManager.LayoutParams p) {
if (mContext != null) {
p.packageName = mContext.getPackageName();
}
final PopupDecorView decorView = mDecorView;
decorView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
mWindowManager.addView(decorView, p);
if (mEnterTransition != null) {
decorView.requestEnterTransition(mEnterTransition);
}
}
这里还是用activity的windowManager添加的,后续的操作和dialog的显示是一样的,这里就不再特别分析了,有个关于token的地方要着重说下
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();
}
}
if (curTitle == null || curTitle.length() == 0) {
String title;
if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) {
title="Media";
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY) {
title="MediaOvr";
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
title="Panel";
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) {
title="SubPanel";
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL) {
title="AboveSubPanel";
} else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) {
title="AtchDlg";
} else {
title=Integer.toString(wp.type);
}
if (mAppName != null) {
title += ":" + mAppName;
}
wp.setTitle(title);
}
} else {
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
if ((curTitle == null || curTitle.length() == 0)
&& mAppName != null) {
wp.setTitle(mAppName);
}
}
if (wp.packageName == null) {
wp.packageName = mContext.getPackageName();
}
if (mHardwareAccelerated) {
wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
可以看到由于popwindow是子窗口类型,token为null的情况下会调用activity的decorview的token,当然在oncreate时也为null了,而dialog就不同了,默认给到的就是null,走了else的逻辑直接把mApptoken也就是activity的token给到了,这也是为何popwindow不能再oncreate显示而dialog可以的最根本原因
总结
先来说下几个问题
1.显示的位置其实和WindowManager.layoutParams有关showAsDropDown其实就是算出锚地的位置然后放到其下方,而showAtLocation没有此段逻辑
2.动画的显示,其实也是依靠WindowManager.layoutParam的属性,可以说一样的
3.点击区域的处理,这个和dialog有所不同,由于不是系统的decorview,所以重写了分发的方法。
4.源码中表明了都是windowManager添加的,但是popwindow并没有新建phonewinow,其实新建一个子window只要windowManager和decorview就能新建,新建了phonewindow默认的属性就是应用类型的window而已,这也是两者最大的不同之处
很多人说popwindow比dialog都要“轻”,但是又说不出个所以然来,其实无非就是子window上不能新建子window,而dialog上可以添加子window罢了
注意点:
1.popwindow和dialog都会存在activity的token不存在了,而不能显示的情况.要做好基类的统一处理
2.popwinow一般用在依靠某个view显示的位置情况,dialog则不然