上一篇中笔者分析了从WorkSpace的addInScreen方法中添加长按监听事件,到DragLayer拦截TouchEvent自己处理直到其TouchUp事件的drop方法流程。本篇则着重分析当打开文件夹时文件夹内部的拖拽以及从All Apps页面长按应用图标拖拽到WorkSpace页面的过程。
文件夹时从WorkSpace的bindItems方法中添加到WorkSpace中的:
public void bindItems(final ArrayList<ItemInfo> shortcuts, final int start, final int end) {
if (waitUntilResume(new Runnable() {
public void run() {
bindItems(shortcuts, start, end);
}
})) {
return;
}
// Get the list of added shortcuts and intersect them with the set of shortcuts here
Set<String> newApps = new HashSet<String>();
newApps = mSharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps);
Workspace workspace = mWorkspace;
for (int i = start; i < end; i++) {
final ItemInfo item = shortcuts.get(i);
// Short circuit if we are loading dock items for a configuration which has no dock
if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
mHotseat == null) {
continue;
}
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
ShortcutInfo info = (ShortcutInfo) item;
String uri = info.intent.toUri(0).toString();
View shortcut = createShortcut(info);
//通过bindItems方法,创造一个个BubbleTextView,然后通过workspace加载到对应celllayout中
workspace.addInScreen(shortcut, item.container, item.screen, item.cellX,
item.cellY, 1, 1, false);
boolean animateIconUp = false;
synchronized (newApps) {
if (newApps.contains(uri)) {
animateIconUp = newApps.remove(uri);
}
}
//需要动画显示的app icon,统一加到AnimationSet中一起播放
if (animateIconUp) {
// Prepare the view to be animated up
shortcut.setAlpha(0f);
shortcut.setScaleX(0f);
shortcut.setScaleY(0f);
mNewShortcutAnimatePage = item.screen;
if (!mNewShortcutAnimateViews.contains(shortcut)) {
mNewShortcutAnimateViews.add(shortcut);
}
}
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this,
(ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
(FolderInfo) item, mIconCache);
workspace.addInScreen(newFolder, item.container, item.screen, item.cellX,
item.cellY, 1, 1, false);
break;
}
}
workspace.requestLayout();
}
可以看到在switch分支LauncherSettings.Favorites.ITEM_TYPE_FOLDER中通过inflate加载R.layout.folder_icon.xml文件,最终加到WorSpace中,在R.layout.folder_icon文件中其实就是一个FolderIcon布局包裹了ImageView和BubbleTexView。此FolderIcon的onClick回调从fromXml方法内部可知即是在Luancher.java中:
public void onClick(View v) {
// Make sure that rogue clicks don't get through while allapps is launching, or after the
// view has detached (it's possible for this to happen if the view is removed mid touch).
if (v.getWindowToken() == null) {
return;
}
if (!mWorkspace.isFinishedSwitchingState()) {
return;
}
Object tag = v.getTag();
if (tag instanceof ShortcutInfo) {//被点击的type为application或者shortcut
// Open shortcut
final Intent intent = ((ShortcutInfo) tag).intent;
int[] pos = new int[2];
v.getLocationOnScreen(pos);
intent.setSourceBounds(new Rect(pos[0], pos[1],
pos[0] + v.getWidth(), pos[1] + v.getHeight()));
boolean success = startActivitySafely(v, intent, tag);
if (success && v instanceof BubbleTextView) {
mWaitingForResume = (BubbleTextView) v;
mWaitingForResume.setStayPressed(true);
}
} else if (tag instanceof FolderInfo) {//被点击的是文件夹
if (v instanceof FolderIcon) {
FolderIcon fi = (FolderIcon) v;
handleFolderClick(fi);//处理close open 文件夹操作
}
} else if (v == mAllAppsButton) {
if (isAllAppsVisible()) {
showWorkspace(true);
} else {
onClickAllAppsButton(v);
}
}
}
FolderIcon的Tag从FolderIcon.fromXml中可知正是FolderInfo。所以文件夹的点击最终进入了handleFolderClick方法中。其内部会执行打开或关闭文件夹的操作。本篇主要讨论在文件夹打开状态时执行的长按拖拽,所以有必要进入handleFolderClick的openFolder方法中去看看:
public void openFolder(FolderIcon folderIcon) {
Folder folder = folderIcon.getFolder();
FolderInfo info = folder.mInfo;
info.opened = true;
// Just verify that the folder hasn't already been added to the DragLayer.
// There was a one-off crash where the folder had a parent already.
if (folder.getParent() == null) {
mDragLayer.addView(folder);
mDragController.addDropTarget((DropTarget) folder);
} else {
Log.w(TAG, "Opening folder (" + folder + ") which already has a parent (" +
folder.getParent() + ").");
}
folder.animateOpen();
growAndFadeOutFolderIcon(folderIcon);
// Notify the accessibility manager that this folder "window" has appeared and occluded
// the workspace items
folder.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
从上边代码可以了解到,每个FolderIcon布局内部都存在一个Folder布局,而Folder布局在文件夹被打开时会被添加到DragLayer中去。由FolderIcon内部添加Folder操作的地方可以知道,Folder布局的xml代码就如以下所示,其内部拥有一个CellLayout用于装载应用图标(ShortcutInfo),并有一个EditText可用于修改当前文件夹的名称。
<com.android.launcher2.Folder
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/portal_container_holo">
<com.android.launcher2.CellLayout
android:id="@+id/folder_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/folder_padding"
android:paddingEnd="@dimen/folder_padding"
android:paddingTop="@dimen/folder_padding"
android:paddingBottom="@dimen/folder_padding"
android:cacheColorHint="#ff333333"
android:hapticFeedbackEnabled="false"
launcher:widthGap="@dimen/folder_width_gap"
launcher:heightGap="@dimen/folder_height_gap"
launcher:cellWidth="@dimen/folder_cell_width"
launcher:cellHeight="@dimen/folder_cell_height" />
<com.android.launcher2.FolderEditText
android:id="@+id/folder_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:paddingTop="@dimen/folder_name_padding"
android:paddingBottom="@dimen/folder_name_padding"
android:background="#00000000"
android:hint="@string/folder_hint_text"
android:textSize="14sp"
android:textColor="#ff33b5e5"
android:textColorHighlight="#ff333333"
android:gravity="center_horizontal"
android:singleLine="true"
android:imeOptions="flagNoExtractUi"/>
</com.android.launcher2.Folder>
至此,我们可以渐渐明朗,我们要分析的其实就是Folder控件内部的CellLayout的拖动。CellLayout的数据来源于Launcher内部createUserFolderIfNecessary方法所创建的FolderInfo。FolderInfo继承自ItemInfo,它标识了FolderIcon(文件夹)的位置信息,与普通的ItemInfo继承者如ShortcutInfo或ApplicationInfo不同的是,FolderInfo内部还有一个ArrayList用于装载文件夹内部所拥有的应用图标(ShortcutInfo)。所以FolderInfo中存放了文件夹所有的应用图标就是Folder内部的CellLayout的数据源。
在Folder控件内部的添加应用图标的方法createAndAddShortcut方法中,正是将添加的应用图标设置了长按事件监听。见以下代码,其中R.layout.application.xml其实就是一个BubbleTextView。
protected boolean createAndAddShortcut(ShortcutInfo item) {
final TextView textView =
(TextView) mInflater.inflate(R.layout.application, this, false);
textView.setCompoundDrawablesWithIntrinsicBounds(null,
new FastBitmapDrawable(item.getIcon(mIconCache)), null, null);
textView.setText(item.title);
if (item.contentDescription != null) {
textView.setContentDescription(item.contentDescription);
}
textView.setTag(item);
textView.setOnClickListener(this);
textView.setOnLongClickListener(this);
// We need to check here to verify that the given item's location isn't already occupied
// by another item.
if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0
|| item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) {
// This shouldn't happen, log it.
Log.e(TAG, "Folder order not properly persisted during bind");
if (!findAndSetEmptyCells(item)) {
return false;
}
}
CellLayout.LayoutParams lp =
new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY);
boolean insert = false;
textView.setOnKeyListener(new FolderKeyEventListener());
mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true);
return true;
}
以此知道,文件夹的长按事件正是被Folder监听了,那我们进入Folder的onLongClick方法瞧瞧:
public boolean onLongClick(View v) {
//内部判断是否WorkSpace正在从数据库中加载数据,即LoaderTask是否正在执行。
if (!mLauncher.isDraggingEnabled()) return true;
Object tag = v.getTag();
//文件夹的内部图标只能是ShortcutInfo
if (tag instanceof ShortcutInfo) {
ShortcutInfo item = (ShortcutInfo) tag;
if (!v.isInTouchMode()) {
return false;
}
//在长按时如果有第一次打开Folder时出现的cling提示,就dismiss掉
mLauncher.dismissFolderCling(null);
//此方法其实就是在WorkSpace中创建一个当前被长按图标的轮廓mDragOutline
mLauncher.getWorkspace().onDragStartedWithItem(v);
//beginDragShared方法内部主要在计算出拖拽控件在DragLayer中的位置后执行DragController的startDrag方法
mLauncher.getWorkspace().beginDragShared(v, this);
mIconDrawable = ((TextView) v).getCompoundDrawables()[1];
//记录当前被拖拽控件在Folder中的位置,然后把被拖拽控件从Folder中移除掉
mCurrentDragInfo = item;
mEmptyCell[0] = item.cellX;
mEmptyCell[1] = item.cellY;
mCurrentDragView = v;
mContent.removeView(mCurrentDragView);
mInfo.remove(mCurrentDragInfo);
mDragInProgress = true;
mItemAddedBackToSelfViaIcon = false;
}
return true;
}
方法内部其一调用WorkSpace的onDragStartedWithItem在WorkSpace中保留被拖拽控件的轮廓,如果Folder中的应用图标被拖拽到WorkSpace中,此轮廓就会显示到离被拖拽位置最近的Cell(mTargetCell)上,其二调用了WorkSpace的beginSharedDrag方法,最终会把Folder的拖拽交给DragLayer的TouchEvent控制器DragController处理,当然,在调用时指定了DragSource为此拖拽图标所在的Folder。
我们知道DragController的startDrag方法会调用handleMoveEvent。其实整个拖拽过程中handleMoveEvent会被一直调用,而在此方法内部对当前moveX,Y进行了判断。
private void handleMoveEvent(int x, int y) {
mDragObject.dragView.move(x, y);
// Drop on someone?
final int[] coordinates = mCoordinatesTemp;
//此处的coordinates要经过修正,得出的相对于workspace的x,y值
DropTarget dropTarget = findDropTarget(x, y, coordinates);
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
//通知workspace当前dragView的xy值(此x,y值已经不是相对于DragLayer,而是经过修正,相对于WorkSpace)等信息,
// 方便Workspace在onDragOver回调处理像显示轮廓等信息
checkTouchMove(dropTarget);
// Check if we are hovering over the scroll areas
//Math.pow是进行次方运算,Math.sqrt进行开方运算
mDistanceSinceScroll +=
Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));
mLastTouch[0] = x;
mLastTouch[1] = y;
//检查当前的x,y值是否进入WorkSpace的页面scroll边界,边界值为20dp,
//也就是说当前dragView进入到距离WorkSpace的左右边界20dp范围内的话就触发scroll页面
checkScrollState(x, y);
}
进入findDropTarget方法内部我们可以看见其内部对mDropTargets集合中的各个DropTarget进行moveX,Y的contains碰撞检查。
private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
final Rect r = mRectTemp;
final ArrayList<DropTarget> dropTargets = mDropTargets;
final int count = dropTargets.size();
for (int i=count-1; i>=0; i--) {
DropTarget target = dropTargets.get(i);
if (!target.isDropEnabled())
continue;
target.getHitRect(r);
// dropCoordinates是target经过缩放、平移等之后的相距parent的位置
target.getLocationInDragLayer(dropCoordinates);
//一般情况下此处offset--0
r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
mDragObject.x = x;
mDragObject.y = y;
if (r.contains(x, y)) {
DropTarget delegate = target.getDropTargetDelegate(mDragObject);
if (delegate != null) {
target = delegate;
target.getLocationInDragLayer(dropCoordinates);
}
//此处解析:原本dropCoordinates数组是WorkSpace相对与DragLayer的位置,
// 在进行减法后结果是x,y的位置都变成了相对于workspace了
dropCoordinates[0] = x - dropCoordinates[0];
dropCoordinates[1] = y - dropCoordinates[1];
return target;
}
}
return null;
}
还记得本篇开始部分分析的点击FolderIcon时调用的openFolder方法(代码段在文章前半部分可见)吗?在openFolder内部其实就把Folder控件添加到mDropTargets集合中了。这就意味着如果当前DragView的moveX,Y如果在Folder控件内部,则找到的DropTarget就是Folder自身。
那么checkTouchMove方法中的onDragEnter,onDragOver方法我们应该从Folder内分析(关于当DragView从Folder被拖拽到WorkSpace中后的拖拽过程此处不进行分析,在上篇中已经着重分析过)。
private void checkTouchMove(DropTarget dropTarget) {
if (dropTarget != null) {
DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);
if (delegate != null) {
dropTarget = delegate;
}
//mLastDropTarget在down事件触发时会被置null,所以当每次重新长按时一定会走onDragEnter
if (mLastDropTarget != dropTarget) {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragObject);
}
//首次时调用onDragEnter
dropTarget.onDragEnter(mDragObject);
}
//以后每次移动时都会调用onDragOver
dropTarget.onDragOver(mDragObject);
} else {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragObject);
}
}
mLastDropTarget = dropTarget;
}
在Folder的onDragEnter内部进行mPreviousTargetCell初始化,并取消掉了mOnExitAlarmListener(内部执行closeFodler)。
public void onDragEnter(DragObject d) {
mPreviousTargetCell[0] = -1;
mPreviousTargetCell[1] = -1;
mOnExitAlarm.cancelAlarm();
}
以下是Folder的onDragOver方法。其在找出了距离DragView最近的mTargetCell后,使用Alarm机制对Folder内部图标进行了排序。
public void onDragOver(DragObject d) {
float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null);
mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1], 1, 1, mTargetCell);
if (isLayoutRtl()) {
mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1;
}
//在onDragOver只需调用过程中如果当前的DragView没有变化Cell位置。则内部只会被调用一次。
if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) {
mReorderAlarm.cancelAlarm();
mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
mReorderAlarm.setAlarm(150);
mPreviousTargetCell[0] = mTargetCell[0];
mPreviousTargetCell[1] = mTargetCell[1];
}
}
鉴于Alarm机制其实就是在指定的时间后调用Listener的onAlarm方法,所以我们可以进入mReoderAlarmListener的onAlarm方法,发现其内部主要调用了Folder的realTimeReorder方法,realTimeReorder内部主要根据DragView原始位置和mTargetCell(离DragView最近的Cell)来让处于两者之间的应用图标往前或者往后挪动一个Cell。
/**
*
* @param empty 指的是长按应用图标所在位置的Cell坐标
* @param target
*/
private void realTimeReorder(int[] empty, int[] target) {
boolean wrap;
int startX;
int endX;
int startY;
int delay = 0;
float delayAmount = 30;
//target位置是否比empty位置靠后
if (readingOrderGreaterThan(target, empty)) {
//DragView原始位置是否是那一行的最后一个,wrap=true,表示是最后一个
wrap = empty[0] >= mContent.getCountX() - 1;
//如果是最后一个,那么startY就为下一行。
startY = wrap ? empty[1] + 1 : empty[1];
//先进行行(hang)遍历
for (int y = startY; y <= target[1]; y++) {
//如果是DragView原始位置所在行,那么只遍历原始位置之后的Cell,
// 如果是其他行,则从第一个开始遍历
startX = y == empty[1] ? empty[0] + 1 : 0;
//如果是最近Cell所在行,则只遍历最近Cell往前的Cell,如果是其他行,则endX为行末
endX = y < target[1] ? mContent.getCountX() - 1 : target[0];
for (int x = startX; x <= endX; x++) {
/**
* 在遍历过程中不断把符合位置筛选的应用图标往前移一个Cell。
*/
View v = mContent.getChildAt(x,y);
if (mContent.animateChildToPosition(v, empty[0], empty[1],
REORDER_ANIMATION_DURATION, delay, true, true)) {
empty[0] = x;
empty[1] = y;
delay += delayAmount;
delayAmount *= 0.9;
}
}
}
} else {//target位置是否比empty位置靠前
//DragView原始位置是否是所在行第一个
wrap = empty[0] == 0;
//如果是所在行第一个则startY(即需要挪动的Y)为上一行,否则为当前DragView原始位置所在行
startY = wrap ? empty[1] - 1 : empty[1];
for (int y = startY; y >= target[1]; y--) {
startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1;
endX = y > target[1] ? 0 : target[0];
for (int x = startX; x >= endX; x--) {
//满足条件(在empty和target之间)的控件都往后移动一个Cell
View v = mContent.getChildAt(x,y);
if (mContent.animateChildToPosition(v, empty[0], empty[1],
REORDER_ANIMATION_DURATION, delay, true, true)) {
empty[0] = x;
empty[1] = y;
delay += delayAmount;
delayAmount *= 0.9;
}
}
}
}
}
在看完TouchMove之后,我们接着来看TouchUp事件,从拖拽分析上篇文章总我们可以知道,TouchUp时主要是调用DropTarget(此处即Folder)的onDrop方法。以下时Folder的onDrop方法,其内部重新添加mCurrentDragView到Folder后,会先隐藏掉它,做动画让DragView平移到mCurrentDragView位置后再让它显示出来。
public void onDrop(DragObject d) {
ShortcutInfo item;
if (d.dragInfo instanceof ApplicationInfo) {
// Came from all apps -- make a copy
item = ((ApplicationInfo) d.dragInfo).makeShortcut();
item.spanX = 1;
item.spanY = 1;
} else {
item = (ShortcutInfo) d.dragInfo;
}
// Dragged from self onto self, currently this is the only path possible, however
// we keep this as a distinct code path.
if (item == mCurrentDragInfo) {
ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag();
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams();
//mEmptyCell位置其实在realTimeReorder过程中一直在变
si.cellX = lp.cellX = mEmptyCell[0];
si.cellX = lp.cellY = mEmptyCell[1];
mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true);
if (d.dragView.hasDrawn()) {
//其内部会先隐藏掉mCurrentDragView,然后让DragView平移到mCurrentDragView位置,
// 动画完成之后再让mCurrentDragView显示
mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView);
} else {
d.deferDragViewCleanupPostAnimation = false;
mCurrentDragView.setVisibility(VISIBLE);
}
mItemsInvalidated = true;
setupContentDimensions(getItemCount());
mSuppressOnAdd = true;
}
mInfo.add(item);
}
OK!文件夹的拖拽过程我们就介绍到这。
接下来我们介绍All Apps页面的拖拽流程。
进入到AppsCustomizePagedView中可以看到在syncAppsPageItems方法和syncWidgetPageItems方法中分别对应用图标和Widget图标都设置了长按监听事件,并且监听者就是自身。然而我们全局搜索却没有发现onLongClick方法,怎么回事呢。我们通过往父类追溯发现onLongClick在其父类PagedViewWithDraggableItems中:
@Override
public boolean onLongClick(View v) {
//如果被点击控件没有处于Touch模式
if (!v.isInTouchMode()) return false;
//如果当前正在滑动page
if (mNextPage != INVALID_PAGE) return false;
// 如果没有当前处于All Apps页面,或者WorkSpace正在切换状态
if (!mLauncher.isAllAppsVisible() ||
mLauncher.getWorkspace().isSwitchingState()) return false;
//如果LauncherModel中的LoaderTask没有执行完毕
if (!mLauncher.isDraggingEnabled()) return false;
return beginDragging(v);
}
可以发现在其判断了不能接收长按事件的几种情况后,调用了beginDragging方法,父类PagedViewWithDraggableItems的beginDragging方法其实被子类AppsCustomizePagedView重写了,所以我们直接看AppsCustomizePagedView的beginDragging方法:
@Override
protected boolean beginDragging(final View v) {
if (!super.beginDragging(v)) return false;
if (v instanceof PagedViewIcon) {
beginDraggingApplication(v);
} else if (v instanceof PagedViewWidget) {
if (!beginDraggingWidget(v)) {
return false;
}
}
// We delay entering spring-loaded mode slightly to make sure the UI
// thready is free of any work.
postDelayed(new Runnable() {
@Override
public void run() {
// We don't enter spring-loaded mode if the drag has been cancelled
if (mLauncher.getDragController().isDragging()) {
//如果当前是第一次进入All Apps页面,那么Cling提示就会存在,所以在长按时应该隐藏Cling提示
mLauncher.dismissAllAppsCling(null);
// Reset the alpha on the dragged icon before we drag
resetDrawableState();
//通知WorkSpace进入SPRING_LOADED小屏模式
mLauncher.enterSpringLoadedDragMode();
}
}
}, 150);
return true;
}
从上可知,其区分为长按的是应用图标还是Widget控件做出不同的响应,且通知了WorkSpace进入小屏模式。
进入beginDraggingApplication方法和beginDraggingWidget方法:
private void beginDraggingApplication(View v) {
mLauncher.getWorkspace().onDragStartedWithItem(v);
mLauncher.getWorkspace().beginDragShared(v, this);
}
private boolean beginDraggingWidget(View v) {
mDraggingWidget = true;
// Get the widget preview as the drag representation
ImageView image = (ImageView) v.findViewById(R.id.widget_preview);
PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag();
// If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and
// we abort the drag.
if (image.getDrawable() == null) {
mDraggingWidget = false;
return false;
}
// Compose the drag image
Bitmap preview;
Bitmap outline;
float scale = 1f;
Point previewPadding = null;
Log.i(TAG,"createItemInfo:"+createItemInfo.getClass().getSimpleName());
if (createItemInfo instanceof PendingAddWidgetInfo) {
// This can happen in some weird cases involving multi-touch. We can't start dragging
// the widget if this is null, so we break out.
if (mCreateWidgetInfo == null) {
return false;
}
//此处mCreateWidgetInfo从onShortPress方法传过来,
// onShortPress方法在PagedViewWidget中被调用
PendingAddWidgetInfo createWidgetInfo = mCreateWidgetInfo;
createItemInfo = createWidgetInfo;
int spanX = createItemInfo.spanX;
int spanY = createItemInfo.spanY;
//此处计算出来的size经过缩放,因为SPRING_LOADED模式一般都比正常NORMAL模式要小。
// 所以此方法经过比例缩放。size->width+height
int[] size = mLauncher.getWorkspace().estimateItemSize(spanX, spanY,
createWidgetInfo, true);
//Widget图片
FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable();
float minScale = 1.25f;
int maxWidth, maxHeight;
maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]);
maxHeight = Math.min((int) (previewDrawable.getIntrinsicHeight() * minScale), size[1]);
int[] previewSizeBeforeScale = new int[1];
preview = mWidgetPreviewLoader.generateWidgetPreview(createWidgetInfo.info, spanX,
spanY, maxWidth, maxHeight, null, previewSizeBeforeScale);
// Compare the size of the drag preview to the preview in the AppsCustomize tray
int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0],
mWidgetPreviewLoader.maxWidthForWidgetPreview(spanX));
scale = previewWidthInAppsCustomize / (float) preview.getWidth();
// The bitmap in the AppsCustomize tray is always the the same size, so there
// might be extra pixels around the preview itself - this accounts for that
if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) {
int padding =
(previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2;
previewPadding = new Point(padding, 0);
}
} else {
PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag();
// Widgets are only supported for current user, not for other profiles.
// Hence use myUserHandle().
Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.shortcutActivityInfo,
android.os.Process.myUserHandle());
preview = Bitmap.createBitmap(icon.getIntrinsicWidth(),
icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
mCanvas.setBitmap(preview);
mCanvas.save();
WidgetPreviewLoader.renderDrawableToBitmap(icon, preview, 0, 0,
icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
mCanvas.restore();
mCanvas.setBitmap(null);
//如果是shortcutInfo则只占一个Cell
createItemInfo.spanX = createItemInfo.spanY = 1;
}
// Don't clip alpha values for the drag outline if we're using the default widget preview
boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo &&
(((PendingAddWidgetInfo) createItemInfo).info.previewImage == 0));
//轮廓图
outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(),
false);
//锁定屏幕方向
mLauncher.lockScreenOrientation();
//在WorkSpace中绘制轮廓。
mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha);
//开始拖拽
mDragController.startDrag(image, preview, this, createItemInfo,
DragController.DRAG_ACTION_COPY, previewPadding, scale);
outline.recycle();
preview.recycle();
return true;
}
除了Widget长按时会根据类型构建不同Span大小的preview图片,两者步骤都是创建轮廓之后调用DragController的startDrag方法。走到startDrag时流程跟在WorkSpace没什么区别了,因为两种方式的DropTarget都为WorkSpace。
我们在此处更关心当前页面是如何通知WorkSpace进入小屏SPRING_LOADED模式的,即BeginDragging方法中postDelayed的Runnable中流程。在Runnable中调用了Launcher.java的enterSpringLoadedDragMode方法通知WorkSpace进入SPRING_LOADED模式。
void enterSpringLoadedDragMode() {
if (isAllAppsVisible()) {
hideAppsCustomizeHelper(State.APPS_CUSTOMIZE_SPRING_LOADED, true, true, null);
hideDockDivider();
mState = State.APPS_CUSTOMIZE_SPRING_LOADED;
}
}
我们进入hideAppsCustomizeHelper方法,此方法就是做了一个All Apps页面到WorkSpace页面的切换动画,包含All Apps页面的放大和透明度动画,WorkSpace页面的CellLayout平移缩放动画、边框透明度动画及WorkSpace的background透明度动画。
private void hideAppsCustomizeHelper(State toState, final boolean animated,
final boolean springLoaded, final Runnable onCompleteRunnable) {
//如果State切换动画没有完成,则取消掉State切换。
if (mStateAnimation != null) {
mStateAnimation.setDuration(0);
mStateAnimation.cancel();
mStateAnimation = null;
}
Resources res = getResources();
final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime);
final int fadeOutDuration =
res.getInteger(R.integer.config_appsCustomizeFadeOutTime);
final float scaleFactor = (float)
res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor);
//动画开始结束的控件,此处与showAppsCustomizeHelper相反。
final View fromView = mAppsCustomizeTabHost;
final View toView = mWorkspace;
Animator workspaceAnim = null;
//enterSpringLoadedDragMode可知toState为APPS_CUSTOMIZE_SPRING_LOADED。
if (toState == State.WORKSPACE) {
int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger);
workspaceAnim = mWorkspace.getChangeStateAnimation(
Workspace.State.NORMAL, animated, stagger);
} else if (toState == State.APPS_CUSTOMIZE_SPRING_LOADED) {
//workspace动画,包含平移+缩放+CellLayout边框+WorkSpace的背景
workspaceAnim = mWorkspace.getChangeStateAnimation(
Workspace.State.SPRING_LOADED, animated);
}
//设置fromView的动画锚点
setPivotsForZoom(fromView, scaleFactor);
//设置壁纸可见性
updateWallpaperVisibility(true);
//主要是HotSeat透明度变化
showHotseat(animated);
if (animated) {
//AppsCustomizePagedView整体的一个放大动画,放大倍数为scaleFactor:7倍
final LauncherViewPropertyAnimator scaleAnim =
new LauncherViewPropertyAnimator(fromView);
scaleAnim.
scaleX(scaleFactor).scaleY(scaleFactor).
setDuration(duration).
setInterpolator(new Workspace.ZoomInInterpolator());
//fromView的透明度动画从1~0
final ObjectAnimator alphaAnim = LauncherAnimUtils
.ofFloat(fromView, "alpha", 1f, 0f)
.setDuration(fadeOutDuration);
alphaAnim.setInterpolator(new AccelerateDecelerateInterpolator());
alphaAnim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float t = 1f - (Float) animation.getAnimatedValue();
//调用fromView及toView的onLauncherTransitionStep方法fromView即AppsCustomizePagedView内部空实现
dispatchOnLauncherTransitionStep(fromView, t);
dispatchOnLauncherTransitionStep(toView, t);
}
});
mStateAnimation = LauncherAnimUtils.createAnimatorSet();
dispatchOnLauncherTransitionPrepare(fromView, animated, true);
dispatchOnLauncherTransitionPrepare(toView, animated, true);
mAppsCustomizeContent.pauseScrolling();
mStateAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
updateWallpaperVisibility(true);
//动画结束时隐藏掉all apps页面
fromView.setVisibility(View.GONE);
dispatchOnLauncherTransitionEnd(fromView, animated, true);
dispatchOnLauncherTransitionEnd(toView, animated, true);
if (mWorkspace != null) {
mWorkspace.hideScrollingIndicator(false);
}
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
mAppsCustomizeContent.updateCurrentPageScroll();
mAppsCustomizeContent.resumeScrolling();
}
});
//此处一起播放FromView的放大动画和透明度动画。
mStateAnimation.playTogether(scaleAnim, alphaAnim);
if (workspaceAnim != null) {
//播放workspace动画,包含平移+缩放+CellLayout边框+WorkSpace的背景
mStateAnimation.play(workspaceAnim);
}
dispatchOnLauncherTransitionStart(fromView, animated, true);
dispatchOnLauncherTransitionStart(toView, animated, true);
LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView);
} else {
fromView.setVisibility(View.GONE);
dispatchOnLauncherTransitionPrepare(fromView, animated, true);
dispatchOnLauncherTransitionStart(fromView, animated, true);
dispatchOnLauncherTransitionEnd(fromView, animated, true);
dispatchOnLauncherTransitionPrepare(toView, animated, true);
dispatchOnLauncherTransitionStart(toView, animated, true);
dispatchOnLauncherTransitionEnd(toView, animated, true);
mWorkspace.hideScrollingIndicator(false);
}
}
接下来在小屏WorkSpace中的拖拽事件,我们在
拖拽过程分析(上)已经有详细介绍,这里就不啰嗦了。