读源码: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()等方法。
这个源码读的比较粗暴,某些细节以后知识运用更熟练了再补充吧

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PopupWindow是Android中的一个弹出窗口,可以在屏幕上方或下方显示,常用于实现下拉菜单、弹出提示等功能。其中,update方法是用于更新PopupWindow的参数的。 PopupWindow的update方法有多个重载形式,常用的参数如下: 1. width和height:设置PopupWindow的宽度和高度,可以使用具体数值或者LayoutParams.WRAP_CONTENT、LayoutParams.MATCH_PARENT等常量。 2. x和y:设置PopupWindow在屏幕上的位置,以屏幕左上角为原点,x为水平方向的偏移量,y为垂直方向的偏移量。 3. gravity:设置PopupWindow的对齐方式,可以使用Gravity类中的常量,如Gravity.TOP、Gravity.BOTTOM等。 4. contentView:设置PopupWindow的内容视图,可以是一个View对象或者一个布局文件的资源ID。 5. animationStyle:设置PopupWindow的动画效果,可以使用R.style中定义的动画样式。 6. focusable:设置PopupWindow是否可以获取焦点,默认为false。 7. outsideTouchable:设置点击PopupWindow以外的区域是否可以关闭PopupWindow,默认为false。 8. backgroundDrawable:设置PopupWindow的背景,可以使用ColorDrawable或者其他Drawable对象。 9. inputMethodMode:设置输入法模式,可以使用InputMethodManager类中的常量,如InputMethodManager.INPUT_METHOD_FROM_FOCUSABLE等。 10. softInputMode:设置软键盘的显示模式,可以使用WindowManager.LayoutParams中的常量,如WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值