本文介绍App开发经常涉及的自定义控件相关技术,主要包括自定义视图的过程与步骤、自定义动画的原理与实现、自定义对话框的概念与示例、自定义通知栏的用法与定制,另外介绍四大组件之一的服务Service的生命周期与启停方式。
自定义视图
自定义视图的编码主要由3部分组成:
(1)重写构造函数,初始化该视图的自有属性。
(2)重写测量函数onMeasure,计算该视图的宽与高(一般只有复杂视图才重写该函数)。
(3)重写绘图函数onLayout、onDraw、dispatchDraw,视情况重写3个中的一个或多个。
一般要重写3个构造函数。前面在演示新控件CustomPagerTab时,示例代码给出了3个构造函数(实际只实现了两个),分别是:
(1)只带一个参数的public CustomPagerTab (Context context)。在代码中声明对象时采用该构造函数。
(2)带两个参数的public CustomPagerTab (Context context,AttributeSet attrs)。在布局文件中引用自定义视图时采用该构造函数。
总结一下onLayout、onDraw、dispatchDraw三个函数的区别:
(1)onLayout只能调整子视图的位置,而onDraw和dispatchDraw允许绘制新图形。
(2)onDraw的调用在绘制子视图之前,而dispatchDraw的调用在绘制子视图之后。
(3)onLayout若想立即显示位置调整后的视图,则要调用requestLayout方法;onDraw和dispatchDraw若想立即显示图形绘制后的视图,则要调用invalidate方法。
自定义动画
Runnable接口可声明一连串任务,定义了接下来要做的事情。简单地说,Runnable接口就是一个代码片段。实现Runnable接口只需重写run函数,在该方法内部存放要运行的任务代码。run函数无须显式调用,在启动Runnable实例时就会调用对象的run方法。
尽管基本视图View提供了post与postDelayed方法用于启动Runnable任务,不过实际开发中经常利用Handler启动任务。下面是Handler处理Runnable任务的常见方法说明:
post:立即启动Runnable任务。
postDelayed:延迟若干时间后启动Runnable任务。
postAtTime:在指定时间启动Runnable任务。
removeCallbacks:移除指定的Runnable任务。
计时器是Runnable的一个简单应用,与动画的实现原理相关,如电影每秒播放20帧画面,连起来就是会动的视频,动画的渲染与之同理。
自定义对话框
App界面附着在窗口Window上。大至整个活动页面,小至Toast的提示窗,还有对话框Dialog,都建立在窗口上。如果想熟练掌握对话框,就必须先了解窗口。读者也许对窗口的概念不甚理解,下面从Window的5个常用方法开始介绍。
-
setContentView:设置内容视图。这个方法是不是很熟悉?我们每天打交道的Activity第一句就是setContentView,查看源码后发现内部原来调用了同名方法getWindow().setContentView。
-
setLayout:设置内容视图的宽、高尺寸。
-
setGravity:设置内容视图的对齐方式。
-
setBackgroundDrawable:设置内容视图的背景。
-
findViewById:根据资源ID获取该视图的对象。这个方法每个Activity代码都要用许多遍。查看Activity源码后可以发现该方法也是调用Window的同名方法getWindow().findViewById。
原来,窗口默默地做了许多事情,只是一般人不知道罢了。熟悉了Window的概念和用法后,再来看看Dialog的工作机制,在屏幕上显示对话框主要有3个步骤:
步骤01 构造一个对话框对象并指定该对话框的样式。
步骤02 获取该对话框依赖的窗口对象,设置内容视图并指定窗口的尺寸。
步骤03 完成相关属性设置,显示对话框。
下面来看具体的对话框操作方法。 -
Dialog构造函数:可定义对话框的主题样式(样式在styles.xml中定义),如是否有标题、是否为半透明、对话框的背景是什么等。
-
getWindow:获取对话框的窗口对象。该方法是自定义对话框的关键,首先获取对话框所在的窗口对象,然后往这个窗口添加定制视图。
-
show:显示对话框。
-
isShowing:判断对话框是否显示。
-
hide:隐藏对话框。
-
dismiss:关闭对话框。
-
setCancelable:设置对话框是否可取消。
-
setCanceledOnTouchOutside:点击对话框外部区域是否自动关闭对话框。默认会自动关闭。
-
setOnShowListener:设置对话框的显示监听器。需实现OnShowListener接口的onShow方法。
-
setOnDismissListener:设置对话框的消失监听器。需实现OnDismissListener接口的onDismiss方法。
远程视图RemoteViews
前面介绍Notification的常用方法时提到setContent方法可以在设置定制的通知栏视图RemoteViews时取代Builder的默认视图模板。这表示通知栏允许自定义,并且自定义通知栏需要采用远程视图RemoteViews。
与活动页面相比,如果说对话框是一个小型页面,远程视图就是一个小型且简化的页面。简化的意思是功能减少了,限制变多了。虽然RemoteViews与Activity一样有自己的布局文件,但是RemoteViews的使用权限小了很多。两者的区别主要有:
(1)RemoteViews主要用于通知栏部件和桌面部件,而Activity用于页面。
(2)RemoteViews只支持少数几种控件,如TextView、ImageView、Button、ImageButton、ProgressBar、Chronometer(计时器)和AnalogClock(模拟时钟)。
(3)RemoteViews不可直接获取和设置控件信息,只能通过该对象的set方法修改控件信息。
下面来看远程视图的常用方法。
- 构造函数:创建一个RemoteViews对象。第一个参数是包名,第二个参数是布局文件id。
- setViewVisibility:设置指定控件是否可见。
- setViewPadding:设置指定控件的间距。
- setTextViewText:设置指定TextView或Button控件的文字内容。
- setTextViewTextSize:设置指定TextView或Button控件的文字大小。
- setTextColor:设置指定TextView或Button控件的文字颜色。
- setTextViewCompoundDrawables:设置指定TextView或Button控件的文字周围图标。
- setImageViewResource:设置ImageView或ImgaeButton控件的资源编号。
- setImageViewBitmap:设置ImageView或ImgaeButton控件的位图对象。
- setChronometer:设置计时器信息。
- setProgressBar:设置进度条信息,包括最大值与当前进度。
- setOnClickPendingIntent:设置指定控件的点击响应动作。
服务Service基础
本节介绍为何使用服务Service和如何使用服务,包括服务的生命周期和在3种启停方式下的生命周期过程,有普通启停、立即绑定和延迟绑定。另外,还介绍了怎样结合通知推送Notification实现把服务推送到前台的功能。
Service的生命周期
服务Service是Android的四大组件之一,常用在看不见页面的高级场合,如第5章定时器用到了系统的闹钟服务,6.4节通知推送用到了系统的通知服务。既然Android有系统服务,App也可以有自己的服务。Service与Activity相比,不同之处在于没有对应的页面,相同之处在于有生命周期。要想用好服务,就要探究其生命周期。
下面是Service与生命周期有关的方法说明。
- onCreate:创建服务。
- onStart:开始服务,Android 2.0以下版本使用,现已废弃。
- onStartCommand:开始服务,Android 2.0及以上版本使用。该函数的返回值说明见表6-5。
- onDestroy:销毁服务。
- onBind:绑定服务。
- onRebind:重新绑定。该方法只有当上次onUnbind返回true的时候才能被调用。
- onUnbind:解除绑定。返回值为true表示允许再次绑定,再绑定时调用onRebind方法;返回值为false表示只能绑定一次,不能再次绑定,默认为false。
Service分好几种生命周期方法。原因是服务存在多种启停方式,如普通启停、立即绑定、延迟绑定,每种启停方式都对应不同的周期方法。下面分别叙述3种启停方式及其生命周期说明。
1. 普通启停
普通启停是最简单的用法。下面是该方式的服务代码:
在Acitivity代码中,启停服务也很简单,调用startService方法即可启动服务,调用stopService方法即可停止服务。当然,也可以在Intent对象中传递参数信息。示例的调用代码如下:
普通启停方式的服务生命周期可通过打印日志观察,也可在页面上直接显示日志。启动服务依次调用了onCreate与onStartCommand方法,如图6-30所示。停止服务调用了onDestroy方法。
停止服务调用了onDestroy方法,如图6-31所示。
2. 立即绑定
绑定方式的服务定义有所不同,因为绑定的服务可能运行于另一个进程,所以必须定义一个Binder对象用来进行进程间的通信。下面是一个绑定方式的服务代码:
这个服务在绑定时会打印日志“绑定服务开始旅程!”,在解除绑定时会打印日志“绑定服务结束旅程!”。在Activity中,绑定/解绑服务的做法与普通方式不同,首先要定义一个ServiceConnection的服务连接对象,然后调用bindService方法或unbindService方法进行绑定或解绑操作,具体的示例代码如下:
接下来,继续观察立即绑定方式的生命周期,该方式的服务周期日志如图6-32和图6-33所示。其中,图6-32所示为立即绑定时的界面,此时依次调用onCreate和onBind方法;图6-33所示为立即解绑时的界面,此时依次调用onUnbind和onDestroy方法。
3. 延迟绑定
延迟绑定与立即绑定的区别在于:延迟绑定是在页面上先通过startService方法启动服务,然后通过bindService方法绑定已存在的服务。这样一来,因为启动操作在先,所以解绑操作只能撤销绑定操作,而不能撤销启动操作。由于解绑服务不能停止服务,因此存在再次绑定服务的可能。
下面观察延迟绑定的日志,验证一下实际结果是否符合之前的猜想。依次查看“启动服务→绑定服务→解绑服务”的运行日志,如图6-34所示;依次查看“绑定服务→解绑服务→停止服务”的运行日志,如图6-35所示。
图6-35 再次绑定的日志
从日志中可以看到,延迟绑定与立即绑定两种方式的生命周期区别在于:
(1)延迟绑定的首次绑定操作只调用onBind方法,再次绑定只调用onRebind方法(是否允许再次绑定要看上次onUnbind方法的返回值)。
(2)延迟绑定的解绑操作只调用onUnbind方法。
推送服务到前台
服务没有自己的布局文件,也就意味着无法直接在页面上展示,要想了解服务的运行情况,要么通过打印日志,要么获取某个页面的静态对象,然后在该页面上显示运行结果。然而活动页面有自身的生命周期,极有可能发生服务尚在运行但页面早已退出的情况,所以该方式不可靠。幸好,服务不只能在外部进行启停或绑定,还能在内部模拟启停,当然仅是模拟而已。
服务内部的启停方法也有对应的两个函数。
- startForeground:把当前服务切换到前台运行。第一个参数表示通知的编号,第二个参数表示Notification对象,意味着切换到前台就是展示到通知栏。
- stopForeground:停止前台运行。参数为true表示清除通知,参数为false表示不清除。
注意,从Android 9.0开始,要想在服务中正常调用startForeground方法,还需修改AndroidManifest.xml,添加如下所示的前台服务权限配置: