上一篇中笔者分析了从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中的拖拽事件,我们在 拖拽过程分析(上)已经有详细介绍,这里就不啰嗦了。