《Android开发艺术探索》
一、Activity 的生命周期和启动模式
1. 当前 Activity 为 A,此时打开 Activity B:A.onPause() → B.onCreate() → B.onStart() → B.onResume() → A.onStop(),故不能在 onPause 中做重量级操作,使新 Activity 尽快显示出来并切换到前台。
2. 当系统内存不足时,系统会按照 [ 后台 Activity → 可见但非前台 Activity → 前台 Activity ] 的优先级杀死目标 Activity 所在的进程。如果一个进程中没有四大组件在执行,那么这个进程将很快被系统杀死 → 故 将后台工作放入 Service 中从而保证进程有一定的优先级,不易被轻易杀死。
3. Activity 的 LaunchMode
(1) standard 标准模式(系统默认)—— 多实例实现
每次启动一个 Activity 都会重新创建一个新的实例,不管这个实例是否已经存在,被创建的实例的生命周期符合典型情况下 Activity 的生命周期。
一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。
谁启动了这个 Activity ,这个 Activity 就运行在启动它的那个 Activity 所在的栈中。
当用 ApplicationContext 启动 standard 模式的 Activity 时会报错,因为非 Activity
类型的 Context 并没有所谓的任务栈,解决:为待启动的 Activity 指定 FLAG_ACTIVITY_NEW_TASK 标记位,这样启动的时候会为它创建一个新的任务栈,此时待启动
Activity 实际是以 singleTask 模式启动的。
(2)singleTop 栈顶复用模式
若新 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,同时它的
onNewIntent 方法会被回调,通过此方法的参数可以取出当前请求的信息。此 Activity 的 onCreate、onStart 不会被调用,因为它并没有发生改变。若不在栈顶,则会重新创建。
(3)singleTask 栈内复用模式 —— 一种单实例模式
只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例。也会回调 onNewIntent。
如 Activity A 为 singleTask 模式启动后,系统首先寻找是否存在 A 想要的任务栈,若不存在,则重新创建一个任务栈,然后创建 A 的实例放入栈中。若存在 A 所需的任务栈,若栈中有 A 的实例存在,系统就把 A 调到栈顶并调用 onNewIntent
方法,同时由于singleTask 默认具有 clearTop 效果,会导致栈内所有在 A 上面的 Activity 全部出栈;若实例不存在,则创建并压入栈中。
(4)singleInstance 单实例模式 —— 加强的 singleTask
具有此种模式的 Activity 只能单独地位于一个任务栈中。
4. Activity 所需的任务栈
参数 TaskAffinity 标识了 Activity 所需的任务栈的名字,其值为字符串,且中间必须含有包名分隔符“.”。默认所有 Activity 所需的任务栈名字为应用的包名。也可为每个 Activity 单独指定 TaskAffinity 属性,但其值不能和包名相同。该属性主要和
singleTask 启动模式或 allowTaskReparenting 属性配对使用。
任务栈分为前台任务栈和后台任务栈,后台任务栈中的 Activity 位于暂停状态。
(1)TaskAffinity 和 singleTask 结合使用:TaskAffinity 是该 Activity 的目前任务栈的名字,待启动的 Activity 会运行在名字和 TaskAffinity 相同的任务栈中。
(2)TaskAffinity 和 allowTaskReparenting 结合使用:当应用 A 启动了应用 B 的某个 Activity 后,若这个 Activity 的 allowTaskReparenting 属性为 true,则当 B 被启动后,此 Activity
会直接从 A 的任务栈转移到 B 的任务栈中。
5. IntentFilter 匹配规则
(1)action
要求 Intent 中必须有一个 action,且必须能够和过滤规则中的某个 action 相同
(2)category
Intent 中可以没有 category,但一旦有 category,则不管有几个必须每个都要能和过滤规则中任何一个 category 相同。
为了 Activity 能够接收隐式调用,必须在 intent-filter 中指定 “android.intent.category.DEFAULT”。
(3)data
语法:
<span style="font-size:14px;"><span style="font-size:14px;"><data android:scheme="string" //URI 的模式,如 http、file、content。若未指定 scheme,则整个 URI 无效。默认为 content 和 file。 android:host="string" //主机名,如 www.baidu.com。若未指定,则 URI 无效。 android:port="string" //端口号,仅当 URI 中指定了 scheme 和 host 时才有效。 android:path="string" android:pathPattern="string" android:pathPrefix="string" android:mimeType="string"/></span></span>
data 由 2 部分组成,mimeType (媒体类型)和 URI。
URI 结构:
<span style="font-size:14px;"><span style="font-size:14px;"><scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]</span></span>
Intent 中必须含有 data,且 data 能完全匹配过滤规则中的某一个 data。
<span style="font-size:14px;"><span style="font-size:14px;">android:pathPattern="string"</span></span>
注:
入口 Activity 会出现在系统应用列表中,二者缺一不可。
<span style="font-size:14px;"><span style="font-size:14px;"><action android:name="android.intent.action.MAIN/> <category android:name="android.intent.category.LAUNCHER"/></span></span>
二、IPC 机制
1. IPC:Inter-Process Communication,进程间通信 / 跨进程通信。
2. 主线程也叫 UI 线程,在 UI 线程里才能操作界面元素。
3. Android 中的多进程模式
在 Android 中使用多进程只有一种方法:给四大组件在 AndroidMenifest 指定
<span style="font-size:14px;"><span style="font-size:14px;">android:process //若未指定,默认进程名是包名</span></span>
属性,即我们无法给一个线程或一个实体类指定其运行时所在的进程。
进程名以 “:” 开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中;
进程名不以 ":" 开头的属于全局进程,其他应用通过 ShareUID 方式可以和它跑在同一个进程中。
Android 系统会为每一个应用分配一个唯一的 UID,具有相同 UID 的应用才能共享数据。
只有 2 个应用有相同的 ShareUID 且签名相同才可以跑在同一个进程中。此时不管它们是否在同一个进程中都可以互相访问对方的私有数据,如 data 目录、组件信息等;若在同一个进程中,则还可以共享内存数据。
Android 为每个进程都分配一个独立的虚拟机(相当于把应用重启了一遍),不同的虚拟机在内存分配上有不同的地址空间,导致在不同的虚拟机中访问同一个类对象会产生多份副本。
所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败。
使用多进程有如下问题:
(1)静态成员和单例模式完全失效。
(2)线程同步机制完全失效。
(3)SharedPreferences 的可靠性下降。(不支持 2 个进程同时写)
(4)Application 会多次创建。
4. 序列化
(1)Serializable 接口
是 Java 提供的序列化接口,是一个空接口,为对象提供标准的序列化和反序列化操作。使用简单,但开销大,需要大量I/O操作。
使用 Serializable 实现序列化只需这个类实现 Serializable 接口并声明如下标识即可自动实现默认的序列化过程:
<span style="font-size:14px;"><span style="font-size:14px;">private static final long serialVersionUID = 87254421243431L;</span></span>
若不声明会对反序列化产生影响。
注:静态成员变量属于类不属于对象,故不会参与序列化过程;用 transient 关键字标记的成员变量不参与序列化过程。
(2)Parcelable 接口
Android 提供的序列化接口。使用稍麻烦,但效率高。
Android 序列化首选 Parcelable,主要用在内存序列化上;将对象序列化到存储设备或通过网络传输应使用 Serializable。
5. Android 中的 IPC 方式
(1)Bundle
传输的数据必须能被序列化,如 基本类型、实现了 Parcelable 或 Serializable 接口的对象以及一些 Android 支持的特殊对象。
(2)文件共享
对文件格式没有要求,只要读 / 写双方约定数据格式即可。
注:SharedPreferences 是 Android 中提供的轻量级存储方案,也是文件的一种,但系统对它的读 / 写有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,在多进程模式下,系统对它的读 / 写就变得不靠谱,当面对高并发的读 / 写访问时会有很大几率丢失数据。故不建议在进程间通信中使用 SharedPreferences。
(3)Messenger
一种轻量级的 IPC 方案,底层实现是 AIDL。
一次处理一个请求,故在服务端不用考虑线程同步的问题,因为服务端中不存在并发执行的问题。
串行方式,不适合大量并发请求。
(4)AIDL
(5)ContentProvider
Android 提供的专门用于不同应用间进行数据共享的方式。底层实现是 Binder。
主要以表格的形式组织数据,还支持文件数据。
需要注册,
<span style="font-size:14px;"><span style="font-size:14px;">android:authorities //ContentProvider 的唯一标识。</span></span>
(6)Socket 套接字
①流式套接字 —— TCP 协议,是面向连接的协议,提供稳定的双向通信功能,三次握手,超时重传机制。
②用户数据报套接字 —— UDP 协议,无连接,不稳定的单向通信。效率更高,但不能保证数据一定能正确传输。
声明权限:
<span style="font-size:14px;"><span style="font-size:14px;"><uses-permission android:name="android.permission.INTERNER"/> <uses-permission android:name="android.permission.ACCESS_NEWWORD_STATE"/></span></span>
不能在主线程中访问网络
7. Binder 连接池
8. 小结
名称 | 优点 | 缺点 | 适用场景 |
Bundle | 简单易用 | 只能传输 Bundle 支持的数据 | 四大组件间的进程间通信 |
文件共享 | 简单易用 | 不适合高并发场景,无法做到进程间的即时通信 | 无并发访问情形,交换简单的数据实时性不高的场景 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信 | 使用稍复杂,需要处理好线程同步 | 一对多通信且有 RPC 需求 |
Messenger | 功能一般,支持一对多串行通信,支持实时通信 | 不能很好处理高并发情形,不支持 RPC,数据通过 Message 进行传输,只能传输 Bundle 支持的数据类型 | 低并发的一对多即时通信,无 RPC 需求,或者无须返回结果的 RPC 需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过 Call 方法扩展其他操作 | 可理解为受约束的 AIDL,主要提供数据源的 CRUD 操作 | 一对多的进程间的数据共享 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍有繁琐,不支持直接的 RPC | 网络数据交换 |
三、View 的事件体系
1.View 的基础知识
(1)View 的位置参数
这些坐标是相对于父容器的。
top、left:View 原始左上角坐标,平移过程中不变。
平移时以下4 个参数会变化:
x、y:View 左上角坐标。
translationX、translationY:View 左上角相对于父容器的偏移量,默认值是 0。
<span style="font-size:14px;"><span style="font-size:14px;">x = left + tanslationX; y = top + translationY;</span></span>
(2)
① MotionEvent
- ACTION_DOWN
- ACTON_MOVE
- ACTION_UP
<span style="font-size:14px;"><span style="font-size:14px;">getX / getY:相对于当前 View 左上角的 x 和 y 坐标; getRawX / getRawY:相对于手机屏幕左上角的 x 和 y;</span></span>
② TouchSlop
是系统所能识别出的被认为是滑动的最小距离。(当手指在屏幕上滑动时,2 次滑动之间的距离小于这个常量的话,系统就不认为这是在进行滑动操作)
这是常量,和设备有关。
获取方式:
<span style="font-size:14px;"><span style="font-size:14px;">ViewConfiguration.get(getContext()).getScaledTouchSlop();</span></span>
(3)
VelocityTracker 速度追踪
① 在 View 的 onTouchEvent 方法中追踪当前单击事件的速度:
<span style="font-size:14px;"><span style="font-size:14px;">VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event);</span></span>
② 获取速度:
<span style="font-size:14px;"><span style="font-size:14px;">velocityTracker.computeCurrentVelocity(1000);//计算速度:在一段时间内手指所划过的像素数,如 1000ms int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity();</span></span>
③ 最后,不需要使用时,应用 clear 方法来重置并回收内存:
<span style="font-size:14px;"><span style="font-size:14px;">velocityTracker.clear(); velocityTracker.recycle();</span></span>
GestureDetector 手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
① 创建 GestureDetector 对象并实现 OnGestureListener 接口:
<span style="font-size:14px;"><span style="font-size:14px;">GestureDetector mGestureDetector = new GestureDetector(this); //解决长按屏幕后无法拖动的现象 mGestureDetector.setIsLongpressEnabled(false);</span></span>
② 接管目标 View 的 onTouchEvent 方法,在待监听 View 的 onTouchEvent 方法中添加如下实现:
<span style="font-size:14px;"><span style="font-size:14px;">boolean consume = mGestureDetector.onTouchEvent(event); return consume;</span></span>
完成上述 2 步后就可以有选择地实现 OnGestureListener 和 OnDoubleTapListener 中的方法了。
Scroller 弹性滑动对象
当使用 View 的 scrollTo / scrollBy 方法滑动时,过程是瞬间完成的。
可使用 Scroller 来实现有过渡效果的滑动,是在一定时间间隔内完成的。
Scroller 本身无法让 View 弹性滑动,需要和 View 的 computeScroll 方法配合使用。
2. View 的滑动
(1)scrollTo / scrollBy —— 对 View 内容的滑动
只能改变 View 中内容的位置,而不能改变 View 在布局中的位置(即滑动 View 里的内容,而非 View 本身)。
不影响内部元素的单击事件。
View 内部 2 个属性 mScrollX 和 mScrollY可以由 getScrollX() 和 getScrollY() 得到。
在滑动过程中,
<span style="font-size:14px;"><span style="font-size:14px;">mScrollX = View 左边缘 - View 内容左边缘; mScrollY = View 上边缘 - View 内容上边缘;</span></span>
即从右向左滑动 mScrollX 为正,从下往上滑动 mScrollY 为正。
(2)使用动画 —— 操作简单,适用于没有交互的 View 和实现复杂的动画效果
View 动画(xml)是对 View 的影像做操作,它并不能真正改变 View 的位置参数,包括宽 / 高,故会影响 View 的单击等交互事件。若希望动画后的位置状态得以保留还必须将 fillAfter 属性设置为 true。
使用属性动画(Java 代码)不存在此问题。
(3)改变布局参数 LayoutParams —— 操作稍复杂,适用于有交互的 View
适用于有交互性的 View。
①
<span style="font-size:14px;"><span style="font-size:14px;">MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams(); params.width += 100; params.leftMargin += 100; btn.requestLayout(); //或 btn.setLayoutParams(params);</span></span>
②
在 Button 左边放一个空的 View,默认宽度为 0,当需要将 Button 右移时,可以重新设置空 View 的宽度即可。
3. 弹性滑动
(1)Scroller
(2)动画
动画本身就是一种渐进的过程。
(3)延时策略
通过发送一系列延时消息从而达到一种渐进式的效果。
① Handler 或 View 的 postDelayed 方法,延时发送一个消息,在消息中来进行 View 的滑动,若接连不断地发送这种延时消息,就可以实现弹性滑动的效果。
② 线程的 sleep 方法,在 while 中不断地滑动 View 和 sleep 来实现。
4. View 的事件分发机制
(1)MotionEvent 点击事件的传递规则
① public boolean dispatchTouchEvent(MotionEvent ev)
进行事件的分发。若事件能传递给当前 View,那么此方法一定会被调用,返回结果表示是否消耗当前事件。
② public boolean onInterceptTouchEvent(MotionEvent ev)
在 ① 内部调用,用来判断是否拦截某个事件。若当前 View 拦截了某个事件,则在同一个事件序列当中,此方法不会再次被调用,返回结果表示是否拦截当前事件。
③ public boolean onTouchEvent(MotionEvent ev)
在 ① 内部调用,用来处理点击事件,返回结果表示是否消耗当前事件。若不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
三者关系可用如下伪代码表示:
<span style="font-size:14px;"><span style="font-size:14px;">public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }</span></span>
点击事件优先级:
OnTouchListener 的 onTouch → onTouchEvent → OnClickListener 的
onClick。
点击事件的传递过程:
Activity → Window → View。
若 View 的 onTouchEvent 返回 false,那么其父容器的 onTouchEvent 将会被调用,依此类推。若所有元素都不处理这个事件,则会最终传递给 Activity,即 Activity 的 onTouchEvent 会被调用。
结论:
(1)同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中产生的一系列事件。以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
(2)正常情况下,一个事件序列只能被一个 View 拦截消耗。但 View 也可以将本该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理。
(3)某个 View 一旦决定拦截,那么这个事件序列都只能由它来处理,并且它的 onInterceptTouchEvent 不会再被调用。
(4)某个 View 一旦开始处理事件,若它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不会再交给它来处理,且事件将重新交给它的父元素去处理,即父元素的 onTouchEvent 会被调用。
(5)若 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 不会被调用,且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
(6)ViewGroup 默认不拦截任何事件。
(7)View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,则会调用 onTouchEvent。
(8)View 的onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false)。
(9)View 的 enable 属性不影响 onTouchEvent 的默认返回值,只要它的 clickable 或 longClickable 有一个为 true,则默认返回 true。
(10)onClick 会发生的前提是当前 View 是可点击的,且收到了 down 和 up 事件。
(11)事件传递过程是由外向内的,即父元素 → 子元素。通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 down 事件除外。
5. View 滑动冲突
在界面中只要内外两层同时可以滑动,就会产生滑动冲突。
(1)外部拦截法(推荐)
符合点击事件的分发机制,重写父容器的 onInterceptTouchEvent 即可。
<span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; //父容器必须返回 false,即不拦截 ACTION_DOWN 事件,否则后续的 MOVE 和 UP 都会直接由父容器处理,而不会传递给子元素了。 break; case MotionEvent.ACTION_MOVE: if (父容器需要当前点击事件) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; //UP 事件没多大意义,必须返回 false。 break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }</span>
(2)内部拦截法
父容器不拦截任何事件,所有事件都传递给子元素,若子元素需要此事件就直接消耗掉,否则就交由父容器处理。
不符合点击事件的分发机制,需配合 requestDisallowInterceptTouchEvent 方法,重写子元素的 dispatchTouchEvent 。
<span style="font-size:14px;">public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要当前点击事件) { parent.requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); }</span>
父元素要默认拦截除了 ACTION_DOWN 以外的其他事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false) 时,父元素才能继续拦截所需的事件。因为 ACTION_DOWN 不受 FLAG_DISALLOW_INTERCEPT 标记位的控制,故一旦父容器拦截
ACTION_DOWN,那么所有的事件都无法传到子元素了。父元素修改如下:
<span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } }</span>
四、View 的工作原理
1. 初识 ViewRoot 和 DecorView
ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建
ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
View 绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout、draw 三个过程才能最终将一个 View 绘制出来。
- measure:测量 View 的宽和高,Measure 完成以后,可以通过 getMeasuredWidth 和 getMeasuredHeight 获取 View 测量后的宽 / 高,在几乎所有的情况下它都等同于 View 的最终的宽 / 高。特殊情况除外。
- layout:确定 View 在父容器中的放置位置,即 View 的四个顶点的坐标和实际的 View 宽 / 高,完成后可以通过 getTop、getBottom、getLeft、getRight获取四个顶点的位置,getWidth 和 getHeight 获取最终宽 / 高。
- draw:负责将 View 绘制在屏幕上,只有 draw 完成后 View 的内容才能显示在屏幕上。
DecorView 作为顶级 View,其实是一个 FrameLayout,一般包含一个竖直方向的 LinearLayout,这个 LinearLayout 包含上下 2 部分:标题栏和内容栏。在 Activity 中通过 setContentVIew 设置的便是内容栏 FrameLayout
的布局,内容栏的 id 是 content。View 层的事件都先经过 DecorView,然后才传递给我们的 View。
<span style="font-size:14px;"><span style="font-size:14px;">ViewGroup content = (ViewGroup) findViewById(android.id.content); //获取 content content.getChildAt(0); //获取我们设置的 View</span></span>
2. MeasureSpec
(1)MeasureSpec 很大程度上决定了一个 View 的尺寸规格,这个过程还受父容器的影响。测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,再根据这个 MeasureSpec 来测量出 View 的宽 / 高。
MeasureSpec 代表一个 32 位 int 值,高 2 位代表 SpecMode 测量模式,低 30 位代表 SpecSize 某种测量模式下的规格大小。MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,且提供了打包和解包方法。SpecMode 和 SpecSize 也是 int 值。
SpecMode 有三类:
- UNSPECIFIED
父容器不对 VIew 有任何限制,要多大给多大,一般用于系统内部,表示一种测量的状态。
- EXACTLY
父容器已经检测出 View 所需要的精确大小,此时 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。
- AT_MOST
父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,对应于 LayoutParams 的 wrap_content。
(2)MeasureSpec 和 LayoutParams 的对应关系
DecorView:其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 共同确定。
普通 View:其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 共同确定。MeasureSpec 一旦确定后,onMeasure 中就可以确定 View 的测量宽 / 高。
DecorView 的 MeasureSpec 的产生过程遵守如下规则:
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但不能超过窗口的大小。
- 固定大小:精确模式,大小为 LayoutParams 中指定的大小。
子元素可用的大小为父容器的尺寸减去 padding。
3. View 的工作流程
(1)measure 过程
由 View 的 measure() 方法完成,此是一个 final 类型方法,子类不能重写,在 measure() 中会调用 View 的 onMeasure() 方法。
关于 getSuggestedMinimumWidth() 方法:
若 View 没有设置背景,那么返回 android:minWidth 的值(可以为 0);若 View 设置了背景,则返回 android:minWidth 和 背景的最小宽度 两者中的最大值。getSuggestedMinimumWidth 的返回值就是
View 在 UNSPECIFIED 情况下的测量宽。
直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。解决如下:
<span style="font-size:14px;">/** * 只需给 View 指定一个默认的内部 宽/高(mWidth 和 mHeight),并在 wrap_content 时设置此宽/高即可。 * 对于非 wrap_content 情形,沿用系统的测量值即可。 */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); } }</span>
问题:若想在 Activity 一启动的时候就做一件任务,但是这一任务需要获取某个 View 的宽/高,因为 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,因此无法保证 Activity 执行了 onCreate、onStart、onResume 时某个 View 已经测量完毕了,若未测量完毕,则获得的宽/高就是0。
解决:
①Activity/View#onWindowFocusChanged。
onWindowFocusChanged 的含义:View 已经初始化完毕了,宽/高已经准备好了,这时可以获取。但是 onWindowFocusChanged 会被调用多次,当
Activity 的窗口得到焦点和失去焦点时均会被调用一次。
public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
②view.post(runnable)。
通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候,View 也已经初始化好了。
protected void onStart() { super.onStart(); view.post(new Runnable() { @override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
③ViewTreeObserver。
使用 ViewTreeObserver 的众多回调可以完成这个功能。
如使用 OnGlobalLayoutListener 这个接口,当 View 树的状态发生改变或 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调,这是一个获取
View 的宽/高的好时机。伴随着 View 树的状态改变等,onGlobalLayout 会被调用多次。
protected void onStart() { super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
④view.measure(int widthMeasureSpec, int heightMeasureSpec)。
手动对 View 进行 measure。
(2)layout 过程
layout 方法确定 View 本身的位置,onLayout 确定所有子元素的位置。
(3)draw 过程
①绘制背景 background.draw(canvas)
②绘制自己 (onDraw)
③绘制 children (dispatchDraw)
④绘制装饰(onDrawScrollBars)
View 有一个特殊方法 setWillNotDraw:若一个 View 不需要绘制任何内容,则设置这个标记位为 true 以后,系统会进行相应的优化。View 默认没有启用这个标记位,但是 ViewGroup 默认启用此标记位。当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位便于系统进行后续优化。当明确知道一个 VIewGroup 需要通过 onDraw 来绘制内容时,需要显式地关闭
WILL_NOT_DRAW 这个标记位。
4.自定义 View
============================================================================================
《Android 应用程序开发权威指南》
一、基础
-
管理器 功能 LocationManager 和设备上的基于位置的服务进行交互 ViewManager,WindowManager 负责显示界面以及设备相关的用户界面的基础 AccessibilityManager 负责辅助事件,提供对物理损伤的用户的设备支持 ClipboardManager 提供了访问设备全局剪贴板的能力,可以剪切和复制内容 DownloadManager 作为系统服务,负责 HTTP 的后台下载 FragmentManager 管理一个 Activity 的 Fragment AudioManager 提供了音频和振铃控制的访问 -
文件 功能 AndroidManifest.xml 应用的核心配置文件。定义了应用程序的功能和权限,以及如何运行。 ic_launcher-web.png 一张 32 位的 512*512 大小的高分辨率图标,用来在 Google Play 商店中显示。该图大小不能超过 1024KB。 proguard-project.txt Android IDE 和 ProGuard 使用的编译文件。可以通过编辑该文件来配置代码优化选项,以及发布版本的混淆设置。 project.properties Android IDE 中使用的编译文件。定义了应用程序的构建目标,以及其他编译系统选项。不要编辑这个文件 /src 必须的文件夹,包含所有的源代码 /gen 必须的文件夹,包含所有的自动生成的文件。 /gen/.../BuildConfig.java 调试应用程序时,该源文件自动生成。不要编辑 /gen/.../R.java 自动生成的资源管理的源文件。不要编辑 /assets 必须的文件夹。包含了项目中未编译的资源文件,一些你不想作为应用程序资源管理的应用程序数据(文件、目录) -
术语 描述 Context(上下文) 是 Android 应用的中央指挥中心。大部分应用特定的功能可以通过上下文访问或引用。Context 类是任何 Android 应用的基本构建模块,提供了访问应用程序范围的功能,譬如应用程序的私有文件、设备资源,以及整个系统的服务。应用程序的 Context 对象会被实例化为一个 Application 对象。 Activity(活动) 一个 Activity 类是 Context 类的子类,因此它也拥有 Context 类的所有功能。 Fragment(碎片) 一个活动有一个独特的任务或目的,但它可以进一步组件化,每一个组件被称为碎片。Fragment 类往往被用来组织活动的功能,从而允许在不同的屏幕大小、方向和纵横比上提供更灵活的用户体验。碎片常常用来在由多个 Activity 类组成的不同的屏幕上,使用相同的代码和屏幕逻辑放置相同的用户界面。 Intent(意图) Android 操作系统使用异步的消息传递机制,将任务匹配到合适的 Activity。每一个请求被打包成一个意图。使用 Intent 类是应用程序组件如活动和服务之间通信的主要方法。 Service(服务) 不需要用户交互的任务可以封装成一个服务。当需要处理耗时任务或需要定时处理时使用服务,用来处理后台操作。继承自 Context 类。 - 应用程序 Context
因为 Activity 类是由 Context 类派生的,故有时可以使用它而不是显示地获取应用程序 Context。但不要在任何情况下都使用 Activity Context,因为可能会导致内存泄漏。
- 获取应用程序资源,如字符串、图形、xml 文件。
getResources()
- 访问应用程序首选项
getSharedPreferences()
- 管理私有的应用程序文件和目录
- 获取未编译的应用程序资产
getAssets()
- 访问系统服务
- 管理自由的应用程序数据库(SQLite)
- 以应用程序权限工作
- 获取应用程序资源,如字符串、图形、xml 文件。
- Activity 生命周期
onCreate()
初始化静态 Activity 数据onResume()
初始化及取回 Activity 数据onPause()
停止、保存和释放 Activity 数据,保存重要数据到用久存储,使用 onSaveInstanceState()
保存一些可以从当前屏幕快速恢复的数据或某些不重要的信息(如 未提交的表单数据或任何其他减少用户麻烦的状态信息)。停止任何声音、视频、动画,也必须停用资源如数据库游标对象 或 其他 Activity 终止时应该清理的对象。onPause() 可能是 Activity 进入后台时最后用来清理或释放不需要的资源的机会,需要在这里保存任何未提交的数据。
调用 onPause() 后,系统保留杀死任何一个 Activity 而没有进一步通知的权利。
Activity 需要在 onPause() 方法中执行快速的代码,因为只有 onPause() 方法返回后,新的前台 Activity 才会启动。
一般来说,任何在 onResume() 方法中获取的资源和数据应该在 onPause() 方法中释放,否则当进程终止后,这些资源可能无法干净地释放。
避免 Activity 被杀死
内存不足时 Android 操作系统可以杀死任何暂停、停止或销毁的 Activity。这基本意味着任何没有在前台的 Activity 都会面临被关闭的可能。
若 Activity 在 onPause() 后被杀掉,那么 onStop() 和 onDestroy() 方法将不会被调用。在 onPause() 方法内更多的释放 Activity 的资源,那么 Activity 就越不太可能在后台被直接杀掉(没有其他的状态切换方法被调用)。
杀死一个 Activity 并不会导致它从 Activity 堆栈中的移除。若 Activity 实现并使用了 onSaveInstanceState() 用于自定义数据,Activity 状态将被保存到 Bundle 对象中(虽然一些 View 数据将会被自动保存)。当用户返回到 Activity 后,onCreate() 方法会被再次调用,这次会有一个有效的 Bundle 对象作为方法参数。
onDestroy()
销毁静态 Activity 数据当 Activity 通过正常的操作被销毁,onDestroy() 方法将会被调用。
onDestroy() 会在 2 种情况下被调用:Activity 自己完成了它的生命周期,或因为资源问题,Activity 被 Android 操作系统杀掉,但仍有足够的时间从容销毁 Activity(与不调用 onDestroy() 方法直接终止 Activity 不同)。
若 Activity 是被 Android 操作系统杀掉的,isFinishing() 方法会返回 false。该方法在 onPause() 中十分有用,可以知道 Activity 是否能够恢复。
- AndroidManifest.xml 清单文件
作用:
- Android 操作系统使用清单文件来安装、更新和运行应用程序包
- 显示应用程序的详细信息,如名称、描述、图标
- 指定应用的系统需求,包括对 Android SDK 的支持、设备配置的需求(如方向键),以及应用依赖的平台功能(如多点触摸)
- 以市场过滤为目的,指定应用的哪些功能是必须的
- 注册应用的 Activity,并指定如何启动
- 管理应用程序的权限
- 配置其他高级的应用组件配置详细信息,包括定义 Service、Broadcast Receiver 及 Content Provider
- 为你的 Activity、Service 及 Broadcast Receiver 指定 Intent 过滤器
- 为应用测试开启应用设置,如调试和配置仪器
设置应用程序的系统需求:
<uses-feature>
标签用于指定应用需要哪些 Android 功能才能正常运行。这些设置只供参考不会强制使用。当使用<uses-feature>
标签时,可以指定android:required
的可选属性,并设置为
true 或 false,这个可以用于配置 Google Play 商店中的过滤。若该值为 true,Google Play 将只会在具有特定硬件或软件功能的设备上显示你的应用程序(如摄像头)。若应用程序需要多个功能,则必须为每个功能创建一个<uses-feature>
标签。若应用正常运行时并不需要一个特定的功能,与其在应用商店内过滤并限制特定的设备,还可以使用
getPackageManager().hasSystemFeature()
在运行时检查特定的设备功能,并在用户的设备上支持该功能时才允许特定的功能,这样可以最大化安装和使用你应用的人群。<uses-sdk>
标签用于指定应用程序支持的 Android 平台版本。应用商店会根据应用的清单文件的<uses-sdk>
标签等的设置来为给定的用户过滤应用。忽略使用该标签将会在编译环境中产生一个警告信息。<uses-configuration>
标签用于指定应用程序支持的硬件或软件的输入方法。有 5 个方向的配置属性:硬件键盘和键盘类型;方向设备如方向键、轨迹球和滚轮;触摸屏的设置。若应用程序支持多种输入配置,则必须有多个<uses-configuration>
标签。<supports-screens>
标签用于指定应用支持的 Android 屏幕类型。- <application> 内的
<uses-library>
标签用于注册应用中链接到的外部库。 <supports-gl-texture>
用于指定应用支持的 GL 材质的压缩格式。使用图形库的应用使用该标签,并用于兼容可以支持指定压缩格式的设备。<application>
标签属性中设置应用程序范围的主题。<instrumentation>
设置单元测试功能。<activity-alias>
为 Activity 起别名。<receiver>
注册 Broadcast Receivers。<provider>
注册 Content Provider,使用<grant-uri-permission>
和<path-permission>
管理
Content Provider 的权限。<meta-data>
包含应用的 Activity、Service、Receiver 组件注册的其他数据。
- 管理资源
- 所有 Android 应用程序由 2 部分组成:
功能部分——代码指令(程序运行的任何算法)
数据部分——资源(文本字符串、样式主题、尺寸、图片图标、音视频文件等)
- 默认 Android 资源目录,所有资源必须存放在项目的 /res 目录下的指定子目录,且目录名必须小写。
资源子目录 内容 /res/drawable-*/ 图形资源 /res/layout/ 用户界面资源 /res/menu/ 菜单资源,用于显示 Activity 中的选项或操作 /res/values/ 简单的数据,如字符串、样式主题、尺寸资源 /res/values-sw*/ 覆盖默认的尺寸资源 /res/values-v*/ 较新 API 自定义的样式和主题资源 - 常见的资源类型及存储结构
资源类型 所需目录 建议文件名 XML 标签 字符串 /values/ strings.xml <string>
字符串复数形式 /values/ strings.xml <plurals>, <item>
字符串数组 /values/ strings.xml 或 arrays.xml <string-array>, <item>
布尔类型 /values/ bools.xml <bool>
颜色 /values/ colors.xml <color>
颜色状态列表 /color/ 包括 buttonstates.xml, indicators.xml <selector>, <item>
尺寸 /values/ dimens.xml <dimen>
ID /values/ ids.xml <item>
整型 /values/ integers.xml <integer>
整型数组 /values/ integers.xml <integer-array>
混合类型数组 /values/ arrays.xml <array>, <item>
简单可绘制图形(可打印) /values/ drawables.xml <drawable>
XML 文件定义的图形(如形状) /drawable/ 包括 icon.png, logo.jpg 支持的图形文件或可绘制图形 补间动画 /anim/ 包括 fadesequence.xml, spinsequence.xml <set>, <alpha>, <scale>,<translate>, <rotate>
属性动画 /animator/ mypropanims.xml <set>, <objectAnimator>, <valueAnimator>
帧动画 /drawable/ 包括 sequence1.xml, sequence2.xml <animation-list>, <item>
菜单 /menu/ 包括 mainmenu.xml, helpmenu.xml <menu>
XML 文件 /xml/ 包括 data.xml, data2.xml 开发者定义 原始文件 /raw/ 包括 jingle.mp3, video.mp4, text.txt 开发者定义 布局 /layout/ 包括 main.xml, help.xml 多样,但必须是布局类型 样式和主题 /values/ styles.xml, themes.xml <style>
- 使用字符串资源
- 所有包含撇号及单引号的字符串需要被转义 "\" 或被双引号包裹
- 可使用 HTML 样式的属性(粗体<b>、斜体<i>、下划线<u>标签)
- 忽略格式:
String s = getResources().getString(R.string.hello);
- 保留字符串格式: ①
CharSequence cs = getResourece().getText(R.string.hello);
②使用
TextUtils 的 htmlEncode()
- 所有 Android 应用程序由 2 部分组成:
- TexdView 文本中创建上下文链接:autoLink 属性
值 描述 note 禁用所有链接 web 允许 web 网页的 URL 链接 email 允许电子邮件地址链接,并在邮件客户端填写收件人 phone 允许电话号码链接,可以在拨号器应用中填写电话号码来拨打 map 允许街道地址的链接,可以在地图应用中显示位置 all 允许所有类型的链接 开启 autoLink 功能依赖于 Android SDK 中的各种类型的检测。有时候也可能链接不正确或产生误导。
- EditText
- 输入过滤器限制输入
使用
setFilters()
来设置 InputFilter 限制用户输入,InputFilter 接口包含一个filter()
方法。可以实现
InputFilter 接口来创建自定义的过滤器。setFilters()
的参数是 InputFilter 对象的数组,这对于组合多个过滤器是非常有用的。 - 自动完成
AutoCompleteTextView
:基于用户输入的内容来填写整个文本。- 允许用户输入字符串列表,每一个都具有自动完成功能。这些字符串都要以某种方式分隔,提供给
MultiAutoCompleteTextView
对象的Tokenizer
来处理。这对于指定通用标签等的列表有所帮助。也可以自己实现MultiAutoCompleteTextView.Tokenizer
接口,内置逗号分隔符CommaTokenizer()
。
- 输入过滤器限制输入
<span style="font-size:14px;"><span style="font-size:14px;"> <span> </span> android:pathPattern="string"</span></span>