android 开发零起步学习笔记(二十二):ANDROID应用ACTIVITY、DIALOG、POPWINDOW、TOAST窗口添加机制及源码分析(二)

2020年08月13日 阅读数:212
这篇文章主要向大家介绍android 开发零起步学习笔记(二十二):ANDROID应用ACTIVITY、DIALOG、POPWINDOW、TOAST窗口添加机制及源码分析(二),主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。



第二部分:html


4  Android应用PopWindow窗口添加显示机制源码

PopWindow实质就是弹出式菜单,它与Dialag不一样的地方是不会使依赖的Activity组件失去焦点(PopupWindow弹出后可 以继续与依赖的Activity进行交互),Dialog却不能这样。同时PopupWindow与Dialog另外一个不一样点是PopupWindow是 一个阻塞的对话框,若是你直接在Activity的onCreate等方法中显示它则会报错,因此PopupWindow必须在某个事件中显示地或者是开 启一个新线程去调用。java

说这么多仍是直接看代码吧。android

4-1  PopWindow窗口源码分析

依据PopWindow的使用,咱们选择最经常使用的方式来分析,以下先看其中经常使用的一种构造函数:git

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的展现函数,以下:github

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的上面)旁边时经理了上面三步,咱们一步一步来分析,先看第一步,源码以下:app

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类,以下源码:ide

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方法看看第三步,以下源码:oop

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的窗口加载显示机制就分析完毕了,接下来进行总结与应用开发技巧提示。

4-2  PopWindow窗口源码分析总结及应用开发技巧提示

经过上面分析能够发现总结以下图:

能够看见,PopWindow彻底使用了Activity的Window与WindowManager,相对来讲比较简单容易记理解。

再来看一个开发技巧:

若是设置了PopupWindow的background,则点击Back键或者点击PopupWindow之外的区域时PopupWindow就 会dismiss;若是不设置PopupWindow的background,则点击Back键或者点击PopupWindow之外的区域 PopupWindow不会消失。

5  Android应用Toast窗口添加显示机制源码

5-1 基础知识准备

在开始分析这几个窗口以前须要脑补一点东东,咱们从应用层开发来直观脑补,这样下面分析源码时就不蛋疼了。以下是一个咱们写的两个应用实现 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的<action android:name="io.github.yanbober.myapplication.aidl" />
 
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的机制咱们后面会写文章一步一步往下分析。到此的准备知识已经足够用来理解下面咱们的源码分析了。

5-2 Toast窗口源码分析

咱们经常使用的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类的,以下:

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
     private final IBinder mService =  new  INotificationManager.Stub() {
         // Toasts
         // ============================================================================
 
         @Override
         public void enqueueToast(String pkg, ITransientNotification callback, int duration)
         {
             ......
             synchronized (mToastQueue) {
                 int callingPid = Binder.getCallingPid();
                 long callingId = Binder.clearCallingIdentity();
                 try  {
                     ToastRecord record;