欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android对话框Dialog,PopupWindow,Toast的实现机制

程序员文章站 2022-05-31 15:58:01
...
来自:http://blog.csdn.net/feiduclear_up/article/details/49080587



1.前言

Android系统中窗口(Window)分三种类型:应用窗口,子窗口,系统窗口。上一篇博客分析了Android应用窗口Window的创建过程,接下来这篇博客来学习其他两种窗口类型的实现机制。Android开发中经常会使用到Dialog,PopupWindow,Toast等对话框来作为提示信息或者和用户交互。然而这些对话框其实都是窗口,它们的创建和移除机制也就是Android系统对窗口的添加和删除的过程了。 
这篇博客从源码角度来分析Dialog,PopupWindow,Toast的实现原理。

2.Dialog实现的机制

在Android系统中Dialog对话框是子窗口,也就是Dialog对话框窗口必须要有一个父窗口,那么Dialog对话框窗口的父窗口是谁呢?我不说相信大家也知道了吧!没错就是Activity应用窗口,为什么呢?这篇博客来为你解答!

相信很多人平日里用的最多的对话框还是AlertDialog,不过今天它可不是主角,Dialog才是我们今天的重点。其实AlertDialog只是Google官方定制了很多不同主题不同布局的Dialog而已,AlertDialog继承自Dialog类。因此我们只分析Dialog的实现机制。使用对话框都是在Activity中,因此在Activity中创建最简单的Dialog对话框代码如下:

 Dialog dialog = new Dialog(MainActivity.this);
 dialog.setContentView(R.layout.dialog);
 dialog.show();

//取消对话框
dialog.cancel();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

以上是最简单的对话框使用示例,先创建一个Dialog对象实例,然后Dialog加载布局,最后调用show方法来显示该对话框,当用户按“back”键时系统会自动调用cancel方法来移除Dialog对话框窗口。现在我们就就从以上几个过程来详细分析Dialog创建过程。

2.1Dialog对话框创建

来看看Dialog类的构造方法实现代码如下:

public class Dialog implements DialogInterface, Window.Callback,
        KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
    .................
    //使用默认主题的构造方法
    public Dialog(Context context) {
        this(context, 0, true);
    }
    //指定主题的构造方法
    Dialog(Context context, int theme, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (theme == 0) {
                TypedValue outValue = new TypedValue();
                //使用默认的对话框主题
                context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
                        outValue, true);
                theme = outValue.resourceId;
            }
            //创建属于该对话框的Context
            mContext = new ContextThemeWrapper(context, theme);
        } else {
            mContext = context;
        }
        //获得Activity的窗口管理服务
        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        //创建对话框的窗口
        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        //设置窗口回调监听
        w.setCallback(this);
        //设置窗口消失回调监听事件
        w.setOnWindowDismissedCallback(this);
        //给窗口设置窗口管理器
        w.setWindowManager(mWindowManager, null, null);
        //设置当前对话框窗口的位置
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

分析: 
在Dilaog的构造方法中主要做了如下工作:

  • 根据参数createContextThemeWrapper的值来决定是使用参数theme指定的主题还是使用其父窗口Activity的主题。
  • 调用Context#getSystemService方法获得当前应用的窗口管理器WindowManager对象,有上一篇博客知道:一个应用不管有多少个Activity都只有一个WindowManager对象用于管理当前应用中的所有窗口。
  • 为Dialog对话框创建一个窗口Window对象,Window是个抽象类,其实现指向PhoneWindow类。
  • 给窗口设置事件回调监听,因为在Dialog类中实现了Window#Callback接口类,该接口类目的是让Dialog对话框的窗口具有处理响应按键触摸事件的能力,这也就是为什么用户默认创建的Dialog对话框可以响应“Back”回退按键事件和点击对话框窗口以外的地方Dialog对话框会自动消失隐藏。由此可知,Dialog和Activity都实现了消息处理。
  • 设置Window类的内部成员变量值WindowManager,由此知道Window的WindowManager和Dilaog的WindowManager指向同一个对象。
  • 设置当前Dialog窗口的对齐方式为居中,这就是为什么我们默认的对话框都是居中显示了吧。
  • 创建对话框的事件监听对象,用于对话框显示,消失,取消时的一些监听操作。

Dialog内部创建了一个Window对象,窗口是一个抽象的东西,和Activity应用窗口一样,需要往窗口Window中添加视图View来显示内容。因此调用setContentView方法来加载对话框的布局视图。

2.2Dialog加载布局

Dialog#setContentView源码如下:

public void setContentView(int layoutResID) {
        mWindow.setContentView(layoutResID);
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

分析: 
该方法将操作转发给Window类中的setContentView方法,然而mWindow对象是指向PhoneWindow类的,也就是调用PhoneWindow类中的setContentView方法。到此处我们发现Dialog加载布局的流程和Activity加载布局的流程是一样的。因此这里就不仔细分析了,可以参考上一篇博客。到此,Dialog对话框窗口Window内部就已经添加了视图DecorView了。那么剩下的事就是Dilaog对话框怎么显示在手机屏幕上了。

2.3 Dialog的显示

在创建完Dialog对话框之后我们仅仅调用Dialog#show方法就可以让该对话框显示在当前Activity上。

Dilaog#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;
        }
        //设置dialog是否已经取消标志
        mCanceled = false;

        if (!mCreated) {
            dispatchOnCreate(null);
        }
        //是个空方法,可以在创建Dialog时重写该方法
        onStart();
        //得到Dialog对话框窗口的顶层视图DecorView
        mDecor = mWindow.getDecorView();
        //设置窗口actionbar
        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;
        }

        try {
            //重点 添加对话框窗口的顶层视图到Activity上
            mWindowManager.addView(mDecor, l);
            //重置对话框状态
            mShowing = true;
            //异步消息处理机制来处理Dialog对话框显示时候的一个回调监听
            sendShowMessage();
        } finally {
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

分析: 
在show方法里主要做了如下几件工作:

  • 判断当前Dialog对话框窗口是存在,如果存在直接让其显示即可;如果当前窗口不存在,则调用Dialog的回调方法onCreate方法,用户可以在onCreate回调方法中创建一个新的Dialog对话框。
  • 获得Dialog对话框的顶层视图DecorView对象赋值给成员变量mDecor用于addView方法的参数。
  • 根据条件为当前对话框窗口设置导航栏logo图标等。
  • 获得当前窗口的参数属性赋值给l,用于addView方法的参数。
  • 调用WindowManager#addView方法添加Dialog对话框窗口。

自此Dialog对话框的添加过程已经完成了,回过头来会发现,其实Dialog对话框窗口的创建添加过程和Activity应用窗口过程是一样一样的。

2.4 移除Dialog对话框

移除或者隐藏对话框的代码也很简单。用户仅仅调用Dialog#cancel方法就可以移除当前Activity之上的对话框了。

public void cancel() {
        if (!mCanceled && mCancelMessage != null) {
            mCanceled = true;
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mCancelMessage).sendToTarget();
        }
        dismiss();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

该方法也很简单,先发送移除Dialog时的监听事件,之后将操作转发到dismiss方法中。

 /**
     * 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);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

分析: 
注释解释的很清楚了:该方法可以安全的在任何线程中调用,也就是说可以在子线程中移除对话框而不报错。Looper.myLooper()方法获得的Looper对象是当前线程的Looper,而mHandler.getLooper()方法获得的Looper对象是mHandler所在线程的Looper。由于Android系统规定只要有关UI操作都必须在主线程中,而我们在创建Dialog是在主线程中,mHandler对象是在主线程中创建的,因此mHandler.getLooper()就是主线程的Looper。

以上代码:如果当前线程为主线程,则调用dismissDialog方法,如果是子线程,则利用Handler将此操作发送到UI线程中操作。

1.在主线程中移除对话框

void dismissDialog() {
        //如果对话框的顶层视图不存在或者dialog没有正在显示则不做任何处理
        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();
            //空方法,可以在创建dialog的时候重写该方法
            onStop();
            //重置标志位
            mShowing = false;
            //处理对话框移除的监听事件
            sendDismissMessage();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

分析:

  • 如果当前Dialog窗口的视图DecorView为空或者当前窗口不存在,则不做任何处理,直接退出当前方法即可。
  • 如果当前Dialog窗口已经被销毁了也不做任何处理。
  • 调用WindowManager#removeView方法来移除当前对话框窗口。

该方法主要作用就是从Activity的窗口管理器mWindowManager中移除对话框窗口的视图,也就是完成了该对话框的移除操作。

2.在子线程中调用Dialog#cancel

当子线程调用时就会执行 mHandler.post(mDismissAction)代码。该代码的作用就是将操作转发到主线程中。我们看看mDismissAction的实现如下:

private final Runnable mDismissAction = new Runnable() {
        public void run() {
            dismissDialog();
        }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

该类很简单,仅仅实现了run回调方法,然后调用了dismissDialog方法。

2.5 Dialog 触摸事件处理

我们知道Dialog默认是响应“Back”返回键当前对话框消失事件以及点击Dialog对话框视图以外的地方当前对话框也会消失,而默认的PopupWindow对话框是不支持以上两种事件操作的。那么为什么会是这样呢?此处先分析Dialog对触摸事件的处理,下一节分PopupWindow不支持事件处理的原因。

响应“Back”返回键

public class Dialog implements DialogInterface, Window.Callback,
        KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
    ........
           public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                && !event.isCanceled()) {
            onBackPressed();
            return true;
        }
        return false;
    }
    ........
    public void onBackPressed() {
        if (mCancelable) {
            cancel();
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

分析: 
在Dialog类中实现了按键事件KeyEvent.Callback接口类,因此当有用户按键输入事件发生时就会调用KeyEvent.Callback接口类中的相应方法。当按键操作有“抬起”的操作行为时,系统会调用onKeyUp方法。而Dialog类中的onKeyUp方法中会检查当前按键事件是否为“KeyEvent.KEYCODE_BACK”事件,且当前输入事件没有被取消,那么会调用onBackPressed,而该方法中判断如果当前对话框可以被取消则调用cancel方法来取消或者隐藏当前对话框。因此Dialog也就响应了“Back”按键事件之后对话框消失。

Dialog点击对话框视图以外的地方消失

public class Dialog implements DialogInterface, Window.Callback,
        KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
    ........
     public boolean dispatchTouchEvent(MotionEvent ev) {
         //响应窗口的触摸事件分发
        if (mWindow.superDispatchTouchEvent(ev)) {
            return true;
        }
        //响应Dialog的触摸事件
        return onTouchEvent(ev);
    }
    ........
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

分析: 
Dialog类同样也实现了Window.Callback接口事件,同时调用Window#setCallback方法设置了该事件的回调,因此Dialog也同样具有响应触摸事件的功能。当用户点击手机屏幕时,就系统就会自动调用dispatchTouchEvent方法来分发当前窗口的触摸事件。该方法先后做了两件事情:

  1. 先调用Dialog的窗口Window对象的方法Window#superDispatchTouchEvent来处理触摸按键事件。
  2. 如果Window窗口的触摸按键事件处理返回为false,则调用Dialog#onTouchEvent方法来继续处理触摸按键事件。

有关触摸事件传递机制请参考这篇博客:Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

当用户点击Dialog窗口视图以外的地方时,最后时会执行Dialog#onTouchEvent方法的,感兴趣的同学可以自行研究下!那么我们来看看Dialog#onTouchEvent方法源码如下:

 public boolean onTouchEvent(MotionEvent event) {
        if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
            cancel();
            return true;
        }

        return false;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

分析: 
该方法也很简单,如果if条件满足,则直接调用cancel方法来取消当前对话框,if条件不满足时不做任何处理直接返回。那么我们来看看什么情况下if添加满足导致了调用cancel方法取消对话框。必须满足三个条件:当前对话框可以被取消,对话框正在显示,以及Window.shouldCloseOnTouch方法返回true。前两个条件默认都满足,那么来看看第三个条件什么情况下满足吧!

Window.shouldCloseOnTouch源码如下:

/** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                && isOutOfBounds(context, event) && peekDecorView() != null) {
            return true;
        }
        return false;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

分析: 
该方法需要满足四个条件才会返回true。

  • 布尔变量mCloseOnTouchOutside:表示是否支持点击窗口以外的地方窗口可消失。Dialog对话框的窗口默认支持,也就是该条件满足。如果想修改该条件,你可以调用Dialog#setCanceledOnTouchOutside(false)方法来达到点击窗口以外的地方Dialog消失,其实最终是设置mCloseOnTouchOutside变量为false,然后导致shouldCloseOnTouch方法返回false。
  • 当前触摸事件是否为“MotionEvent.ACTION_DOWN”手指按下事件,自然满足。
  • 调用isOutOfBounds方法判断当前手指点击的坐标是否在Dialog对话框窗口视图之外?
  • 当前Dialog对话框窗口是否添加了视图DecorView?如果对话框显示出来了,自然窗口DecorView对象不为空。

因此有上面四个条件分析我们得知:只有当isOutOfBounds方法返回true时,条件才成立,shouldCloseOnTouch方法返回值才为true,手指点击Dialog窗口之外的地方Dialog才会消失。所以主要看isOutOfBounds方法的实现了。

Window#isOutOfBounds源码如下:

 private boolean isOutOfBounds(Context context, MotionEvent event) {
        final int x = (int) event.getX();
        final int y = (int) event.getY();
        final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
        final View decorView = getDecorView();
        return (x < -slop) || (y < -slop)
                || (x > (decorView.getWidth()+slop))
                || (y > (decorView.getHeight()+slop));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

此方法实现也很简单,判断当前手指按下点击屏幕的坐标x,y是否在Window窗口的视图DecorView宽度高度之外,如果是,则返回true,否则返回false。

至此:有关Dialog响应“Back”返回按键事件和点击Dialog窗口之外的地方Dialog自动消失事件分析完成了。其实这一块的原理和Activity处理“Back”返回键当前Activity会调用finish方法一样。

Dialog总结: 
Dialog对话框窗口Window的实现机制和Activity一样。Dialog有一个Window对象,该对象属于PhoneWindow类型用于描述Dialog对话框窗口;PhoneWindow类有一个内部类DecorView,用于描述当前窗口的顶层视图。同样Dialog也实现了Window.Callback接口回调,以便Dialog也可以处理用户的触摸和按键事件。

Dialog窗口Window视图View层次关系图如下:

Android对话框Dialog,PopupWindow,Toast的实现机制

3 PopupWindow弹出式对话框加载过程

开发中用的最多的对话框AlertDialog,如果需要定制自己的对话框风格或者AlertDialog无法满足你的需求时,就可以考虑下PopupWindow对话框了。弹出式对话框PopupWinsow的使用也很简单,仅仅调用已下几行代码就能实现最简单的对话框了!

//获得父窗口视图中的某个View对象
View parentView =  findViewById(R.id.main);
//加载popupWindow对话框布局
View popWindow = LayoutInflater.from(MainActivity.this).inflat(R.layout.dialog, null);
//创建对话框
PopupWindow pw = new PopupWindow(popWindow,ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT, true);
//显示对话框
pw.showAtLocation(parentView, Gravity.CENTER, 0, 0);

//移除对话框
pw.dismiss();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

分析: 
使用PopupWindow弹出式对话框主要以下几个步骤: 
1. 获得父窗口中的某一个View对象 
2. 加载对话框视图布局文件 
3. 创建对话框实例 
4. 显示该对话框 
5. 移除对话框

我们从PopupWindow类中的构造方法开始分析

3.1 创建PopupWindow

PopupWindow构造方法源码如下:

        public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            //获得所依赖窗口(父窗口)的context对象
            mContext = contentView.getContext();
            //获得所依赖窗口的WindowManager对象
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //设置对话框的布局
        setContentView(contentView);
        //设置对话框的宽度
        setWidth(width);
        //设置对话框的高度
        setHeight(height);
        //设置对话框是否可获得焦点
        setFocusable(focusable);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

分析: 
PopupWindow构造方法中主要做了如下几个工作:

  • 获得父窗口的context对象,也就是当前Activity的Context对象,然后有context对象获得整个应用的WindowManager对象,从上一篇博客知道:一个应用只有唯一一个WindowManager对象用于管理整个应用的窗口。
  • 设置对话框布局,该操作主要是将对话框视图赋值给PopupWindow类的成员变量mContentView。
  • 分别设置对话框布局的宽度,高度,以及获得 焦点的能力。这三个方法的主要操作还是对PopupWindow类中的成员变量mWidth,mHeight,mFocusable赋值,以便对话框显示的时候使用。

PopupWindow对话框创建完成,接下来看看怎么来显示它。

3.2 PopupWindow对话框显示源码分析

PopupWindow对话框显示的方法有两种:

  1. showAtLocation
  2. showAsDropWown

其实这两种方法实现的原理是相同的,仅仅是显示的位置控制不一样而已,因此这里就分析其中一个方法showAtLocation实现的原理。

PopupWindow#showAtLocation源码如下:

public void showAtLocation(View parent, int gravity, int x, int y) {
        showAtLocation(parent.getWindowToken(), gravity, x, y);
    }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

该方法仅仅将操作转发给同名方法,只是利用第一个参数parent来获得父窗口的标识符token对象,然而,父窗口Window视图中的任何一个View得到的标识符都是同一个对象。因此在构建parent参数的时候只要满足一个条件就可以了:那就是参数parent只要是对话框所依赖的父类窗口中的其中一个子View即可,也就是Activity布局中的任何一个子View都可以作为PopupWindow类中showAtLocation方法的第一个参数。

同名方法showAtLocation源码如下:

public void showAtLocation(IBinder token, int gravity, int x, int y) {
        //对话框正在显示或者对话框布局为空不做任何处理
        if (isShowing() || mContentView == null) {
            return;
        }

        unregisterForScrollChanged();
        //重置标记位
        mIsShowing = true;
        mIsDropdown = false;
        //创建窗口布局参数
        WindowManager.LayoutParams p = createPopupLayout(token);
        //窗口入场动画
        p.windowAnimations = computeAnimationResource();
        //对话框准备工作
        preparePopup(p);
        //对话框显示的相对位置
        if (gravity == Gravity.NO_GRAVITY) {
            gravity = Gravity.TOP | Gravity.START;
        }
        p.gravity = gravity;
        //对话框起点坐标
        p.x = x;
        p.y = y;
        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
        //添加对话框过程
        invokePopup(p);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

以上方法主要做了三件事:

  1. 创建对话框窗口布局参数
  2. 创建对话框窗口的视图
  3. 添加对话框窗口的过程

我们依次来分析以上三步: 
1.创建窗口参数:

PopupWindow#createPopupLayoutParams源码如下:

  private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        //设置窗口默认对齐方式为最左最顶
        p.gravity = Gravity.START | Gravity.TOP;
        //设置窗口特征标记
        p.flags = computeFlags(p.flags);
        //设置窗口类型为面板窗口即子窗口
        p.type = mWindowLayoutType;
        //将父窗口的token标识符赋值给子窗口
        p.token = token;
        //设置软输入法模式
        p.softInputMode = mSoftInputMode;
        //设置窗口入场动画
        p.windowAnimations = computeAnimationResource();
        //设置窗口位图格式
        if (mBackground != null) {
            p.format = mBackground.getOpacity();
        } else {
            p.format = PixelFormat.TRANSLUCENT;
        }
        //设置窗口宽度和高度
        if (mHeightMode < 0) {
            p.height = mLastHeight = mHeightMode;
        } else {
            p.height = mLastHeight = mHeight;
        }

        if (mWidthMode < 0) {
            p.width = mLastWidth = mWidthMode;
        } else {
            p.width = mLastWidth = mWidth;

        }
        return p;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

分析: 
该方法主要是设置对话框的 gravity(对齐方式),flag(窗口特征),type(窗口类型),softInputMode (软输入法模式),windowAnimations(窗口相关动画),width,height等参数。

2.创建对话框视图

API22 PopupWindow#preparePopup源码如下:

 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.");
        }
        //窗口背景不为空
        if (mBackground != null) {
            final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
            int height = ViewGroup.LayoutParams.MATCH_PARENT;
            if (layoutParams != null &&
                    layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                height = ViewGroup.LayoutParams.WRAP_CONTENT;
            }

            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            popupViewContainer.setBackground(mBackground);
            popupViewContainer.addView(mContentView, listParams);

            mPopupView = popupViewContainer;
        } else {//背景窗口为空
            mPopupView = mContentView;
        }
        //设置窗口的阴影宽度
        mPopupView.setElevation(mElevation);
        mPopupViewInitialLayoutDirectionInherited =
                (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
        mPopupWidth = p.width;
        mPopupHeight = p.height;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

分析: 
有以上代码我们发现在创建对话框窗口视图过程中有两种情况

  • 窗口背景mBackground不为空。
  • 窗口背景mBackground为空。

而我们知道在PopupWindow类中成员变量mBackground默认是为空的,只有调用setBackgroundDrawable方法才能修改mBackground成员变量的值,也就是为PopupWindow对话框设置背景。

当窗口背景mBackground不为空时,if条件满足,先创建PopupViewContainer对象,该对象是FrameLayout类型。然后将窗口布局视图mContentView添加到popupViewContainer视图上,也就是PopupViewContainer类作为父类视图来添加窗口的布局视图mContentView。也就是当前对话框窗口视图mContentView外面还包裹着一成PopupViewContainer。那么我们来看看PopupViewContainer做了什么工作。

private class PopupViewContainer extends FrameLayout {
        private static final String TAG = "PopupWindow.PopupViewContainer";

        public PopupViewContainer(Context context) {
            super(context);
        }

        @Override
        protected int[] onCreateDrawableState(int extraSpace) {
            if (mAboveAnchor) {
                // 1 more needed for the above anchor state
                final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
                View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
                return drawableState;
            } else {
                return super.onCreateDrawableState(extraSpace);
            }
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            //处理back按键事件
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                if (getKeyDispatcherState() == null) {
                    return super.dispatchKeyEvent(event);
                }

                if (event.getAction() == KeyEvent.ACTION_DOWN
                        && event.getRepeatCount() == 0) {
                    KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null) {
                        state.startTracking(event, this);
                    }
                    return true;
                } else if (event.getAction() == KeyEvent.ACTION_UP) {
                    KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null && state.isTracking(event) && !event.isCanceled()) {  
                        //当满足添加back按键时该对话框消失
                        dismiss();
                        return true;
                    }
                }
                return super.dispatchKeyEvent(event);
            } else {
                return super.dispatchKeyEvent(event);
            }
        }

        //处理对话框触摸事件
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();
            //此处判断点击对话框以外的地方调用dismiss来隐藏对话框
            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                dismiss();
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();
                return true;
            } else {
                return super.onTouchEvent(event);
            }
        }

        @Override
        public void sendAccessibilityEvent(int eventType) {
            // clinets are interested in the content not the container, make it event source
            if (mContentView != null) {
                mContentView.sendAccessibilityEvent(eventType);
            } else {
                super.sendAccessibilityEvent(eventType);
            }
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86

PopupViewContainer 继承自FrameLayout是一个ViewGroup是图组,然后你会发现其实该类里面并没有实现什么逻辑处理,仅仅是重写了dispatchKeyEvent和dispatchTouch按键和触摸事件分发而已。而你会发现在按键和触摸事件方法里面处理了点击PopupWindow对话框之外的像素位置时,对话框调用了dismiss方法,也就是移除对话框。并且处理了按返回键时对话框移除的事件,同样当用户按back键时也调用了dismiss方法。这就是为什么PopupWindow在默认情况下是不响应back事件和点击对话框之外的地方PopupWindow是不消失的。所以,如果你想要你的PopupWindow类型的对话框能像Dialog一样响应back和点击对话框以外的地方消失,你就可以调用PopupWindow#setBackgroundDrawable方法来实现了。

当用户没有设置窗口背景也就是没有调用PopupWindow#setBackgroundDrawable方法时mBackground为空,那么当前窗口的视图就直接是mContentView了。然而所有View默认的按键和触摸事件是没有处理back事件和点击对话框之外的地方对话框消失的处理的。因此,使用PopupWindow对话框不设置对话框背景时是不响应“back”返回按键和点击窗口之外的地方消失的。

3.3添加对话框窗口过程

PopupWindow#invokePopup源码如下:

private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }
        mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(mPopupView, p);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

分析: 
该方法也很简单,主要是调用了WindowManager#addView方法来添加对话框视图。从而PopupWindow对话框显示在Activity应用窗口之上了。

3.4移除对话框

public void dismiss() {
        //只有当对话框正在显示且对话框视图不为空
        if (isShowing() && mPopupView != null) {
            //重置标志位
            mIsShowing = false;

            unregisterForScrollChanged();

            try {
                从Activity上移除对话框视图
                mWindowManager.removeViewImmediate(mPopupView);
            } finally {
                if (mPopupView != mContentView && mPopupView instanceof ViewGroup) {        
                    //移除其子View
                    ((ViewGroup) mPopupView).removeView(mContentView);
                }
                mPopupView = null;
                //设置对话框移除时的监听事件
                if (mOnDismissListener != null) {
                    mOnDismissListener.onDismiss();
                }
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

分析: 
移除对话框的过程和Dialog移除对话框相识,这里不仔细分析了。

自此PopupWindow对话框的创建,添加,移除的过程已经分析完成了。其主要流程就是获得当前应用程序的WindowManager对象,然后将对话框的视图添加到WindowManager上来显示PopupWindow对话框,调用WindowManager#remove方法移除对话框视图来达到移除当前对话框。所以PopupWindow类型的对话框必须要依附在某一个Activity之上,也就是PopupWindow是一个子窗口。

PopupWindow总结

Dialog对话框和PopupWindow对话框最主要的区别就是Dialog窗口内部拥有一个PhoneWindow对象来处理了输入事件,而PopupWindow窗口内部没有PhoneWindow对象来理输入事件。这也就导致了Dialog能响应“Back”返回键对话框消失和点击对话框之外的地方对话框消失而PopupWindow不能的原因。

PopupWindow对话框窗口视图关系如下:

Android对话框Dialog,PopupWindow,Toast的实现机制

4.Toast显示的机制

Toast也经常使用,而且使用简单,仅仅需要如下一行代码即可实现吐司效果

Toast.makeText(MainActivity.this, "Toast", Toast.LENGTH_SHORT).show();
  • 1
  • 1

其实分两步调用,Toast#makeText,Toast#show。

4.1Toast#makeText

 public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        Toast result = new Toast(context);

        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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

分析:首先调用Toast的构造方法,然后加载Toast布局视图,将布局视图和Toast显示时间参数赋值给Toast类的成员变量mNextView和mDuration。

4.2Toast构造方法

public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Toast构造方法也很简单,new了一个内部类TN,然后给TN类中的成员变量mY和mGravity赋值。那么主要的操作就在内部类TN的构造方法了。

Toast#TN构造方法

private static class TN extends ITransientNotification.Stub {
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

......

TN() {
            // 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;
            //Toast高度
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            //Toast宽度
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            //Toast入场动画
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            //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;
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

TN类的构造方也很简单,仅仅是创建了布局参数mParams并且赋值操作。

4.3Toast#show

public void show() {
        //Toast视图不能为空
        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
        }
    }


    .........

    private static INotificationManager sService;
    //利用AIDL机制实现跨进程通信,此处是客户端获取服务的过程
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

分析:在该方法中主要作用就是调用远程服务NotificationManagerService中的enqueueToast方法将Toast的内部类TN对象入队列的一个过程。进入NotificationManagerService类的enqueueToast方法看看

public class NotificationManagerService extends SystemService {

.............
private final IBinder mService = new INotificationManager.Stub() {


        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }
            //判断Toast是否是系统窗口
            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));

            if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
                if (!isSystemToast) {
                    Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
                    return;
                }
            }

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    //判断当前Toast对象是否在系统Toast队列中,如果在则更新Toast的位置,将当前Toast插入到队列末尾。
                    int index = indexOfToastLocked(pkg, callback);
                    // If it's already in the queue, we update it in place, we don't
                    // move it to the end of the queue.
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        // Limit the number of toasts that any given package except the android
                        // package can enqueue.  Prevents DOS attacks and deals with leaks.             
                        //此处限制一个应用同一时刻Toast队列不能超过50个
                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i<N; i++) {
                                 final ToastRecord r = mToastQueue.get(i);
                                 if (r.pkg.equals(pkg)) {
                                     count++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }
                        //创建Toast记录ToastRecord类的对象
                        record = new ToastRecord(callingPid, pkg, callback, duration);              //将toast添加到Toast队列mToastQueue
                        mToastQueue.add(record);
                        //获得队列末尾位置
                        index = mToastQueue.size() - 1;
                        //保持Toast所在的进程处于**状态
                        keepProcessAliveLocked(callingPid);
                    }
                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
                    // new or just been updated.  Call back and tell it to show itself.
                    // If the callback fails, this will remove it from the list, so don't
                    // assume that it's valid after this.
                    //当你整个系统第一次创建Toast添加到队列中时,index为0添加满足
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }
...........
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

分析: 
1.enqueueToast方法首先判断当前Toast是否是系统所创建的Toast,不过我们一般都是在Activity中创建Toast,因此不是系统Toast。 
2.判断当前Toast对象是否已经存在系统Toast队列中,如果系统Toast队列中有该Toast消息,则更新该Toast消息在队列中的位置,目的是让刚添加的Toast消息更靠前。 
3.我们这里分析Toast不是系统Toast情况,通过一个遍历循环判断当前Toast所在的应用中所有Toast消息是否已经超过最大限制50个。言外之意是一个应用最多只能同时存在50Toast消息未显示。

4.将当前Toast消息添加到Android系统Toast队列中,获得队列的末尾位置index。 
5.该方法的最后判断队列是否只有一个Toast,如果是则调用showNextToastLocked方法来进一步处理,我们第一次使用Toast系统的Toast队列都是只有一个,因此,第一次使用Toast该条件是成立了的。进入showNextToastLocked方法继续分析:

void showNextToastLocked() {
        //获得队列中第一个Toast对象
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                //远程调用Toast#TN#show方法显示Toast
                record.callback.show();
                //处理Toast超时显示
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

分析:循环变量当前系统Toast队列,获得队列的第一个Toast消息然后调用record.callback.show()方法来显示Toast对话框到手机屏幕上。有前面我们知道,record.callback是Toast类中的内部类TN实例。因此这里就是远程调用Toast#TN#show方法来显示Toast对话框。之后再调用scheduleTimeoutLocked方法来处理Toast显示时间。

4.4Toast显示

 private static class TN extends ITransientNotification.Stub {
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

final Handler mHandler = new Handler(); 

.........

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

远程调用又回到了Toast#TN里面了。我们发现show方法里面Toast是以消息队列的形式在主线程中处理的,此处以消息的形式处理Toast。而消息的处理在mShow类的接口方法run中实现了,也就是handleShow方法了。

public void handleShow() {

            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                //Toast内容布局
                mView = mNextView;
                //获得当前应用的context对象
                Context context = mView.getContext().getApplicationContext();
                //得到当前应用包名
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                //得到当前应用窗口管理器
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                //以下设置Toast窗口参数
                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;
                }
                //Toast窗口起点xy坐标
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                //防止重复添加
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }

                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

分析:该方法最主要的作用就是获得当前Toast所在的应用的窗口管理服务WindowManager,然后设置Toast窗口的一些参数,最后调用WindowManager#addView方法添加Toast视图到屏幕上显示。众所周知,Toast显示机制是在手机屏幕上显示一定时间之后自动消失的,那么这是怎么做到的呢?还是要回到系统服务NotificationManagerService类中

4.5Toast显示时间处理scheduleTimeoutLocked

public class NotificationManagerService extends SystemService {
.................

 mHandler = new WorkerHandler();

private void scheduleTimeoutLocked(ToastRecord r)
    {
        //移除消息队列中已有的ToastRecord类型的消息
        mHandler.removeCallbacksAndMessages(r);
        //重写构建当前Toast消息
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        //设置Toast显示时长,此处我们发现系统已经写死了Toast只能显示两种时长。因此不能自定义时长。
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        //发送延时的消息
        mHandler.sendMessageDelayed(m, delay);
    }

...........

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

分析:此处主要是构建一个Toast消息,然后通过系统的Handler来发送一个延时的消息。那么我们看看mHandler对消息的处理如下:

public class NotificationManagerService extends SystemService { 

    ...................

    private final class WorkerHandler extends Handler
    {
        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MESSAGE_TIMEOUT:
                    //处理超时消息
                    handleTimeout((ToastRecord)msg.obj);
                    break;
                ................
            }
        }

    }
    ..............

    private void handleTimeout(ToastRecord record)
        {

        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                //取消Toast显示
                cancelToastLocked(index);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

分析:以上主要是在消息队列中处理Toast显示时长的消息。最后调用了cancelToastLocked方法来处理

void cancelToastLocked(int index) {
        //从mToastQueue得到当前正在显示的Toast
        ToastRecord record = mToastQueue.get(index);
        try {
            //远程调用Toast#TN#hide方法隐藏当前显示的Toast
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }
        //从mToastQueue移除已经显示完的Toast
        mToastQueue.remove(index);
        keepProcessAliveLocked(record.pid);
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            //继续显示系统mToastQueue队列中下一个Toast
            showNextToastLocked();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

分析: 
该方法同样也是先远程调用Toast#TN#hide方法来隐藏当前Toast。当前Toast移除之后,调用mToastQueue.remove(index);将该Toast从系统的Toast队列中移除,之后继续判断当前系统Toast队列是否还有未处理的Toast消息,如果有则继续调用showNextToastLocked方法来处理,到此又回到刚才处理Toast的地方了,相当于循环处理系统Toast队列中的Toast消息。我们来看看record.callback.hide();远程调用做了什么。

private static class TN extends ITransientNotification.Stub {
 /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

    final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

调用的机制和Toast显示的机制一样,这里就不解释了。主要还是调用了handleHide()方法。

public void handleHide() {
            if (mView != null) {
                if (mView.getParent() != null) {
                    mWM.removeView(mView);
                }
                mView = null;
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

分析:该方法也显而易见,调用WindowManager服务的removeView方法将当前Toast的视图View从当前应用的WindowManager中移除,从此就完成了Toast的隐藏或者移除工作。

4.6Toast总结

Android 中的Toast被系统的NotificationManagerService通知管理服务以Toast队列在系统中管理,也就是Android系统中存在那么一个Toast队列,然后Android应用中所有创建的Toast都会先被加入到系统的Toast队列,系统的NotificationManagerService通知管理服务会以Bindler远程调用的方式自动去管理所有的Toast的显示和隐藏以及Toast显示时长。Toast对话框和Dialog,PopupWindow管理机制不同。Dialog,PopupWindow都是在当前应用中管理,不涉及到系统管理。

Android对话框Dialog,PopupWindow,Toast的实现机制

总结:

自此开发常用的对话框Dialog,PopupWindow,Toast的实现原理基本分析完成。从上面分析你会发现,它们实现的原理其实都一样,都是通过调用WindowManger类中的addView,removeView方法来实现对话框的显示和移除。有上一篇博客知道,整个应用不管有多少个Context对象,都只有一个WindowManager对象管理者整个应用所有的窗口。

结合上一篇博客Android Activity应用窗口的创建过程分析我们知道在Android系统中凡是有关窗口添加移除的操作都有WindowManager对象来管理,然而WindowManager其实是继承自ViewManager,因此Adnroid系统中所有有关窗口视图的显示操作都由ViewManager类来管理

public interface ViewManager
{
    /**
     * Assign the passed LayoutParams to the passed View and add the view to the window.
     * <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
     * errors, such as adding a second view to a window without removing the first view.
     * <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
     * secondary {@link Display} and the specified display can't be found
     * (see {@link android.app.Presentation}).
     * @param view The view to be added to this window.
     * @param params The LayoutParams to assign to view.
     */
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

分析: 
到最后你会发现,其实Android系统中所谓的窗口管理其实就是对视图View的管理,因为窗口Window是一个抽象的概念,Window窗口都是有视图View来呈现,所以对窗口的管理既是对窗口(Window)视图View的管理。

悬浮窗口的实现

如今很多应用都带有一个悬浮窗口,例如:360安全卫士,QQ小火箭等,不管进入任何应用那些悬浮窗口都存在不消失,其实它们也是一个窗口,他们实现的原理是什么呢?如今我们分析了Android系统中窗口Window的管理原理就应该知道怎么实现了。那么让我们来实验一把简单的悬浮窗口的实现吧!

由于悬浮窗口在任何界面都可以显示,因此我们可以在Service里面创建一个悬浮窗口,因为Service一直在后台,除非系统或者用户将它杀死。

注意事项: 
1.首先在配置文件中添加窗口权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"></uses-permission>
  • 1
  • 1

2.如果你是小米手机,请打开该应用的悬浮窗口权限,否则窗口不出现。

/**
 * Created by xjp on 2015/10/13.
 */
public class ServiceFloatView extends Service {
    private WindowManager wm;
    private View floatView;
    private WindowManager.LayoutParams params;


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        showFloatWindow();
    }

    /**
     * 显示悬浮窗口
     */
    private void showFloatWindow() {
        wm = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
        params = new WindowManager.LayoutParams();
        //窗口类型为系统窗口
        params.type = WindowManager.LayoutParams.TYPE_PHONE;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        params.format = PixelFormat.RGBA_8888;
        //窗口起点位置
        params.x = 0;
        params.y = 0;
        //窗口宽高
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        //窗口对齐方式
        params.gravity = Gravity.CENTER;
        floatView = createFloatView();
        //添加窗口
        wm.addView(floatView, params);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        //移除窗口
        wm.removeViewImmediate(floatView);
    }

    private View createFloatView() {
        FloatView floatView = new FloatView(this);
        Button button = new Button(this);
        button.setText("点我啊!");
        button.setTextColor(Color.BLACK);
        button.setBackgroundResource(R.drawable.image);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(ServiceFloatView.this.getApplicationContext(),
                        "Hi,我是悬浮窗口", Toast.LENGTH_SHORT).show();
            }
        });
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        floatView.addView(button, params);
        return floatView;
    }

    /**
     * 重写dispatchTouch事件,使得窗口随着手指滑动而移动
     */
    private class FloatView extends FrameLayout {

        public FloatView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }

        public FloatView(Context context) {
            super(context);
        }

        public FloatView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            float x, y;
            switch (ev.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    x = ev.getX();
                    y = ev.getY();
                    params.x = (int) x;
                    params.y = (int) y;
                    wm.updateViewLayout(floatView, params);
                    break;
            }
            return super.dispatchTouchEvent(ev);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109

分析: 
创建窗口主要注意一下几个参数

  • width:描述窗口的宽度,该变量是父类ViewGroup.LayoutParams的成员变量。
  • height:描述窗口的高度,该变量同样是父类ViewGroup.LayoutParams的成员变量。
  • x:描述窗口的起点X轴的坐标。
  • y:描述窗口起点Y轴的坐标。
  • type:窗口的类型,分为三个大类型:应用窗口,子窗口,系统窗口。
  • flag:窗口特征标记,比如是否全屏,是否隐藏标题栏等。
  • gravity:窗口的对齐方式,居中还是置顶或者置底等等。

MainActivity代码如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final Intent service = new Intent(MainActivity.this, ServiceFloatView.class);
        //启动悬浮窗口
        findViewById(R.id.start).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity.this.startService(service);
            }
        });
        //关闭悬浮窗口
        findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity.this.stopService(service);
            }
        });

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

分析: 
以上仅仅是简单的悬浮窗口的实现,如你想实现更加炫酷的效果或者功能,请自行实现,主要实现思路就是以上代码了。

Android对话框Dialog,PopupWindow,Toast的实现机制

总结: 
对于窗口焦点的flag类型有如下三种: 
//窗口不能获得焦点 
public static final int FLAG_NOT_FOCUSABLE = 0x00000008; 
//窗口不接受触摸屏事件 
public static final int FLAG_NOT_TOUCHABLE = 0x00000010; 
//即使在该window在可获得焦点情况下,允许该窗口之外的点击事件传递到当前窗口后面的的窗口去 
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;

利用Toast反射实现悬浮窗

就不写逻辑了,直接上代码把

public class FloatToastManager {


    private Context mContext;
    private WindowManager.LayoutParams mParams;
    private Toast mToast;
    private Object mTN;
    private Method mShowMethod;
    private Method mHideMethod;
    private boolean isShow = false;
    private Field mParamsField;
    private int mGravity = Gravity.CENTER;
    private int mXOffset = 0;
    private int mYOffset = 0;

    private int mHeight = WindowManager.LayoutParams.MATCH_PARENT;
    private int mWidth = WindowManager.LayoutParams.MATCH_PARENT;

    private boolean mFocusable = false; // 是否获取焦点,决定弹窗外的界面是否能够响应事件
    private boolean mNotInterceptEvent = false; // 是否拦截消息,决定被弹窗盖住的界面是否能够响应事件
    private boolean mIsInited = false;
    private View mView;
    private boolean mEnableToastAnimation = true;


    public FloatToastManager(Context context) {
        mContext = context;
        mToast = new Toast(context);
    }


    /**
     * 决定弹窗的位置,例如:Gravity.CENTER
     */
    public void setGravity(int gravity, int xOffset, int yOffset) {
        mGravity = gravity;
        mXOffset = xOffset;
        mYOffset = yOffset;
    }

    /**
     * 决定弹窗的大小,例如:MATCH_PARENT
     */
    public void setSize(int height, int width) {
        mHeight = height;
        mWidth = width;
    }

    /**
     * 决定弹窗的外部能否响应事件
     */
    public void setFocusable(boolean enable) {
        mFocusable = enable;
    }

    /**
     * 是否启用默认的toast 动画, 默认为TRUE.
     * <remark>全屏时启用此动画, MIUI V8上会出现一个瞬间的底部白线.</remark>
     *
     * @param enable
     */
    public void setEnableToastAnimation(boolean enable) {
        mEnableToastAnimation = enable;
    }

    /**
     * 决定弹窗是否透传事件到后面的窗口
     */
    public void setNotInterceptEvent(boolean enable) {
        mNotInterceptEvent = enable;
    }

    private void initVar(View view) {
        if (mIsInited) {
            return;
        }
        //处理悬浮窗响应物理按钮事件
        view.setFocusableInTouchMode(true);
        mView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if ((keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_BACK) && event.getAction() == KeyEvent.ACTION_DOWN) {
                    //处理悬浮窗相应back,menu物理事件
                }
                return false;
            }
        });

        try {

            Field tnField = mToast.getClass().getDeclaredField("mTN");
            tnField.setAccessible(true);
            mToast.setGravity(mGravity, mXOffset, mYOffset);

            mTN = tnField.get(mToast);
            mShowMethod = mTN.getClass().getMethod("show");
            mHideMethod = mTN.getClass().getMethod("hide");

            mParamsField = mTN.getClass().getDeclaredField("mParams");
            mParamsField.setAccessible(true);
            mParams = (WindowManager.LayoutParams) mParamsField.get(mTN);
            mParams.height = mHeight;
            mParams.width = mWidth;
            mParams.gravity = Gravity.CENTER;
            if (!mEnableToastAnimation) {
                mParams.windowAnimations = android.R.style.Animation;
            }

            mParams.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
                    WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;

            mParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

            mIsInited = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void showView(View view) {

        initVar(view);

        if (isShow) return;
        if (view.getVisibility() != View.VISIBLE) {
            view.setVisibility(View.VISIBLE);
        }
        mToast.setView(view);
        try {
            /**调用tn.mShowMethod()之前一定要先设置mNextView*/
            Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
            tnNextViewField.setAccessible(true);
            tnNextViewField.set(mTN, mToast.getView());
            mShowMethod.invoke(mTN);
            isShow = true;
            mView = view;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void hideView() {
        if (!isShow) return;

        try {
            if (null != mView) {
                // 解决消失时渐隐动画导致的闪烁问题
                mView.setVisibility(View.INVISIBLE);
                mView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (null != mView) {
                            mView.setVisibility(View.VISIBLE);
                            mView = null;
                        }
                    }
                }, 100);
            }
            mHideMethod.invoke(mTN);
            //mToast.cancel();

            isShow = false;
            Log.i("FloatToastManager", "hideView called");
        } catch (Exception e) {
            e.printStackTrace();
            Log.i("FloatToastManager", "hideView exception " + e.getMessage());
        }
    }

    public View getView() {
        return mView;
    }

    public boolean isShow() {
        return isShow;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176


相关标签: android