读源码:PopupWindow
Posted Iaouei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读源码:PopupWindow相关的知识,希望对你有一定的参考价值。
读源码是为了了解并学习它的实现机制,并更好的运用它,如果在读源码之前已经知道它的怎么运用,这将会更容易理解源码。所以在这读源码开头我推荐阅读一下一位大神写的相关博文,浅显易懂,条理清晰:
PopUpWindow使用详解(一)——基本使用
PopUpWindow使用详解(二)——进阶及答疑
PopupWindow这个类用来实现一个弹出框,可以使用任意布局的View作为其内容,这个弹出框是一个浮动的容器,悬浮在当前activity之上的.
PopupWindow可以说是一种view,但它不同于一般的view,它不继承自view类或其子类,而是直接继承自Object ,是基于WindowManager出生的。
PopupWindow类中有一个接口,两个内部类,从内部类从这里切入,方便理解与PopupWindow密切相关的实例,从而更容易理顺类的思路。
一,内部的接口:OnDismissListener
当弹出框被dismissed(解雇,驳回,消失)调用这个接口.其内结构非常简单:
public interface OnDismissListener
public void onDismiss();
看到这个接口,相信都很有熟悉感,因为Dialog中也有一个一样的接口。Dialog通过setOnDismissListener(OnDismissListener onDismissListener)在点击对话框按钮消失后执行设置的行为(如dismissDialog(int id) ),PopupWindow的这个接口和对话框的功能相同,在PopupWindow同样可以找到对接口OnDismissListener进行包装的和对话框的类一样的setOnDismissListener(OnDismissListener onDismissListener)方法:
public void setOnDismissListener(OnDismissListener onDismissListener)
mOnDismissListener = onDismissListener;
用法也一样。
二,弹出框的根view:PopupDecorView
如果理解什么是DecorView,看到这个类名就会知道它的作用。DecorView是窗口界面所有视图的根,关于它推荐阅读Android中将布局文件/View添加至窗口过程分析 —- 从setContentView()谈起
PopupDecorView从字面意思猜测可能就是弹出框的根视图了。就是下图弹出框带来的阴影。
图片来自:PopUpWindow使用详解(一)——基本使用
PopupDecorView继承自FrameLayout,其内部七个方法可以分为两部分:事件分发和进出动画。
三,弹出框的背景view:PopupBackgroundView
这个内部类定义了弹出框的背景视图:
private class PopupBackgroundView extends FrameLayout
public PopupBackgroundView(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);
之所以贴出这段代码,是推荐学习这种Drawable式的自定义方式,可以参考学习Android Drawable 那些不为人知的高效用法
四,PopupWindow
先定义了三个与输入法相关的常量:INPUT_METHOD_FROM_FOCUSABLE,INPUT_METHOD_NEEDED,INPUT_METHOD_NOT_NEEDED。这些常量表示弹出框与输入法弹出框的关系。接着声明一些成员变量并且或初始化值。
值得注意的是PopupWindow有九个构造器,这些构造器可以分为两组,代表分别为:
public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
//1,获取context
mContext = context;
//2,获取WindowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//3,加载属性
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes);
final Drawable bg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0);
mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false);
// Preserve default behavior from Gingerbread. If the animation is
// undefined or explicitly specifies the Gingerbread animation style,
// use a sentinel value.
if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupAnimationStyle))
final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, 0);
if (animStyle == R.style.Animation_PopupWindow)
mAnimationStyle = ANIMATION_STYLE_DEFAULT;
else
mAnimationStyle = animStyle;
else
mAnimationStyle = ANIMATION_STYLE_DEFAULT;
final Transition enterTransition = getTransition(a.getResourceId(
R.styleable.PopupWindow_popupEnterTransition, 0));
final Transition exitTransition;
if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupExitTransition))
exitTransition = getTransition(a.getResourceId(
R.styleable.PopupWindow_popupExitTransition, 0));
else
exitTransition = enterTransition == null ? null : enterTransition.clone();
//4,
a.recycle();
//5,读取资源进行相关设置
setEnterTransition(enterTransition);//设置动画
setExitTransition(exitTransition);
setBackgroundDrawable(bg);//设置背景
public PopupWindow(View contentView, int width, int height, boolean focusable)
if (contentView != null)
//1,获取Context
mContext = contentView.getContext();
//2,获取WindowManager
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
//3,构造器参数
setContentView(contentView);
setWidth(width);
setHeight(height);
setFocusable(focusable);
然后其他的构造器都是这两个构造器的参数依次设为了默认。这两组构造器侧重点不同,在使用的时候可以根据情景需要和使用习惯进行选择。上面的第一组构造器方法在我们平时的自定义view中比较典型,所以如果记住会在自定义view中有所帮助。并且在其中值得注意的是在TypedArray后的recycle()调用,在TypedArray后调用recycle主要是为了缓存Style中属性,重复使用。当recycle被调用后,这就说明这个对象从现在可以被重用了。TypedArray 内部持有部分数组,它们缓存在Resources类中的静态字段中,这样就不用每次使用前都需要分配内存。recycle()源码为:
/**
* Give back a previously retrieved StyledAttributes, for later re-use.
*/
public void recycle()
synchronized (mResources.mTmpValue)
TypedArray cached = mResources.mCachedStyledAttributes;
if (cached == null || cached.mData.length < mData.length)
mXml = null;
mResources.mCachedStyledAttributes = this;
接下来是一些属性方法的set和get方法,然后这个类中的比较重要的逻辑方法:
public void showAtLocation(IBinder token, int gravity, int x, int y)
//1
if (isShowing() || mContentView == null)
return;
//2
TransitionManager.endTransitions(mDecorView);
//3
unregisterForScrollChanged();
//4
mIsShowing = true;
mIsDropdown = false;
//5
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
preparePopup(p);
//6
// Only override the default if some gravity was specified.
if (gravity != Gravity.NO_GRAVITY)
p.gravity = gravity;
p.x = x;
p.y = y;
//7
invokePopup(p);
和
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity)
//1 确定没出现并且view不为空
if (isShowing() || mContentView == null)
return;
//2 动画
TransitionManager.endTransitions(mDecorView);
//3 滚动监听
registerForScrollChanged(anchor, xoff, yoff, gravity);
//4 相关值设置
mIsShowing = true;
mIsDropdown = true;
//5 在此位置准备弹出框
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
preparePopup(p);
//6 更新,,,
final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
updateAboveAnchor(aboveAnchor);
//7 唤起弹出框
invokePopup(p);
showAtLocation()弹出框是在父控件绝对位置显示,showAsDropDown()是在相对位置出现。比较这两个方法可以发现,大致流程相似,某些细节不同。
先看此第三步骤,滚动监听。在类的开头部分有一个匿名内部类,作用为滚动监听:
//匿名内部类
private final OnScrollChangedListener mOnScrollChangedListener = new OnScrollChangedListener()
@Override
public void onScrollChanged()
final View anchor = mAnchor != null ? mAnchor.get() : null;
if (anchor != null && mDecorView != null)
final WindowManager.LayoutParams p = (WindowManager.LayoutParams)
mDecorView.getLayoutParams();
updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
mAnchoredGravity));
update(p.x, p.y, -1, -1, true);
;
然后再类中有注册监听和取消监听的私有方法:
private void unregisterForScrollChanged()
final WeakReference<View> anchorRef = mAnchor;
final View anchor = anchorRef == null ? null : anchorRef.get();
if (anchor != null)
final ViewTreeObserver vto = anchor.getViewTreeObserver();
vto.removeOnScrollChangedListener(mOnScrollChangedListener);
mAnchor = null;
private void registerForScrollChanged(View anchor, int xoff, int yoff, int gravity)
unregisterForScrollChanged();
mAnchor = new WeakReference<>(anchor);
final ViewTreeObserver vto = anchor.getViewTreeObserver();
if (vto != null)
vto.addOnScrollChangedListener(mOnScrollChangedListener);
mAnchorXoff = xoff;
mAnchorYoff = yoff;
mAnchoredGravity = gravity;
第五步骤的不同之处是在不同的位置上准备弹出框,首先获取位置p,然后调用preparePopup(p)方法准备弹出框:
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;
最大的不同在于第六步,showAtLocation的简单,比较一目了然,相比之下showAsDropDown的就比较复杂,显示调用findDropDownPosition(),然后调用updateAboveAnchor():
private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams p, int xoff,
int yoff, int gravity)
final int anchorHeight = anchor.getHeight();
final int anchorWidth = anchor.getWidth();
if (mOverlapAnchor)
yoff -= anchorHeight;
anchor.getLocationInWindow(mDrawingLocation);
p.x = mDrawingLocation[0] + xoff;
p.y = mDrawingLocation[1] + anchorHeight + yoff;
final int hgrav = Gravity.getAbsoluteGravity(gravity, anchor.getLayoutDirection())
& Gravity.HORIZONTAL_GRAVITY_MASK;
if (hgrav == Gravity.RIGHT)
// Flip the location to align the right sides of the popup and
// anchor instead of left.
p.x -= mPopupWidth - anchorWidth;
boolean onTop = false;
p.gravity = Gravity.LEFT | Gravity.TOP;
anchor.getLocationOnScreen(mScreenLocation);
final Rect displayFrame = new Rect();
anchor.getWindowVisibleDisplayFrame(displayFrame);
final int screenY = mScreenLocation[1] + anchorHeight + yoff;
final View root = anchor.getRootView();
if (screenY + mPopupHeight > displayFrame.bottom
|| p.x + mPopupWidth - root.getWidth() > 0)
// If the drop down disappears at the bottom of the screen, we try
// to scroll a parent scrollview or move the drop down back up on
// top of the edit box.
if (mAllowScrollingAnchorParent)
final int scrollX = anchor.getScrollX();
final int scrollY = anchor.getScrollY();
final Rect r = new Rect(scrollX, scrollY, scrollX + mPopupWidth + xoff,
scrollY + mPopupHeight + anchorHeight + yoff);
anchor.requestRectangleOnScreen(r, true);
// Now we re-evaluate the space available, and decide from that
// whether the pop-up will go above or below the anchor.
anchor.getLocationInWindow(mDrawingLocation);
p.x = mDrawingLocation[0] + xoff;
p.y = mDrawingLocation[1] + anchorHeight + yoff;
// Preserve the gravity adjustment.
if (hgrav == Gravity.RIGHT)
p.x -= mPopupWidth - anchorWidth;
// Determine whether there is more space above or below the anchor.
anchor.getLocationOnScreen(mScreenLocation);
onTop = (displayFrame.bottom - mScreenLocation[1] - anchorHeight - yoff) <
(mScreenLocation[1] - yoff - displayFrame.top);
if (onTop)
p.gravity = Gravity.LEFT | Gravity.BOTTOM;
p.y = root.getHeight() - mDrawingLocation[1] + yoff;
else
p.y = mDrawingLocation[1] + anchorHeight + yoff;
if (mClipToScreen)
final int displayFrameWidth = displayFrame.right - displayFrame.left;
final int right = p.x + p.width;
if (right > displayFrameWidth)
p.x -= right - displayFrameWidth;
if (p.x < displayFrame.left)
p.x = displayFrame.left;
p.width = Math.min(p.width, displayFrameWidth);
if (onTop)
final int popupTop = mScreenLocation[1] + yoff - mPopupHeight;
if (popupTop < 0)
p.y += popupTop;
else
p.y = Math.max(p.y, displayFrame.top);
p.gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
return onTop;
private void updateAboveAnchor(boolean aboveAnchor)
if (aboveAnchor != mAboveAnchor)
mAboveAnchor = aboveAnchor;
if (mBackground != null && mBackgroundView != null)
// If the background drawable provided was a StateListDrawable
// with above-anchor and below-anchor states, use those.
// Otherwise, rely on refreshDrawableState to do the job.
if (mAboveAnchorBackgroundDrawable != null)
if (mAboveAnchor)
mBackgroundView.setBackground(mAboveAnchorBackgroundDrawable);
else
mBackgroundView.setBackground(mBelowAnchorBackgroundDrawable);
else
mBackgroundView.refreshDrawableState();
第七步骤,弹出弹出框
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);
private void setLayoutDirectionFromAnchor()
if (mAnchor != null)
View anchor = mAnchor.get();
if (anchor != null && mPopupViewInitialLayoutDirectionInherited)
mDecorView.setLayoutDirection(anchor.getLayoutDirection());
showAtLocation和showAsDropDown就到此为止了,这个类中比较重要的方法还有dismiss(),update()等;还有比较典型的属性设置方法有setContentView(View contentView),setBackgroundDrawable(Drawable background),设置动画的方法逻辑等,在有时候自定义view时可以拿来参考。
另外,虽然PopupWindow相当于一个view,但由于它不是继承自view或其子类,所以PopupWindow类中没有onLayout(),onDraw()等方法。
这个源码读的比较粗暴,某些细节以后知识运用更熟练了再补充吧
以上是关于读源码:PopupWindow的主要内容,如果未能解决你的问题,请参考以下文章
从源码剖析PopupWindow 兼容Android 6.0以上版本点击外部不消失