读源码: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的主要内容,如果未能解决你的问题,请参考以下文章

读Zepto源码之样式操作

读Zepto源码之操作DOM

从源码剖析PopupWindow 兼容Android 6.0以上版本点击外部不消失

Android6.0源码分析之menu键弹出popupwindow菜单流程分析

Android基础 (11) PopupWindow详解

popupWindow 用法总结 控制位置