通过前两篇的分析,我们已经知道了LauncherModel的初始化及工作流程,如果您还不熟悉的话请看前两篇博文
android M Launcher之LauncherModel (一)
android M Launcher之LauncherModel (二)
了解了LauncherModel的工作过程后,我们继续来学习LauncherModel中提供的一些工具,从而了解Google工程师在自家系上怎么开发的。
1、线程等待waitForIdle
我们在这个系列第二篇开始讲LoaderTask的run方法时,在加载绑定桌面数据之后,加载与绑定应用程序之前,有一个等待动作即调用了waitForIdle方法,为什么要去调这个方法呢,大家都知道在绑定桌面数据时我们是去UI线程中处理的,接触过应用开发的都知道UI线程正常情况下是不能阻塞的,否则有可能产生ANR,这将严重影响用户体验。所有这里LoaderTask在将结果发送给UI线程之后,为了保证界面绑定任务可以高效的完成,往往会将自己的任务暂停下来,等待UI线程处理完成。不知道大家有没有这样设计过,反正我是没有。从这儿也可以看出Google工程师是比较严谨的。
那我们就来分析下waitForIdle函数是怎么实现的。
首先,创建一个UI线程闲时执行的任务,这个任务负责设置某些关键的控制标志,并将其通过PostIdle方法加入处理器的消息队列中。
mHandler.postIdle(new Runnable() {
public void run() {
synchronized (LoaderTask.this) {
mLoadAndBindStepFinished = true;
if (DEBUG_LOADERS) {
Log.d(TAG, "done with previous binding step");
}
LoaderTask.this.notify();
}
}
});
这样一来,只有UI线程闲置下来的时候,这里定义的任务才会得到执行,这也就说明了界面已经刷新完成。而一旦这个任务得到执行,就会将mLoadAndBindStepFinished 置为true,以控制即将来临的有条件的无限等待。
最后 设置一个有条件的无限等待,等待来自UI线程的指示。
while (!mStopped && !mLoadAndBindStepFinished) {
try {
// Just in case mFlushingWorkerThread changes but we aren't woken up,
// wait no longer than 1sec at a time
this.wait(1000);
} catch (InterruptedException ex) {
// Ignore
}
}
只要没有中断且加载没有完成这里将一直等待,知道完成,这样看还是很简单的逻辑呀,你有想到么。
2、停止加载工作stopLocked
对于一项任务,有时候我们需要停止它的工作,以保证数据或者流程的正确性。我们看下LoaderTask是怎么做的:
public void stopLocked() {
synchronized (LoaderTask.this) {
mStopped = true;
this.notify();
}
}
可以看出要停止工作只需将mStopped 置为true, 而LoaderTask在运行过程中会频繁的查询mStopped 标志,所有在开始设置之前需对LoaderTask上锁,以保证mStopped 得到正确的设定,有了上锁就必须有解锁,这就是this.notify();的作用。
3、获取通道 tryGetCallbacks
LoaderTask在执行一次加载任务的时候,都毫无意外的需要检验通往Launcher的通道是否存在,或者当前的通道是否经历过重建,如果这样的情况存在,那么LoaderTask久没有继续执行的必要了。
tryGetCallbacks工具的作用就是帮助完成通往Launcher通道的验证。
Callbacks tryGetCallbacks(Callbacks oldCallbacks) {
synchronized (mLock) {
if (mStopped) {
return null;
}
if (mCallbacks == null) {
return null;
}
final Callbacks callbacks = mCallbacks.get();
if (callbacks != oldCallbacks) {
return null;
}
if (callbacks == null) {
Log.w(TAG, "no mCallbacks");
return null;
}
return callbacks;
}
}
这里代码应该比较好明白吧。
4、桌面空间判断工具 checkItemPlacement
桌面上的每一个组件想要加载到桌面或者HotSeat,都需要确定当前的桌面或者HotSeat中是否还有足够的空间,checkItemPlacement方法用于完成这个任务。
这个方法分为几步,
获取Launcher属性及item属性
要检查需要处理的项是否有足够的空间,首先要知道当前Launcher有多少空间可以被占用,以及当前的项所处的空间情况,
LauncherAppState app = LauncherAppState.getInstance();
InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
final int countX = profile.numColumns;
final int countY = profile.numRows;
//被处理项的桌面索引
long containerIndex = item.screenId;
- 处理被处理项在所处容器的占用情况。
主要处理两种情况,一种是HotSeat容器,一种是桌面容器。
如果需要处理的是HotSeat容器 ,它需要一些判断,如下:
if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
// Return early if we detect that an item is under the hotseat button
if (mCallbacks == null ||
mCallbacks.get().isAllAppsButtonRank((int) item.screenId)) {
Log.e(TAG, "Error loading shortcut into hotseat " + item
+ " into position (" + item.screenId + ":" + item.cellX + ","
+ item.cellY + ") occupied by all apps");
return false;
}
//获取HotSeat容器中空间的占用情况,
final ItemInfo[][] hotseatItems =
occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT);
//如果被处理项要求的位置超过了HotSeat的容量,返回false
if (item.screenId >= profile.numHotseatIcons) {
Log.e(TAG, "Error loading shortcut " + item
+ " into hotseat position " + item.screenId
+ ", position out of bounds: (0 to " + (profile.numHotseatIcons - 1)
+ ")");
return false;
}
//如果HotSeat空间已经被Launcher分配
if (hotseatItems != null) {
//如果被处理的项所要求的位置已经被占用,返回分配失败
if (hotseatItems[(int) item.screenId][0] != null) {
Log.e(TAG, "Error loading shortcut into hotseat " + item
+ " into position (" + item.screenId + ":" + item.cellX + ","
+ item.cellY + ") occupied by "
+ occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT)
[(int) item.screenId][0]);
return false;
//如果该空间空闲,则将此空间分配给此项,返回分配成功
} else {
hotseatItems[(int) item.screenId][0] = item;
return true;
}
//如果HotSeat空间还没有被分配,分配空间后返回分配成功
} else {
//开辟HotSeat空间
final ItemInfo[][] items = new ItemInfo[(int) profile.numHotseatIcons][1];
items[(int) item.screenId][0] = item;
//将分配的空间加入占用列表中维护
occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, items);
return true;
}
}
如果是桌面容器
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
if (!workspaceScreens.contains((Long) item.screenId)) {
// The item has an invalid screen id.
return false;
}
}
其他情况一律返回分配成功。
- 添加占用列表
如果对应的桌面索引存在,但Launcher并没有在这个桌面页上分配空间,那么Launcher需要为此分配足够的空间:
if (!occupied.containsKey(item.screenId)) {
ItemInfo[][] items = new ItemInfo[countX + 1][countY + 1];
occupied.put(item.screenId, items);
}
- 判断快捷方式是否有足够的空间
在这里需要判断当前处理的项是否有足够的空间容纳该项,因为该项有可能是一个桌面小部件,可能占用比较大的空间。
//获取屏幕上被占用情况
final ItemInfo[][] screens = occupied.get(item.screenId);
//如果被处理的项超过范围,则返回分配失败
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
item.cellX < 0 || item.cellY < 0 ||
item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
Log.e(TAG, "Error loading shortcut " + item
+ " into cell (" + containerIndex + "-" + item.screenId + ":"
+ item.cellX + "," + item.cellY
+ ") out of screen bounds ( " + countX + "x" + countY + ")");
return false;
}
- 检查空间及屏幕占用
有了足够的空间,还需要判断该项需要占用的空间内是否还被其他桌面项(快捷方式 桌面小部件) 占用,如果该项需要的空间范围内只有一块被占用也属于分配失败。
// Check if any workspace icons overlap with each other
for (int x = item.cellX; x < (item.cellX + item.spanX); x++) {
for (int y = item.cellY; y < (item.cellY + item.spanY); y++) {
if (screens[x][y] != null) {
Log.e(TAG, "Error loading shortcut " + item
+ " into cell (" + containerIndex + "-" + item.screenId + ":"
+ x + "," + y
+ ") occupied by "
+ screens[x][y]);
return false;
}
}
}
- 让被处理的项占用它需要的空间
for (int x = item.cellX; x < (item.cellX + item.spanX); x++) {
for (int y = item.cellY; y < (item.cellY + item.spanY); y++) {
screens[x][y] = item;
}
}
6、桌面组件的排序工具sortWorkspaceItemsSpatially
在Launcher中,在绑定桌面组件之前都需要对桌面组件进行一次排序,原则上是Y轴方向从上到下,X轴从左到右
final LauncherAppState app = LauncherAppState.getInstance();
final InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
// XXX: review this
Collections.sort(workspaceItems, new Comparator<ItemInfo>() {
@Override
public int compare(ItemInfo lhs, ItemInfo rhs) {
int cellCountX = (int) profile.numColumns;
int cellCountY = (int) profile.numRows;
int screenOffset = cellCountX * cellCountY;
int containerOffset = screenOffset * (Launcher.SCREEN_COUNT + 1); // +1 hotseat
long lr = (lhs.container * containerOffset + lhs.screenId * screenOffset +
lhs.cellY * cellCountX + lhs.cellX);
long rr = (rhs.container * containerOffset + rhs.screenId * screenOffset +
rhs.cellY * cellCountX + rhs.cellX);
return (int) (lr - rr);
}
});
这个是java对一个数组的排序方式。
以上就是LoaderTask提供的一些重要工具,通过学习Google的源码我们也可以把它应用到实际的开发中去,提高自己代码的健壮性。
好了,从第一篇开始到这篇结束,我们终于把LauncherModel的整个工作流程走了一遍。以下是整个LauncherModel系列的索引
一 、LauncherModel的的实例化
二 、 LauncherModel数据的加载流程
三 、LauncherModel中一些工具的介绍