第二部分:
PopWindow实质就是弹出式菜单,它与Dialag不同的地方是不会使依赖的Activity组件失去焦点(PopupWindow弹出后可 以继续与依赖的Activity进行交互),Dialog却不能这样。同时PopupWindow与Dialog另一个不同点是PopupWindow是 一个阻塞的对话框,如果你直接在Activity的onCreate等方法中显示它则会报错,所以PopupWindow必须在某个事件中显示地或者是开 启一个新线程去调用。
说这么多还是直接看代码吧。
依据PopWindow的使用,我们选择最常用的方式来分析,如下先看其中常用的一种构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class PopupWindow {
......
//我们只分析最常用的一种构造函数
public PopupWindow(View contentView, int width, int height, boolean focusable) {
if
(contentView !=
null
) {
//获取mContext,contentView实质是View,View的mContext都是构造函数传入的,View又层级传递,所以最终这个mContext实质是Activity!!!很重要
mContext = contentView.getContext();
//获取Activity的getSystemService的WindowManager
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
//进行一些Window类的成员变量初始化赋值操作
setContentView(contentView);
setWidth(width);
setHeight(height);
setFocusable(focusable);
}
......
}
|
可以看见,构造函数只是初始化了一些变量,看完构造函数继续看下PopWindow的展示函数,如下:
1
2
3
4
5
6
7
8
9
10
11
|
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
......
//anchor是Activity中PopWindow准备依附的View,这个View的token实质也是Activity的Window中的token,也即Activity的token
//第一步 初始化WindowManager.LayoutParams
WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
//第二步
preparePopup(p);
......
//第三步
invokePopup(p);
}
|
可以看见,当我们想将PopWindow展示在anchor的下方向(Z轴是在anchor的上面)旁边时经理了上面三步,我们一步一步来分析,先看第一步,源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private WindowManager.LayoutParams createPopupLayout(IBinder token) {
//实例化一个默认的WindowManager.LayoutParams,其中type=TYPE_APPLICATION
WindowManager.LayoutParams p =
new
WindowManager.LayoutParams();
//设置Gravity
p.gravity = Gravity.START | Gravity.TOP;
//设置宽高
p.width = mLastWidth = mWidth;
p.height = mLastHeight = mHeight;
//依据背景设置format
if
(mBackground !=
null
) {
p.format = mBackground.getOpacity();
}
else
{
p.format = PixelFormat.TRANSLUCENT;
}
//设置flags
p.flags = computeFlags(p.flags);
//修改type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,mWindowLayoutType有初始值,type类型为子窗口
p.type = mWindowLayoutType;
//设置token为Activity的token
p.token = token;
......
return
p;
}
|
接着回到showAsDropDown方法看看第二步,如下源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private void preparePopup(WindowManager.LayoutParams p) {
......
//有无设置PopWindow的background区别
if
(mBackground !=
null
) {
......
//如果有背景则创建一个PopupViewContainer对象的ViewGroup
PopupViewContainer popupViewContainer =
new
PopupViewContainer(mContext);
PopupViewContainer.LayoutParams listParams =
new
PopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, height
);
//把背景设置给PopupViewContainer的ViewGroup
popupViewContainer.setBackground(mBackground);
//把我们构造函数传入的View添加到这个ViewGroup
popupViewContainer.addView(mContentView, listParams);
//返回这个ViewGroup
mPopupView = popupViewContainer;
}
else
{
//如果没有通过PopWindow的setBackgroundDrawable设置背景则直接赋值当前传入的View为PopWindow的View
mPopupView = mContentView;
}
......
}
|
可以看见preparePopup方法的作用就是判断设置View,如果有背景则会在传入的contentView外面包一层 PopupViewContainer(实质是一个重写了事件处理的FrameLayout)之后作为mPopupView,如果没有背景则直接用 contentView作为mPopupView。我们再来看下这里的PopupViewContainer类,如下源码:
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
|
private class PopupViewContainer extends FrameLayout {
......
@Override
protected int[] onCreateDrawableState(int extraSpace) {
......
}
@Override
public boolean dispatchKeyEvent(KeyEvent 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) {
......
if
(xxx) {
dismiss();
}
......
}
@Override
public void sendAccessibilityEvent(int eventType) {
......
}
}
|
可以看见,这个PopupViewContainer是一个PopWindow的内部私有类,它继承了FrameLayout,在其中重写了Key 和Touch事件的分发处理逻辑。同时查阅PopupView可以发现,PopupView类自身没有重写Key和Touch事件的处理,所以如果没有将 传入的View对象放入封装的ViewGroup中,则点击Back键或者PopWindow以外的区域PopWindow是不会消失的(其实PopWindow中没有向Activity及Dialog一样new新的Window,所以不会有新的callback设置,也就没法处理事件消费了)。
接着继续回到showAsDropDown方法看看第三步,如下源码:
1
2
3
4
5
6
7
8
|
private void invokePopup(WindowManager.LayoutParams p) {
if
(mContext !=
null
) {
p.packageName = mContext.getPackageName();
}
mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
mWindowManager.addView(mPopupView, p);
}
|
可以看见,这里使用了Activity的WindowManager将我们的PopWindow进行了显示。
到此可以发现,PopWindow的实质无非也是使用WindowManager的addView、updateViewLayout、 removeView进行一些操作展示。与Dialog不同的地方是没有新new Window而已(也就没法设置callback,无法消费事件,也就是前面说的PopupWindow弹出后可以继续与依赖的Activity进行交互 的原因)。
到此PopWindw的窗口加载显示机制就分析完毕了,接下来进行总结与应用开发技巧提示。
通过上面分析可以发现总结如下图:
可以看见,PopWindow完全使用了Activity的Window与WindowManager,相对来说比较简单容易记理解。
再来看一个开发技巧:
如果设置了PopupWindow的background,则点击Back键或者点击PopupWindow以外的区域时PopupWindow就 会dismiss;如果不设置PopupWindow的background,则点击Back键或者点击PopupWindow以外的区域 PopupWindow不会消失。
在开始分析这几个窗口之前需要脑补一点东东,我们从应用层开发来直观脑补,这样下面分析源码时就不蛋疼了。如下是一个我们写的两个应用实现 Service跨进程调用服务ADIL的例子,客户端调运远程Service的start与stop方法控制远程Service的操作。
Android系统中的应用程序都运行在各自的进程中,进程之间是无法直接交换数据的,但是Android为开发者提供了AIDL跨进程调用Service的功能。其实AIDL就相当于双方约定的一个规则而已。
先看下在Android Studio中AIDL开发的工程目录结构,如下:
由于AIDL文件中不能出现访问修饰符(如public),同时AIDL文件在两个项目中要完全一致而且只支持基本类型,所以我们定义的AIDL文件如下:
ITestService.aidl
1
2
3
4
5
6
|
package io.github.yanbober.myapplication;
interface ITestService {
void start(int id);
void stop(int id);
}
|
再来看下依据aidl文件自动生成的ITestService.java文件吧,如下:
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
|
/*
* This file is auto-generated. DO NOT MODIFY.
*/
package io.github.yanbober.myapplication;
public interface ITestService extends android.os.IInterface
{
//Stub类是ITestService接口的内部静态抽象类,该类继承了Binder类
public static abstract class Stub extends android.os.Binder implements io.github.yanbober.myapplication.ITestService
{
......
//这是抽象静态Stub类中的asInterface方法,该方法负责将service返回至client的对象转换为ITestService.Stub
//把远程Service的Binder对象传递进去,得到的是远程服务的本地代理
public static io.github.yanbober.myapplication.ITestService asInterface(android.os.IBinder obj)
{
......
}
......
//远程服务的本地代理,也会继承自ITestService
private static class Proxy implements io.github.yanbober.myapplication.ITestService
{
......
@Override
public void start(int id) throws android.os.RemoteException
{
......
}
@Override
public void stop(int id) throws android.os.RemoteException
{
......
}
}
......
}
//两个方法是aidl文件中定义的方法
public void start(int id) throws android.os.RemoteException;
public void stop(int id) throws android.os.RemoteException;
}
|
这就是自动生成的java文件,接下来我们看看服务端的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
26
27
28
29
30
31
32
33
|
//记得在AndroidManifet.xml中注册Service的
public class TestService extends Service {
private TestBinder mTestBinder;
//该类继承ITestService.Stub类而不是Binder类,因为ITestService.Stub是Binder的子类
//进程内的Service定义TestBinder内部类是继承Binder类
public class TestBinder extends ITestService.Stub {
@Override
public void start(int id) throws RemoteException {
Log.i(
null
,
"Server Service is start!"
);
}
@Override
public void stop(int id) throws RemoteException {
Log.i(
null
,
"Server Service is stop!"
);
}
}
@Override
public IBinder onBind(Intent intent) {
//返回Binder
return
mTestBinder;
}
@Override
public void onCreate() {
super
.onCreate();
//实例化Binder
mTestBinder =
new
TestBinder();
}
}
|
现在服务端App的代码已经OK,我们来看下客户端的代码。客户端首先也要像上面的工程结构一样,把AIDL文件放好,接着在客户端使用远程服务端的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
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
|
public class MainActivity extends Activity {
private static final String REMOT_SERVICE_ACTION =
"io.github.yanbober.myapplication.aidl"
;
private Button mStart, mStop;
private ITestService mBinder;
private ServiceConnection connection =
new
ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//获得另一个进程中的Service传递过来的IBinder对象
//用IMyService.Stub.asInterface方法转换该对象
mBinder = ITestService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStart = (Button)
this
.findViewById(R.id.start);
mStop = (Button)
this
.findViewById(R.id.stop);
mStart.setOnClickListener(clickListener);
mStop.setOnClickListener(clickListener);
//绑定远程跨进程Service
bindService(
new
Intent(REMOT_SERVICE_ACTION), connection, BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super
.onDestroy();
//取消绑定远程跨进程Service
unbindService(connection);
}
private View.OnClickListener clickListener =
new
View.OnClickListener() {
@Override
public void onClick(View v) {
////调用远程Service中的start与stop方法
switch
(v.getId()) {
case
R.id.start:
try
{
mBinder.start(0x110);
}
catch
(RemoteException e) {
e.printStackTrace();
}
break
;
case
R.id.stop:
try
{
mBinder.stop(0x120);
}
catch
(RemoteException e) {
e.printStackTrace();
}
break
;
}
}
};
}
|
到此你对应用层通过AIDL使用远程Service的形式已经很熟悉了,至于实质的通信使用Binder的机制我们后面会写文章一步一步往下分析。到此的准备知识已经足够用来理解下面我们的源码分析了。
我们常用的Toast窗口其实和前面分析的Activity、Dialog、PopWindow都是不同的,因为它和输入法、墙纸类似,都是系统窗口。
我们还是按照最常用的方式来分析源码吧。
我们先看下Toast的静态makeText方法吧,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
//new一个Toast对象
Toast result =
new
Toast(context);
//获取前面有篇文章分析的LayoutInflater
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//加载解析Toast的布局,实质transient_notification.xml是一个LinearLayout中套了一个@android:id/message的TextView而已
View v = inflate.inflate(com.android.internal.R.layout.transient_notification,
null
);
//取出布局中的TextView
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
//把我们的文字设置到TextView上
tv.setText(text);
//设置一些属性
result.mNextView = v;
result.mDuration = duration;
//返回新建的Toast
return
result;
}
|
可以看见,这个方法构造了一个Toast,然后把要显示的文本放到这个View的TextView中,然后初始化相关属性后返回这个新的Toast对象。
当我们有了这个Toast对象之后,
可以通过show方法来显示出来,如下看下show方法源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public void show() {
......
//通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,当前Toast类相当于上面例子的客户端!!!相当重要!!!
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try
{
//把TN对象和一些参数传递到远程NotificationManagerService中去
service.enqueueToast(pkg, tn, mDuration);
}
catch
(RemoteException e) {
// Empty
}
}
|
我们看看show方法中调运的getService方法,如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
//远程NotificationManagerService的服务访问接口
private static INotificationManager sService;
static private INotificationManager getService() {
//单例模式
if
(sService !=
null
) {
return
sService;
}
//通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口
sService = INotificationManager.Stub.asInterface(ServiceManager.getService(
"notification"
));
return
sService;
}
|
通过上面我们的基础脑补实例你也能看懂这个getService方法了吧。那接着我们来看mTN吧,好像mTN在Toast的构造函数里见过一眼,我们来看看,如下:
1
2
3
4
5
6
7
8
|
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);
}
|
可以看见mTN确实是在构造函数中实例化的,那我们就来看看这个TN类,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//类似于上面例子的服务端实例化的Service内部类Binder
private static class TN extends ITransientNotification.Stub {
......
//实现了AIDL的show与hide方法
@Override
public void show() {
if
(localLOGV) Log.v(TAG,
"SHOW: "
+
this
);
mHandler.post(mShow);
}
@Override
public void hide() {
if
(localLOGV) Log.v(TAG,
"HIDE: "
+
this
);
mHandler.post(mHide);
}
......
}
|
看见没有,TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub。你这时指定好奇 ITransientNotification.Stub是个啥玩意,对吧?其实你在上面的脑补实例中见过它的,他出现在服务端实现的Service中, 就是一个Binder对象,也就是对一个aidl文件的实现而已,我们看下这个ITransientNotification.aidl文件,如下:
1
2
3
4
5
6
7
|
package android.app;
/** @hide */
oneway interface ITransientNotification {
void show();
void hide();
}
|
看见没有,和我们上面的例子很类似吧。
再回到上面分析的show()方法中可以看到,我们的Toast是传给远程的NotificationManagerService管理的,为了 NotificationManagerService回到我们的应用程序(回调),我们需要告诉NotificationManagerService 我们当前程序的Binder引用是什么(也就是TN)。是不是觉得和上面例子有些不同,这里感觉Toast又充当客户端,又充当服务端的样子,实质就是一 个回调过程而已。
继续来看Toast中的show方法的service.enqueueToast(pkg, tn, mDuration);语句,service实质是远程的NotificationManagerService,所以enqueueToast方法就是 NotificationManagerService类的,如下: