AlertDialog实现原理

对于一个页面的窗口展示,很多时候都可以理解为是一个activity,这样理解没错,但更确切的说window才是负责页面展示的,那为什么可以理解为一个页面就是一个activity呢?这是由于activity持有window对象,activity负责加载布局并将布局添加到window对象中。这里需要想个问题,dialog的显示并不由activity来控制,所以这里可以推测dialog的显示应该是由window来控制的,那到底是不是呢?那就得去源码中看看了。

先来个简单的使用:

        mTextMessage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                builder.setTitle("titile");
                builder.setMessage("message");
                AlertDialog dialog = builder.create();
                dialog.show();
            }
        });

使用起来还是挺简单的,就从这段简单的代码开始入手了,先来看下AlertDialog.Builder的构造函数:

        public Builder(@NonNull Context context) {
            this(context, resolveDialogTheme(context, 0));
        }

        public Builder(@NonNull Context context, @StyleRes int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
            mTheme = themeResId;
        }

初始化了一个AlertController.AlertParams类型的P对象,可以理解为是一个存放设置dialog属性的容器,再来看下AlertController.AlertParams的构造函数:

        public AlertParams(Context context) {
            mContext = context;
            mCancelable = true;
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

其实就是初始化了一些成员变量。

到这AlertDialog.Builder的初始化就算完了,还是比较简单的,接下来看dialog的参数是怎么设置的:

        builder.setTitle("titile")
        builder.setMessage("message")

这是设置dialog的显示信息,当然还有好多属性可以设置,不过都是类似的,所以这里就看这两个属性的实现:

        public Builder setTitle(@Nullable CharSequence title) {
            P.mTitle = title;
            return this;
        }
        public Builder setMessage(@Nullable CharSequence message) {
            P.mMessage = message;
            return this;
        }

可以看到,这些设置进来的属性都赋值给了P对象,到目前为止,已经设置了显示dialog的消息,接下来执行的是:

        AlertDialog dialog = builder.create();

看来得去看看create()方法中做了些什么了:

        public AlertDialog create() {
            // We can't use Dialog's 3-arg constructor with the createThemeContextWrapper param,
            // so we always have to re-set the theme
            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

先创建了AlertDialog对象,那就去看下AlertDialog的构造方法了:

    protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, resolveDialogTheme(context, themeResId));
        mAlert = new AlertController(getContext(), this, getWindow());
    }

这里创建了一个AlertController类型的mAlert对象,create()方法中有用到,AlertDialog继承自Dialog:

    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);
        // 设置window显示的位置,默认是居中显示
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

这里主要是创建了一个Window对象(Window的唯一实现是PhoneWindow),然后设置了一些回调和Window显示的位置,所以Dialog是持有Window对象的。在回到create()方法中:

        public AlertDialog create() {
            // We can't use Dialog's 3-arg constructor with the createThemeContextWrapper param,
            // so we always have to re-set the theme
            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

创建AlertDialog对象后,接着调用的是P.apply(dialog.mAlert),P对象是在构建Builder的时候创建的,其类型是AlertController.AlertParams,mAlert对象在AlertDialog构造函数初始化的,那就来看下apply()函数做了些什么:

        public void apply(AlertController dialog) {
            if (mCustomTitleView != null) {
                dialog.setCustomTitle(mCustomTitleView);
            } else {
                if (mTitle != null) {
                    dialog.setTitle(mTitle);
                }
                if (mIcon != null) {
                    dialog.setIcon(mIcon);
                }
                if (mIconId != 0) {
                    dialog.setIcon(mIconId);
                }
                if (mIconAttrId != 0) {
                    dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
                }
            }
            if (mMessage != null) {
                dialog.setMessage(mMessage);
            }
            if (mPositiveButtonText != null || mPositiveButtonIcon != null) {
                dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                        mPositiveButtonListener, null, mPositiveButtonIcon);
            }
            if (mNegativeButtonText != null || mNegativeButtonIcon != null) {
                dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                        mNegativeButtonListener, null, mNegativeButtonIcon);
            }
            if (mNeutralButtonText != null || mNeutralButtonIcon != null) {
                dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
                        mNeutralButtonListener, null, mNeutralButtonIcon);
            }
            // For a list, the client can either supply an array of items or an
            // adapter or a cursor
            if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
                createListView(dialog);
            }
            if (mView != null) {
                if (mViewSpacingSpecified) {
                    dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
                            mViewSpacingBottom);
                } else {
                    dialog.setView(mView);
                }
            } else if (mViewLayoutResId != 0) {
                dialog.setView(mViewLayoutResId);
            }

            /*
            dialog.setCancelable(mCancelable);
            dialog.setOnCancelListener(mOnCancelListener);
            if (mOnKeyListener != null) {
                dialog.setOnKeyListener(mOnKeyListener);
            }
            */
        }

前面有说到使用Builder设置参数时,实际是将参数设置到P对象中,所以apply()函数的作用就是将P对象中参数又设置到了mAlert对象中。接着回到create()函数中看剩下的方法,基本是对dialog的回调设置,这里就不跟进去了。

分析到这,dialog的配置就算是全部配置完了,接下去要执行的是:

        dialog.show();

嗯嗯,看来实现就在这个方法中了:

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

先是if(mShowing)判断dialog是否处在显示状态,初次显示这里为false,那这里这样做判断有什么用意呢?在Dialog中有个hide()方法,这个方法是隐藏dialog,实际是没用销毁dialog的,且dialog的mShowing还是为true的,这就是这里所作判断的用意,继续往下看,接下来是if(!mCreated),默认是false,所以这里要执行的是dispatchOnCreate(null):

    void dispatchOnCreate(Bundle savedInstanceState) {
        if (!mCreated) {
            onCreate(savedInstanceState);
            mCreated = true;
        }
    }
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAlert.installContent();
    }

在执行完dispatchOncreate(null)后将mCreated置为true,紧接着执行是onCreate(),这个在Dialog中是空实现,在AlertDialog有实现,就执行了mAlert.installContent(),作用是什么呢?看看就知道了:

    public void installContent() {
        final int contentView = selectContentView();
        mDialog.setContentView(contentView);
        setupView();
    }

这里简单说下这几行代码的作用:

1、selectContentView()是获取显示dialog的布局文件;

2、mDialog.setContentView(),这里是将布局文件设置到window对象中去,还记得activity是如何设置view的么?没错,用的就是setContentView()这个方法,这里和activity中使用是一个意思,其实质都是将布局文件设置到window对象中,之后就可显示了。

3、将布局文件设置到window对象中,会生成view对象,但是这些view对象还没有设置内容和一些事件监听,这也就是setupView()要做的事;

做完这些后就可以回到show()方法中接着往下走了,mWindowManager.adddView()这个方法,这个方法实际起到什么用呢?那就的好好说下了,在这方法中会创建一个ViewRootImpl对象,然后将view对象添加到ViewRootImpl对象中去,ViewRootImpl中会通知下一帧屏幕信号到来时执行view的测量、布局、绘制,这样dialog就显示到界面上了。

最后在说下sendShowMessage()方法,当设置了显示dialog时的回调监听时,这里实际的作用就是去通知这个回调。

接下来看下Dialog的一些不常见设置,也可以说是一些高级点的设置,上面说到dialog实际也是有window来显示,那么就可以对window进行一些设置,已达到想要的效果,这里先说几点需求:

1、指定dialog的宽高;

2、控制dialog的透明度;

3、调整dialog外部的灰度;

4、指定dialog的显示位置;

5、设置dialog的出现和消失时的动画;

如果你对window对象不太明白,那这个实现起来那就会有点头大了,但是当你明白window对象后,你就会发现这实在是太简单了,因为只需要设置一些window的属性就可以了,来看看:

        mTextMessage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                builder.setTitle("titile");
                builder.setMessage("message");
                AlertDialog dialog = builder.create();
                dialog.show();
                Window window = dialog.getWindow();
                WindowManager.LayoutParams params = window.getAttributes();
                params.width = 200;
                params.height = 100;
                params.alpha = 1f;
                params.dimAmount = 0.5f;
                params.gravity  = Gravity.BOTTOM;
                params.windowAnimations = R.style.BaseDialog_Bottom_Anim;
                window.setAttributes(params);
                dialog.show();
            }
        });

这里稍微提下这几个属性,还有其他的一些属性,自己可以去测试下,代码还是比较简单的:

params.width = 200;params.height = 100;  设置dialog显示的宽高;

params.alpha = 1f;params.dimAmount = 0.5;  设置弹框的透明度和弹框外部的灰度;

params.gravity = Gravity.BOTTOM;  设置dialog显示的位置在底部,如果想指定在具体位置,可以使用params.x;

params.windowAnimations = R.style.BaseDialog_Botton_Anim;  设置dialog显示隐藏动画;

R.style.BaseDialog_Botton_Anim的配置如下:

    <style name="BaseDialog_Bottom_Anim">
        <item name="android:windowEnterAnimation">@anim/netlib_bottom_enter_anim</item>
        <item name="android:windowExitAnimation">@anim/netlib_bottom_exit_anim</item>
    </style>

netlib_bottom_enter_anim:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300">
    <translate
        android:fromYDelta="100%p"
        android:toYDelta="0%p" />

    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
</set>

netlib_bottom_exit_anim:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300">
    <translate
        android:fromYDelta="0%p"
        android:toYDelta="100%p" />

    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="0.3" />
</set>

到这就算说完了,有不明白的欢迎提问一起探讨!

 

 

 

 

猜你喜欢

转载自blog.csdn.net/tangedegushi/article/details/82884931