1. 程式人生 > >Android開發筆記(二) 關於彈窗

Android開發筆記(二) 關於彈窗

Android中提供幾種不同的彈窗模式,Toast,Dialog,PopupWindow 每種彈窗又對應了不同的應用場景,我們可以根據不同業務場景來選擇。下面將會分別介紹上面四種不同彈窗的應用,同時也對每中彈窗的原始碼和所遇到的問題進行分別分析。

1.Toast

Toast是Android中最輕量級的檢視,該檢視已浮於應用程式之上的形式呈現給使用者。它並不獲得焦點,即使使用者正在輸入什麼也不會受到影響,不會與使用者互動。旨在儘可能以不顯眼的方式,讓使用者看到提示的資訊。而且顯示的時間是有限制的,過一段時間後會自動消失,Toast本身可以控制顯示時間的長短。

Toast原始碼解析

我們都知道簡單應用Toast時,進行如下:

Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();

進入檢視原始碼:

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
                            throws Resources.NotFoundException {
    return makeText(context, context.getResources().getText(resId), duration);
}

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);
}

/**
 * Make a standard toast to display using the specified looper.
 * If looper is null, Looper.myLooper() is used.
 * @hide
 */
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
        @NonNull CharSequence text, @Duration int duration) {
        
    Toast result = new Toast(context, looper);

    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;
}

可以看出,首先new一個Toast物件,然後載入了一個簡單的系統佈局並將傳入的字串資訊設定到佈局中的TextView中。

我們看一下建構函式:

/**
 * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
 * @hide
 */
public Toast(@NonNull Context context, @Nullable Looper looper) {
    mContext = context;
    mTN = new TN(context.getPackageName(), looper);
    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);
}

建構函式中new一個TN的物件,然後設定了toast的Y的偏移量和Gravity方向。繼續來看TN類:

private static class TN extends ITransientNotification.Stub {
    .....
}

首先可以知道TN繼承了ITransientNotification.Stub,而ITransientNotification是一個aidl檔案,裡面內容是:

ITransientNotification.aidl:

package android.app;

/** @hide */
oneway interface ITransientNotification {
    void show();
    void hide();
}

我們可以看到TN中對這兩個方法的重寫:

/**
 * schedule handleShow into the right thread
 */
@Override
public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

/**
 * schedule handleHide into the right thread
 */
@Override
public void hide() {
    if (localLOGV) Log.v(TAG, "HIDE: " + this);
    mHandler.obtainMessage(HIDE).sendToTarget();
}

然後繼續分析TN中的建構函式:

TN(String packageName, @Nullable Looper looper) {
    // 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;
    params.height = WindowManager.LayoutParams.WRAP_CONTENT;
    params.width = WindowManager.LayoutParams.WRAP_CONTENT;
    params.format = PixelFormat.TRANSLUCENT;
    params.windowAnimations = com.android.internal.R.style.Animation_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;

    mPackageName = packageName;

    if (looper == null) {
        // Use Looper.myLooper() if looper is not specified.
        looper = Looper.myLooper();
        if (looper == null) {
            throw new RuntimeException(
                    "Can't toast on a thread that has not called Looper.prepare()");
        }
    }
    mHandler = new Handler(looper, null) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SHOW: {
                    IBinder token = (IBinder) msg.obj;
                    handleShow(token);
                    break;
                }
                case HIDE: {
                    handleHide();
                    // Don't do this in handleHide() because it is also invoked by
                    // handleShow()
                    mNextView = null;
                    break;
                }
                case CANCEL: {
                    handleHide();
                    // Don't do this in handleHide() because it is also invoked by
                    // handleShow()
                    mNextView = null;
                    try {
                        getService().cancelToast(mPackageName, TN.this);
                    } catch (RemoteException e) {
                    }
                    break;
                }
            }
        }
    };
}

Toast中給TN傳入的引數分別是:context.getPackageName(), null。方法裡定義了WindowManager.LayoutParams來確定了寬高的大小,視窗的載入動畫以及type和flags。接著定義了當前執行緒中的mHandler,裡面分別處理了
handleShow和handleHide。

public void handleShow(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
            + " mNextView=" + mNextView);
    // If a cancel/hide is pending - no need to show - at this point
    // the window token is already invalid and no need to do any work.
    if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
        return;
    }
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        mView = mNextView;
        Context context = mView.getContext().getApplicationContext();
        String packageName = mView.getContext().getOpPackageName();
        if (context == null) {
            context = mView.getContext();
        }
        mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        // We can resolve the Gravity here by using the Locale for getting
        // the layout direction
        final Configuration config = mView.getContext().getResources().getConfiguration();
        final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
        mParams.gravity = gravity;
        if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
            mParams.horizontalWeight = 1.0f;
        }
        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
            mParams.verticalWeight = 1.0f;
        }
        mParams.x = mX;
        mParams.y = mY;
        mParams.verticalMargin = mVerticalMargin;
        mParams.horizontalMargin = mHorizontalMargin;
        mParams.packageName = packageName;
        mParams.hideTimeoutMilliseconds = mDuration ==
            Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
        mParams.token = windowToken;
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeView(mView);
        }
        if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
        // Since the notification manager service cancels the token right
        // after it notifies us to cancel the toast there is an inherent
        // race and we may attempt to add a window after the token has been
        // invalidated. Let us hedge against that.
        try {
            mWM.addView(mView, mParams);
            trySendAccessibilityEvent();
        } catch (WindowManager.BadTokenException e) {
            /* ignore */
        }
    }
}

public void handleHide() {
    if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
    if (mView != null) {
        // note: checking parent() just to make sure the view has
        // been added...  i have seen cases where we get here when
        // the view isn't yet added, so let's try not to crash.
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeViewImmediate(mView);
        }

        mView = null;
    }
}

這裡面重要的是初始化了一個WindowManager物件,然後將mView,也就是mNextView即inflate的佈局,新增到WindowManager中來。而handleHide則是將上面的View移除。
以上完成了初始化,Toast是直接通過呼叫show來顯示的:

/**
 * Show the view for the specified duration.
 */
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) {
        // Empty
    }
}

首先判斷mNextView不能為空,然後取得名為INotificationManager的service物件:

static private INotificationManager getService() {
    if (sService != null) {
        return sService;
    }
    sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
    return sService;
}

這裡我們可以看到服務是通知的服務,這裡將TN加入到ToastRecord的列表中。由列表中逐一show出來,這裡呼叫的是剛才重寫的show方法,最後是上面的handleShow方法來處理。

由此我們可以得出,原來所有的Toast都是由WindowManager來進行管理的,通過addView和removeView來控制顯示和消失。而這個顯示和消失都是通過Notification的服務來進行後臺控制的。

瞭解了上面的原始碼後(SDK26),我們知道了在8.0以上中如果關閉了通知服務後,就無法顯示toast。由此我們的解決方案可以是,判斷通知是否被關閉,如果沒有關閉就使用系統通知,否則就使用自定義Toast來模擬顯示,這裡我們可以借鑑Toast內部的實現原始碼,同樣可以使用WindowManager來管理View。

2.Dialog

相較於Toast,Dialog提示了一些資訊讓使用者可以自主選擇,允許使用者與之互動,接收使用者的輸入資訊,而且還可以通過內部介面來設定彈窗能否被取消。Dialog 類是對話方塊的基類,但應該避免直接例項化 Dialog,而應使用其子類AlertDialog。

Dialog原始碼解析

首先來分析下建構函式:

/**
 * Creates a dialog window that uses the default dialog theme.
 * <p>
 * The supplied {@code context} is used to obtain the window manager and
 * base theme used to present the dialog.
 *
 * @param context the context in which the dialog should run
 * @see android.R.styleable#Theme_dialogTheme
 */
public Dialog(@NonNull Context context) {
    this(context, 0, true);
}

/**
 * Creates a dialog window that uses a custom dialog style.
 * <p>
 * The supplied {@code context} is used to obtain the window manager and
 * base theme used to present the dialog.
 * <p>
 * The supplied {@code theme} is applied on top of the context's theme. See
 * <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">
 * Style and Theme Resources</a> for more information about defining and
 * using styles.
 *
 * @param context the context in which the dialog should run
 * @param themeResId a style resource describing the theme to use for the
 *              window, or {@code 0} to use the default dialog theme
 */
public Dialog(@NonNull Context context, @StyleRes int themeResId) {
    this(context, themeResId, true);
}

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    if (createContextThemeWrapper) {
        if (themeResId == ResourceId.ID_NULL) {
            final TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
            themeResId = outValue.resourceId;
        }
        mContext = new ContextThemeWrapper(context, themeResId);
    } 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.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);

    mListenersHandler = new ListenersHandler(this);
}

protected Dialog(@NonNull Context context, boolean cancelable,
        @Nullable OnCancelListener cancelListener) {
    this(context);
    mCancelable = cancelable;
    updateWindowForCancelable();
    setOnCancelListener(cancelListener);
}

建構函式中,首先指定了dialog中的主題styles,初始化mWindowManager和mWindow 為PhoneWindow。

顯示Dialog直接通過show方法:

/**
 * Start the dialog and display it on screen.  The window is placed in the
 * application layer and opaque.  Note that you should not override this
 * method to do initialization when the dialog is shown, instead implement
 * that in {@link #onStart}.
 */
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);
    } else {
        // Fill the DecorView in on any configuration changes that
        // may have occured while it was removed from the WindowManager.
        final Configuration config = mContext.getResources().getConfiguration();
        mWindow.getDecorView().dispatchConfigurationChanged(config);
    }

    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;
    }

    mWindowManager.addView(mDecor, l);
    mShowing = true;

    sendShowMessage();
}

方法中會執行dispatchOnCreate方法,這裡面後來呼叫的是dialog的onCreate方法。

// internal method to make sure mCreated is set properly without requiring
// users to call through to super in onCreate
void dispatchOnCreate(Bundle savedInstanceState) {
    if (!mCreated) {
        onCreate(savedInstanceState);
        mCreated = true;
    }
}

/**
 * Similar to {@link Activity#onCreate}, you should initialize your dialog
 * in this method, including calling {@link #setContentView}.
 * @param savedInstanceState If this dialog is being reinitialized after a
 *     the hosting activity was previously shut down, holds the result from
 *     the most recent call to {@link #onSaveInstanceState}, or null if this
 *     is the first time.
 */
protected void onCreate(Bundle savedInstanceState) {
}

使用者可以重寫onCreate方法初始化dialog,然後呼叫setContentView方法,就像Activity#onCreate方法中一樣。

回到show()方法中,執行完OnCreate方法後,繼續執行onStart()。然後通過mWindow.getDecorView() 初始化mDecor。之後將mDecor 新增到mWindowManager中來,標誌mShowing為true。

如此就完成了Dialog的初始化到顯示到Window中。從上面可知,Dialog的顯示邏輯和Activity中載入佈局很相似,通過onCreate方法載入使用者dialog的佈局,然後佈局新增到了mDecor中,之後又將mDecor載入到PhoneWindow中來。於是dialog就顯示出來了。

Dialog中提供hide和dismiss方法來控制Dialog的消失,這兩個方法區別就是hide只會讓dialog顯示不可見但是window上的View還存在,而dismiss則直接將view從window上移除。

/**
 * Hide the dialog, but do not dismiss it.
 */
public void hide() {
    if (mDecor != null) {
        mDecor.setVisibility(View.GONE);
    }
}

/**
 * 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);
    }
}

void dismissDialog() {
    if (mDecor == null || !mShowing) {
        return;
    }

    if (mWindow.isDestroyed()) {
        Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
        return;
    }

    try {
        mWindowManager.removeViewImmediate(mDecor);
    } finally {
        if (mActionMode != null) {
            mActionMode.finish();
        }
        mDecor = null;
        mWindow.closeAllPanels();
        onStop();
        mShowing = false;

        sendDismissMessage();
    }
}
3.PopupWindow

相較於Dialog,PopupWindow又有它的不同。在應用場景中,PopupWindow有著更加靈活的控制,可以實現基於任何View的相對位置實現,定位更加準確,寬高和邊界都比較清晰。

兩者比較本質的區別就是:

Dialog是非阻塞式對話方塊:Dialog彈出時,後臺還可以繼續做其他事情;
PopupWindow是阻塞式對話方塊:PopupWindow彈出時,程式會等待,在PopupWindow退出前,程式一直等待,只有當我們呼叫了dismiss方法的後,PopupWindow退出,程式才會向下執行。
注意這裡的阻塞,並不是我們通常理解的阻塞某個執行緒讓當前執行緒wait,而是指它完全取得了使用者操作的響應處理許可權,從而使其它UI控制元件不被觸發。

PopupWindow原始碼解析

首先分析建構函式:

/**
 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
 *
 * <p>The popup does provide a background.</p>
 */
public PopupWindow(Context context) {
    this(context, null);
}

/**
 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
 *
 * <p>The popup does provide a background.</p>
 */
public PopupWindow(Context context, AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.popupWindowStyle);
}

/**
 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
 *
 * <p>The popup does provide a background.</p>
 */
public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

/**
 * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p>
 *
 * <p>The popup does not provide a background.</p>
 */
public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    mContext = context;
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    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();
    }

    a.recycle();

    setEnterTransition(enterTransition);
    setExitTransition(exitTransition);
    setBackgroundDrawable(bg);
}

/**
 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
 *
 * <p>The popup does not provide any background. This should be handled
 * by the content view.</p>
 */
public PopupWindow() {
    this(null, 0, 0);
}

/**
 * <p>Create a new non focusable popup window which can display the
 * <tt>contentView</tt>. The dimension of the window are (0,0).</p>
 *
 * <p>The popup does not provide any background. This should be handled
 * by the content view.</p>
 *
 * @param contentView the popup's content
 */
public PopupWindow(View contentView) {
    this(contentView, 0, 0);
}

....

/**
 * <p>Create a new popup window which can display the <tt>contentView</tt>.
 * The dimension of the window must be passed to this constructor.</p>
 *
 * <p>The popup does not provide any background. This should be handled
 * by the content view.</p>
 *
 * @param contentView the popup's content
 * @param width the popup's width
 * @param height the popup's height
 * @param focusable true if the popup can be focused, false otherwise
 */
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);
}

這裡有兩個不同的建構函式,相同的是都初始化了mWindowManager。然後當傳入了context時,就會設定了進入退出的動畫和背景色。如果傳入的是contentView,則會執行setContentView,設定寬高,預設不設定獲取焦點。
我們一般通過setContentView來載入PopupWindow要顯示的內容:

/**
 * <p>Change the popup's content. The content is represented by an instance
 * of {@link android.view.View}.</p>
 *
 * <p>This method has no effect if called when the popup is showing.</p>
 *
 * @param contentView the new content for the popup
 *
 * @see #getContentView()
 * @see #isShowing()
 */
public void setContentView(View contentView) {
    if (isShowing()) {
        return;
    }

    mContentView = contentView;

    if (mContext == null && mContentView != null) {
        mContext = mContentView.getContext();
    }

    if (mWindowManager == null && mContentView != null) {
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    // Setting the default for attachedInDecor based on SDK version here
    // instead of in the constructor since we might not have the context
    // object in the constructor. We only want to set default here if the
    // app hasn't already set the attachedInDecor.
    if (mContext != null && !mAttachedInDecorSet) {
        // Attach popup window in decor frame of parent window by default for
        // {@link Build.VERSION_CODES.LOLLIPOP_MR1} or greater. Keep current
        // behavior of not attaching to decor frame for older SDKs.
        setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion
                >= Build.VERSION_CODES.LOLLIPOP_MR1);
    }

}

這個方法中主要是賦值給mContentView,mContext和mWindowManager 屬性。
然後PopupWindow通過showAsDropDown 來顯示:

public void showAsDropDown(View anchor) {
    showAsDropDown(anchor, 0, 0);
}

public void showAsDropDown(View anchor, int xoff, int yoff) {
    showAsDropDown(anchor, xoff, yoff, DEFAULT_ANCHORED_GRAVITY);
}

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
    if (isShowing() || !hasContentView()) {
        return;
    }

    TransitionManager.endTransitions(mDecorView);

    attachToAnchor(anchor, xoff, yoff, gravity);

    mIsShowing = true;
    mIsDropdown = true;

    final WindowManager.LayoutParams p =
            createPopupLayoutParams(anchor.getApplicationWindowToken());
    preparePopup(p);

    final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
            p.width, p.height, gravity, mAllowScrollingAnchorParent);
    updateAboveAnchor(aboveAnchor);
    p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;

    invokePopup(p);
}

popup window 中的內容錨定在另一個View的邊角。window位於指定的 gravity 和 指定的x,y的座標偏移。分別看裡面幾個方法,先執行了attachToAnchor方法:

/** @hide */
protected final void attachToAnchor(View anchor, int xoff, int yoff, int gravity) {
    detachFromAnchor();

    final ViewTreeObserver vto = anchor.getViewTreeObserver();
    if (vto != null) {
        vto.addOnScrollChangedListener(mOnScrollChangedListener);
    }
    anchor.addOnAttachStateChangeListener(mOnAnchorDetachedListener);

    final View anchorRoot = anchor.getRootView();
    anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
    anchorRoot.addOnLayoutChangeListener(mOnLayoutChangeListener);

    mAnchor = new WeakReference<>(anchor);
    mAnchorRoot = new WeakReference<>(anchorRoot);
    mIsAnchorRootAttached = anchorRoot.isAttachedToWindow();
    mParentRootView = mAnchorRoot;

    mAnchorXoff = xoff;
    mAnchorYoff = yoff;
    mAnchoredGravity = gravity;
}

初始化變數之後,繼續執行preparePopup 方法:

/**
 * Prepare the popup by embedding it into a new ViewGroup if the background
 * drawable is not null. If embedding is required, the layout parameters'
 * height is modified to take into account the background's padding.
 *
 * @param p the layout parameters of the popup's content view
 */
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.
    p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);

    mPopupViewInitialLayoutDirectionInherited =
            (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
}

這裡判斷mBackground 不為空,然後為mContentView建立一個父容器佈局,在給該佈局設定背景色。然後裡面再執行了createDecorView 方法來 獲取PopupDecorView。

/**
 * Wraps a content view in a FrameLayout.
 *
 * @param contentView the content view to wrap
 * @return a FrameLayout that wraps the content view
 */
private PopupDecorView createDecorView(View contentView) {
    final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
    final int height;
    if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
        height = WRAP_CONTENT;
    } else {
        height = MATCH_PARENT;
    }

    final PopupDecorView decorView = new PopupDecorView(mContext);
    decorView.addView(contentView, MATCH_PARENT, height);
    decorView.setClipChildren(false);
    decorView.setClipToPadding(false);

    return decorView;
}

執行了preparePopup之後,完成了背景色設定和mDecorView賦值。接著就執行了invokePopup:

/**
 * <p>Invoke the popup window by adding the content view to the window
 * manager.</p>
 *
 * <p>The content view must be non-null when this method is invoked.</p>
 *
 * @param p the layout parameters of the popup's content view
 */
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);
    }
}

這裡才開始將mDecorView加入到 WindowManager 中,於是popupWindow 顯示了出來。
到這裡才完成了PopupWindow 呼叫。
同樣我們也可以使用showAtLocation方法來控制PopupWindow的呼叫。裡面也是同樣呼叫了核心方法preparePopup 和 invokePopup,這裡就不繼續分析了。