Android 4.0 Launher分析
一.整个Launcher应用的构成.
要看一个app的构成和入口,首先得了解它的AndroidManifest.xml.application标签包含以下内容:
1.Launcher 整个桌面的载体,整个应用真正的入口 .
2.wallpaperChooser 选择壁纸界面
3. RocketLauncher 好像没有用到.
4.installShorcutReceiver/uninstallShorcutReceiver接受添加/删除快捷方式的广播接收器
5.LauncherProvider 操作数据库,保存home的数据,比如桌面有那些小部件,快捷方式等.
二.Launcher 架构:
从MVC模式分析,Launcher处于controller层,LauncherMode 处于mode层,而view层是DragLayer,它是整个lanuncher布局的根.
通过launcher.xml可以看到整个布局的层次结构:
从左图可以看出Draglayer是launcher布局的根节点.
前两个imageview是divider和page_indicator.
Workspace作为子view,默认包含五个cellLayout(每屏)
Hotseat就是桌面底部应用快捷栏,与Android2.3不同的是它也是个cellLayout,可以自定义里面的快捷方式.
SearchDropTargetBar 就是桌面固定不动的搜索框,以及删除图标时的删除区。
Android使用向导
应用程序界面.
三.启动流程分析.
启动LauncherActivity 进入onCreate()
执行LauncherApplicationapp = ((LauncherApplication)getApplication());
mModel = app.setLauncher(this);
初始化LauncherMode对象.
---->整个应用真正的入口,LaucherApplicationonCreate();
创建mModel =new LauncherModel(this,mIconCache);LauncherMode继承自 BroadcastReceiver.注册监听应用应用程序的安装,卸载等等。
LauncherApplication 注册ContentResolver监 听favorites表的变化。
private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
static {
sWorkerThread.start();
}
private static final Handler sWorker = new Handler(sWorkerThread.getLooper());
mLoaderTask = new LoaderTask(mApp, isLaunching);
sWorkerThread.setPriority(Thread.NORM_PRIORITY);
sWorker.post(mLoaderTask);
private class LoaderTask implements Runnable {
------>launcher.java 初始化Widget框架相关类
mAppWidgetManager = AppWidgetManager.getInstance(this);
mAppWidgetHost =new LauncherAppWidgetHost(this,APPWIDGET_HOST_ID);
mAppWidgetHost.startListening();作为Appwidget Service和 app交互的桥梁。
------>setupViews()初始化所有view对象
mDragLayer.setup(this,dragController);配置DragController对象,DrayLayer的一些事件处理,会调用DragController中的方法处理。如:onInterceptTouchEvent,onTouchEvent.
mWorkspace.setHapticFeedbackEnabled(false);
mWorkspace.setOnLongClickListener(this);
mWorkspace.setup(dragController);
dragController.addDragListener(mWorkspace);
初始化workspace信息,实现DragListener,赋给 dragController,workspace中view拖动到不同状态的具体处理,会调用workspace中的方法。如onStartDrag,onDrop等。
mAppsCustomizeContent.setup(this,dragController);初始化应用主界面。
初始化dragController信息,如:dragController.setDragScoller(mWorkspace);
初始化桌面搜索和删除view。mSearchDropTargetBar.setup(this,dragController);
-------->mModel.startLoader(this, true);开始加载launcher数据。
进入LauncherModel中 startLoader,加载默认的AllAppsList.loadTopPackage(context);
mLoaderTask= new LoaderTask(context, isLaunching);sWorker.post(mLoaderTask);
通过Task加载Launcher的内容。其run中会调用一下方法加载和绑定workpace和AllApp内容 onlyLoadWorkspace();onlyLoadAllApps();bindWorkspace();onlyBindAllApps();
------>LoadWorkspace()加载桌面上的内容。
sWorkspaceItems.clear();
sAppWidgets.clear();
sFolders.clear();
sItemsIdMap.clear();
sDbIconCache.clear();
清空各个list,每个放的信息显而易见,如AppWidgets 小部件信息。 contentResolver.query(LauncherSettings.Favorites.CONTENT_URI,...);
查询Favorites表中的信息,其中存放的信息包括桌面上的小部件,快捷方式,文件夹等, 分类初始化后,放入list中以备后用。
------>loadApps加载应用程序界面信息
packageManager.queryIntentActivities(mainIntent,0);查询所有app信息,每个app对应一个ResolveInfo对象。
mAllAppsList.add(newApplicationInfo(packageManager, apps.get(i),
mIconCache,mLabelCache));
根据app的信息创建ApplicationInfo对象,并放入mAllAppsList中以备后用。
------>bindWorkspace()绑定桌面上的小部件,快捷方式等。
首先通过post一Runnable()调用为callbacks.startBinding();callbacks其实是Launcher
在launcherApplication调用mModel.initialize(launcher);
创建多个new Runnable(),通过Handler的post执行callbacks.bindItems, callbacks.bindFolders,callbacks.bindAppWidget,调用Launcher中对应的方法。
Launcher中绑定内容以bindAppWidget(...)为例,根据widget的信息
mAppWidgetHost.createView(this,appWidgetId, appWidgetInfo);创建view,通过
workspace.addInScreen添加到workpace中。
和2.3不同的是addInScreen中加入了一下判断,
if(container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
layout= mLauncher.getHotseat().getLayout();
child.setOnKeyListener(null);
//Hide folder title in the hotseat
if (child instanceof FolderIcon) {
((FolderIcon)child).setTextVisible(false);
}
if(screen < 0) {
screen= mLauncher.getHotseat().getOrderInHotseat(x, y);
} else{
x= mLauncher.getHotseat().getCellXFromOrder(screen);
y= mLauncher.getHotseat().getCellYFromOrder(screen);
}
}
这是由于,4.0的hostseat可以自己定制,把app快捷方式突入即可。
------>接下来Launcher调用onResume()
if(mRestoring || mOnResumeNeedsLoad) {//如果Launcher回到前台,需要重新加载,执行
mWorkspaceLoading= true;
mModel.startLoader(this,true);
mRestoring= false;
mOnResumeNeedsLoad= false;
}
mAppsCustomizeTabHost.onResume();唤醒应用程序界面。
四.功能能点分析
1.托放功能分析
Luancher有一个相对比较复杂的功能就是拖放功能,要深入了解launcher,深入理解拖放功能是有必要的.
1.首先直观感受什么时候开始拖放?我们长按桌面一个应用图标或者控件的时候拖放就开始了,包括在应用程序界面中长按应用图标,下面就是我截取的拖放开始的代码调用堆栈
1.
2. at com.android.launcher2.DragController.startDrag
3. at com.android.launcher2.Workspace.startDrag
4. at com.android.launcher2.Launcher.onLongClick
5. at android.view.View.performLongClick
6. at android.widget.TextView.performLongClick
7. at android.view.View$CheckForLongPress.run
8. at android.os.Handler.handleCallback
9. at android.os.Handler.dispatchMessage
10. at android.os.Looper.loop
桌面应用图标由Launcher.onLongClick负责监听处理,插入断点debug进入onLongclick函数
if (!(vinstanceof CellLayout)) {
v = (View)v.getParent().getParent();
}
CellLayout.CellInfolongClickCellInfo = (CellLayout.CellInfo) v.getTag();
//获得长按的item的tag
...
else {
if (!(itemUnderLongClickinstanceof Folder)) {
…
// User long pressed on an item
mWorkspace.startDrag(longClickCellInfo);
首先是获取被拖动的对象v.getTag()( v : worksapce.celllayout,hotseat),Tag什么时候被设置进去的了.相应onTouch之前,
onInterceptTouchEvent的处理从DargLayer,到workspace,再到CellLayout中的onInterceptTouchEven t.
@Override
publicbooleanonInterceptTouchEvent(MotionEvent ev) {
// First we clear the tag toensure that on every touch down we start with a fresh slate,
// even in the case where wereturn early. Not clearing here was causing bugs whereby on
// long-press we'd end up pickingup an item from a previous drag operation.
...
if (action == MotionEvent.ACTION_DOWN) {
clearTagCellInfo();
}
if (mInterceptTouchListener !=null &&mInterceptTouchListener.onTouch(this, ev)) {
//mInterceptTouchListener是workspace中的。
returntrue;
}
if (action == MotionEvent.ACTION_DOWN) {
setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY());
}
returnfalse;
}
clearTagCellInfo------》初始化tag信息。
privatevoid clearTagCellInfo() {
final CellInfo cellInfo =mCellInfo;
cellInfo.cell =null;
cellInfo.cellX = -1;
cellInfo.cellY = -1;
cellInfo.spanX = 0;
cellInfo.spanY = 0;
setTag(cellInfo);
}
setTagToCellInfoForPoint-------》
final nt count =mChildren.getChildCount();
...
boolean found =false;
for (int i = count - 1; i >= 0; i--) {
final View child = mShortcutsAndWidgets.getChildAt(i);
child.getHitRect(frame);
…
// The child hitrectis relative to the CellLayoutChildren parent, so we need to
// offset that by thisCellLayout's padding to test an (x,y) point that is relative
// to this view.
frame.offset(mPaddingLeft, mPaddingTop);
if (frame.contains(x, y)) {
cellInfo.cell = child;
cellInfo.cellX = lp.cellX;
cellInfo.cellY = lp.cellY;
cellInfo.spanX = lp.cellHSpan;
cellInfo.spanY = lp.cellVSpan;
found = true;
break;
}
...
if (!found) {
finalint cellXY[] =mTmpXY;
pointToCellExact(x, y, cellXY);
cellInfo.cell =null;
cellInfo.cellX = cellXY[0];
cellInfo.cellY = cellXY[1];
cellInfo.spanX = 1;
cellInfo.spanY = 1;
}
setTag(cellInfo);
看了上面代码知道,当开始点击桌面时,celllayout会先清楚tag,然后调用就会根据点击区域去查找在该区域是否有child存在,若有把它设置为tag.cell,后面在开始拖放时launcher.onlongclick中对tag进行处理.
这个理顺了,再深入到workspace.startDrag函数.
child.setVisibility(GONE);//隐藏真是item.
// The outline is usedto visualize where the item will land if dropped
mDragOutline =createDragOutline(child, canvas, bitmapPadding);
//创建拖动桌面item时,item下面可放置位置的轮廓。
workspace.startDrag调用DragController.startDrag去处理拖放
mDragController.startDrag(child, this,child.getTag(), DragController.DRAG_ACTION_MOVE);
再分析一下上面调用的几个参数
child = tag.cell ->the longclick icon
this = workspace
child.getTag()是什么呢?在什么时候被设置?再仔细回顾原来launcher加载过程代码,在launcher.createShortcut中它被设置了:注意下面我代码中的注释
View createShortcut(int layoutResId, ViewGroupparent, ShortcutInfo info) {
BubbleTextView favorite =(BubbleTextView)mInflater.inflate(layoutResId, parent,false);
favorite.applyFromShortcutInfo(info, mIconCache);
favorite.setOnClickListener(this);
return favorite;
}
进入 applyFromShortcutInfo我们会看到:
publicvoid applyFromShortcutInfo(ShortcutInfo info, IconCacheiconCache) {
Bitmap b =info.getIcon(iconCache);
setCompoundDrawablesWithIntrinsicBounds(null,new FastBitmapDrawable(b),//图标
null,null);
setText(info.title);//图标下的字
setTag(info);//设置item信息
}
继续深入解读DragController.startDrag函数
publicvoid startDrag(Bitmap b,int dragLayerX,int dragLayerY,
DragSource source, Object dragInfo, int dragAction, Point dragOffset, RectdragRegion) {
// Hide soft keyboard, if visible
if (mInputMethodManager ==null) {
mInputMethodManager = (InputMethodManager)
mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE);
}
mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
for (DragListener listener :mListeners) {
listener.onDragStart(source,dragInfo, dragAction);
}
finalint registrationX =mMotionDownX – dragLayerX;
//记住手指点击位置与屏幕左上角位置偏差
finalint registrationY =mMotionDownY - dragLayerY;
finalint dragRegionLeft = dragRegion ==null ? 0 : dragRegion.left;
finalint dragRegionTop = dragRegion ==null ? 0 : dragRegion.top;
//手指触点相对draglayer左上角的偏移量。
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
mVibrator.vibrate(VIBRATE_DURATION);
final DragView dragView =mDragObject.dragView =new DragView(mLauncher, b,registrationX, registrationY, 0, 0, b.getWidth(), b.getHeight());
//用于手指拖动的,一个和这是item大小样式相同的view。
...
dragView.show(mMotionDownX,mMotionDownY);
//显示drayView,并开启弹出到手指触点的动画。
handleMoveEvent(mMotionDownX,mMotionDownY);
}
对于 handleMoveEvent(...):
privatevoid handleMoveEvent(int x, int y) {
//移动到手指点击位置
mDragObject.dragView.move(x, y);
// Drop on someone?
finalint[] coordinates =mCoordinatesTemp;
//查找可放置此view的目标容器。
DropTarget dropTarget =findDropTarget(x, y, coordinates);
…
}
接下进入 findDropTarget(...):
DropTarget findDropTarget(int x,int y,int[] dropCoordinates) {
...
DropTarget target =dropTargets.get(i);
target.getHitRect(r);
// Convert the hitrect toDragLayer coordinates
//target位置
target.getLocationInDragLayer(dropCoordinates);
r.offset(dropCoordinates[0] -target.getLeft(), dropCoordinates[1] - target.getTop());
...
if (r.contains(x, y)) {//手机触点是否在target区域
//一般返回空,是否一个可以吧事件传递给一个object。
DropTarget delegate =target.getDropTargetDelegate(mDragObject);
if (delegate !=null) {...}
// Make dropCoordinates relativeto the DropTarget
//手指触点,相对target位移
dropCoordinates[0] = x -dropCoordinates[0];
dropCoordinates[1] = y -dropCoordinates[1];
return target;
}
}
通过上边代码,找到目前可放置的target后,
接下来继续handleMoveEvent(...)中的代码:
if (dropTarget !=null) {
//一般返回空,是否一个可以吧事件传递给一个object。
DropTarget delegate =dropTarget.getDropTargetDelegate(mDragObject);
if (delegate !=null) {...}
if (mLastDropTarget != dropTarget) {
if (mLastDropTarget !=null) {
//和上次在不同target,调用 onDragExit,重置上次target数据信息。
mLastDropTarget.onDragExit(mDragObject);
}
//准备当前Target数据,
dropTarget.onDragEnter(mDragObject);
}
dropTarget.onDragOver(mDragObject);
} else {
if (mLastDropTarget !=null) {
mLastDropTarget.onDragExit(mDragObject);
}
}
接着进入onDragOver:如workspace中的onDragOver
publicvoid onDragOver(DragObject d){
// Skip drag over events while weare dragging over side pages
if (mInScrollArea)return;
if (mIsSwitchingState)return;
// Identify whether we havedragged over a side page
if (isSmall()) {
if (mLauncher.getHotseat() !=null &&!isExternalDragWidget(d)) {
mLauncher.getHotseat().getHitRect(r);
if (r.contains(d.x, d.y)) {
//target是否是launcher上的快捷栏。
layout = mLauncher.getHotseat().getLayout();
}
}
if (layout ==null) {
//获得view托进的cellayout
layout =findMatchingPageForDragOver(d.dragView, d.x, d.y,false);
}
...
}
// Handle the drag over
if (mDragTargetLayout !=null) {
// We want the point to be mappedto the dragTarget.
if (mLauncher.isHotseatLayout(mDragTargetLayout)) {
mapPointFromSelfToSibling(mLauncher.getHotseat(),mDragViewVisualCenter);
} else {
mapPointFromSelfToChild(mDragTargetLayout,mDragViewVisualCenter,null);
}
ItemInfo info = (ItemInfo) d.dragInfo;
//找到view放置的离当前最近的cell位置信息。
mTargetCell = findNearestArea((int)mDragViewVisualCenter[0],
(int)mDragViewVisualCenter[1], 1, 1,mDragTargetLayout,mTargetCell);
final View dragOverView =mDragTargetLayout.getChildAt(mTargetCell[0],
mTargetCell[1]);//可放置的target view
我们继续handleMoveEvent(...)中剩余代码。
mLastDropTarget = dropTarget;
//After a scroll, the touch point will still be in the scroll region.
//Rather than scrolling immediately, require a bit oftwiddling to scroll again
finalint slop =ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
mDistanceSinceScroll +=
Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));
mLastTouch[0] = x;
mLastTouch[1] = y;
...
mScrollRunnable.setDirection(SCROLL_LEFT);
mHandler.postDelayed(mScrollRunnable,SCROLL_DELAY);
-------->最后拖动的出口是在DragController中onTouchEvent的MotionEvent.ACTION_UP。
privatevoid drop(float x, float y) {
finalint[] coordinates =mCoordinatesTemp;
//查找可放置的target
final DropTarget dropTarget =findDropTarget((int) x, (int) y, coordinates);
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
boolean accepted =false;
if (dropTarget !=null) {
mDragObject.dragComplete =true;
dropTarget.onDragExit(mDragObject);
if (dropTarget.acceptDrop(mDragObject)) {
//判断target是否可以放置view
dropTarget.onDrop(mDragObject);
accepted = true;
}
}
mDragObject.dragSource.onDropCompleted((View) dropTarget,mDragObject, accepted);
}
publicvoid onDrop(DragObject d) {// workspace.ondrop
…
//Reparent the view
getParentCellLayoutForView(cell).removeView(cell);
addInScreen(cell, container, screen,mTargetCell[0],mTargetCell[1],
mDragInfo.spanX,mDragInfo.spanY);
…
if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
cell instanceofLauncherAppWidgetHostView) {
final CellLayout cellLayout =dropTargetLayout;
// We post this call sothat the widget has a chance to be placed
// in its finallocation
//4.0支持resize widget,以下是显示resize把手。
finalLauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
AppWidgetProviderInfopinfo = hostView.getAppWidgetInfo();
if (pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE){
final Runnable resizeRunnable=new Runnable() {
publicvoid run() {
DragLayer dragLayer=mLauncher.getDragLayer();
dragLayer.addResizeFrame(info,hostView, cellLayout);
}
};
post(new Runnable() {
publicvoid run() {
if (!isPageMoving()) {
//设置resize frame大小位置。
resizeRunnable.run();
} else {
mDelayedResizeRunnable = resizeRunnable;
}
}
});
:::
DragLayer = 》
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
int action = ev.getAction();
int x = (int) ev.getX();
int y = (int) ev.getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (handleTouchDown(ev, false)) {
return true;
}
}
}
if (mCurrentResizeFrame != null) {
handled = true;
switch (action) {
case MotionEvent.ACTION_MOVE:
mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
mCurrentResizeFrame.onTouchUp();
mCurrentResizeFrame = null;
}
}
if (handled) return true;
return mDragController.onTouchEvent(ev);
}
。。。
}
AppWidgetResizeFrame ==》 static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
int spanX, int spanY) {
:::
//保存view的信息到数据库。
LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, lp.cellX,lp.cellY);
…
android4.0支持快捷方式合并:
// If the item being droppedis a shortcut and the nearest drop
// cell also contains a shortcut,then create a folder with the two shortcuts.
if (!mInScrollArea &&createUserFolderIfNecessary(cell, container,
dropTargetLayout, mTargetCell, false, d.dragView,null)) {
return;
}
if(addToExistingFolderIfNecessary(cell, dropTargetLayout,mTargetCell, d, false)) {
return;
}
createUserFolderIfNecessary:
View v = target.getChildAt(targetCell[0],targetCell[1]);
boolean aboveShortcut =(v.getTag()instanceof ShortcutInfo);
boolean willBecomeShortcut =(newView.getTag()instanceof ShortcutInfo);
//如果都是快捷方式,才能进行合并
if (aboveShortcut&& willBecomeShortcut) {
ShortcutInfo sourceInfo =(ShortcutInfo) newView.getTag();
ShortcutInfo destInfo =(ShortcutInfo) v.getTag();
// if the drag started here, weneed to remove it from the workspace
if (!external) {
getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
}
Rect folderLocation = new Rect();
float scale =mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v,folderLocation);
target.removeView(v);
FolderIcon fi =
mLauncher.addFolder(target, container,screen, targetCell[0], targetCell[1]);
// If the dragView is null, wecan't animate
boolean animate = dragView !=null;
if (animate) {
fi.performCreateAnimation(destInfo,v, sourceInfo, dragView, folderLocation, scale,
postAnimationRunnable);
} else {
fi.addItem(destInfo);
fi.addItem(sourceInfo);
}
returntrue;
}
…
-------------->最后调用endDrag()
privatevoid endDrag() {
if (mDragging) {
mDragging =false;
for (DragListener listener :mListeners) {
listener.onDragEnd();
}
if (mDragObject.dragView != null) {
mDragObject.dragView.remove();
mDragObject.dragView =null;
}
mDragObject =null;
}
到此Launcher 上view的拖动就分析完了。
2.
Launcher开关门切换效果:
Workspace:
screenScrolledStandardUI()
PagedView : screenCenter = mScrollX +halfScreenSize 代表当前页面中心移动到的位置
getScrollProgress(screenCenter, cl, index)获取滑动的距离占最大距离。
int totalDistance = getScaledMeasuredWidth(v)+ mPageSpacing;
int delta = screenCenter -(getChildOffset(page) -
getRelativeChildOffset(page) +halfScreenSize);
float scrollProgress = delta /(totalDistance * 1.0f);
这样我们可以通过scrollProgress这个百分比,计算每个屏幕每个时刻旋转的角度,透明度,然后,根据屏幕位置设置旋转的轴位置,旋转方向。
循环滑动:
由于但scrollX = 0时,viewgroup就不能向左滑动,因此应该设置初始化位置离左边界很远,10万,这样想滑动到边界也要上万次滑动。向右滑动就不停romove第一个view,添加到最后,
然后再reLayout.更新view顺序在computeScrollHelper 的else中去处理。