Android开发笔记(二) 关于弹窗

Android中提供几种不同的弹窗模式,Toast,Dialog,PopupWindow 每种弹窗又对应了不同的应用场景,我们可以根据不同业务场景来选择。下面将会分别介绍上面四种不同弹窗的应用,同时也对每中弹窗的源码和所遇到的问题进行分别分析。

1.Toast

Toast是Android中最轻量级的视图,该视图已浮于应用程序之上的形式呈现给用户。它并不获得焦点,即使用户正在输入什么也不会受到影响,不会与用户交互。旨在尽可能以不显眼的方式,让用户看到提示的信息。而且显示的时间是有限制的,过一段时间后会自动消失,Toast本身可以控制显示时间的长短。

通用的Toast:UniversalToast

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。

通用的Toast:UniversalToast

2.Dialog

相较于Toast,Dialog提示了一些信息让用户可以自主选择,允许用户与之交互,接收用户的输入信息,而且还可以通过内部接口来设置弹窗能否被取消。Dialog 类是对话框的基类,但应该避免直接实例化 Dialog,而应使用其子类AlertDialog。

一个多功能Dialog:dialogplus

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

一个多功能Dialog:dialogplus

3.PopupWindow

相较于Dialog,PopupWindow又有它的不同。在应用场景中,PopupWindow有着更加灵活的控制,可以实现基于任何View的相对位置实现,定位更加准确,宽高和边界都比较清晰。

两者比较本质的区别就是:

Dialog是非阻塞式对话框:Dialog弹出时,后台还可以继续做其他事情;
PopupWindow是阻塞式对话框:PopupWindow弹出时,程序会等待,在PopupWindow退出前,程序一直等待,只有当我们调用了dismiss方法的后,PopupWindow退出,程序才会向下执行。
注意这里的阻塞,并不是我们通常理解的阻塞某个线程让当前线程wait,而是指它完全取得了用户操作的响应处理权限,从而使其它UI控件不被触发。

一个多功能PopupWindow:superPopupWindow

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,这里就不继续分析了。

一个多功能PopupWindow:superPopupWindow

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值