Launcher3拖拽流程分析

本文主要分析下Launcher3的拖拽流程。Launcher3的点击事件比较好分析,大都都是打开某个应用或者进入文件夹app界面。而拖拽流程就比较复杂,拖拽的触发是View的长按事件,而长按事件的处理是在Launcher3.java的onLongClick()方法中,我们一起来看下。



    @Override
    public boolean onLongClick(View v) {
        ...
        if (v instanceof Workspace) {
            Log.i("Tim_C","v is a workspace");
            if (!mWorkspace.isInOverviewMode()) {
                Log.i("Tim_C","v is in isInOverviewMode");
                if (!mWorkspace.isTouchActive()) {
                    Log.i("Tim_C","v is workspace enter overview mode");
                    showOverviewMode(true);--------1
                    mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                            HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }

       ...

        // The hotseat touch handling does not go through Workspace, and we always allow long press
        // on hotseat items.
        if (!mDragController.isDragging()) {
            if (itemUnderLongClick == null) {
                Log.i("Tim_C","User long pressed on empty space");
                // User long pressed on empty space
                mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);--------3
                if (mWorkspace.isInOverviewMode()) {
                    mWorkspace.startReordering(v);
                } else {
                    showOverviewMode(true);------2
                }
            } else {
                    ....
                    mWorkspace.startDrag(longClickCellInfo, dragOptions);
                }
            }
        }
        return true;
    }

在调试代码的时候,发现在长按workspace后不会进入“----1”处的逻辑,而是直接进入"-------2"处的逻辑。原来在布局的时候CellLayout处于workspace的上面,事件先被CellLayout拦截,所v是一个CellLayout对象而不是Workspace对象。代码中有一个performHapticFeedback方法调用,主要作用是开启Workspace的触感反馈效果。函数经过一系列的判断函数最终会调用Workspace的startDrag()方法进入拖拽流程。startDrag()的具体实现在beginDragShared()函数中

packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
    public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
            DragPreviewProvider previewProvider, DragOptions dragOptions) {
        child.clearFocus();
        child.setPressed(false);
        mOutlineProvider = previewProvider;

        // The drag bitmap follows the touch point around on the screen
        final Bitmap b = previewProvider.createDragBitmap(mCanvas);
        ...
        DeviceProfile grid = mLauncher.getDeviceProfile();
        Point dragVisualizeOffset = null;
        Rect dragRect = null;
        if (child instanceof BubbleTextView) {
            int iconSize = grid.iconSizePx;
            int top = child.getPaddingTop();
            int left = (b.getWidth() - iconSize) / 2;
            int right = left + iconSize;
            int bottom = top + iconSize;
            dragLayerY += top;
            // Note: The drag region is used to calculate drag layer offsets, but the
            // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
            dragRect = new Rect(left, top, right, bottom);
        } else if (child instanceof FolderIcon) {
            int previewSize = grid.folderIconSizePx;
            dragVisualizeOffset = new Point(- halfPadding, halfPadding - child.getPaddingTop());
            dragRect = new Rect(0, child.getPaddingTop(), child.getWidth(), previewSize);
        }
        ...
        DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
                dragObject, dragVisualizeOffset, dragRect, scale, dragOptions);
        dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
        b.recycle();
        return dv;
    }

注意函数中有对mOutlineProvider赋值,DragPreviewProvider的作用有两点,一是通过调用createDragBitmap()方法生成随手指移动的应用图标,二是生成outline图标(显示在手指下面的那个白框)。函数最后会调用DragController的startDrag()函数。

packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
    public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
            DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
            float initialDragViewScale, DragOptions options) {
        ...
       
        mDragObject = new DropTarget.DragObject();

        mIsDragDeferred = !mOptions.deferDragCondition.shouldStartDeferredDrag(0);

        final Resources res = mLauncher.getResources();
        final float scaleDps = FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND
                ? res.getDimensionPixelSize(R.dimen.dragViewScale)
                : mIsDragDeferred
                    ? res.getDimensionPixelSize(R.dimen.deferred_drag_view_scale)
                    : 0f;
        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
                registrationY, initialDragViewScale, scaleDps);

        mDragObject.dragComplete = false;
        if (mOptions.isAccessibleDrag) {
            // For an accessible drag, we assume the view is being dragged from the center.
            mDragObject.xOffset = b.getWidth() / 2;
            mDragObject.yOffset = b.getHeight() / 2;
            mDragObject.accessibleDrag = true;
        } else {
            mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
            mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
            mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);

            mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions);
        }

        mDragObject.dragSource = source;
        mDragObject.dragInfo = dragInfo;
        mDragObject.originalDragInfo = new ItemInfo();
        mDragObject.originalDragInfo.copyFrom(dragInfo);

        if (dragOffset != null) {
            dragView.setDragVisualizeOffset(new Point(dragOffset));
        }
        if (dragRegion != null) {
            dragView.setDragRegion(new Rect(dragRegion));
        }

        mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        dragView.show(mMotionDownX, mMotionDownY);//显示跟随手指移动的应用图标
        ...
        mLastTouch[0] = mMotionDownX;
        mLastTouch[1] = mMotionDownY;
        handleMoveEvent(mMotionDownX, mMotionDownY);//拖拽的移动处理
        mLauncher.getUserEventDispatcher().resetActionDurationMillis();
        return dragView;
    }

函数创建了一个DropTarget.DragObject对象,之后对其实参数进行赋值,其中创建的DragView是对Bitmap的封装,并调用其show()方法来显示应用图标,最后调用handleMoveEvent()方法来处理拖拽的移动。

packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
  private void handleMoveEvent(int x, int y) {
        mDragObject.dragView.move(x, y);
        // Drop on someone?
        final int[] coordinates = mCoordinatesTemp;
        DropTarget dropTarget = findDropTarget(x, y, coordinates);
        mDragObject.x = coordinates[0];
        mDragObject.y = coordinates[1];
        checkTouchMove(dropTarget);

        // Check if we are hovering over the scroll areas
        mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y);
        mLastTouch[0] = x;
        mLastTouch[1] = y;
        checkScrollState(x, y);

        if (mIsDragDeferred && mOptions.deferDragCondition.shouldStartDeferredDrag(
                Math.hypot(x - mMotionDownX, y - mMotionDownY))) {
            startDeferredDrag();
        }
    }

代码中调用dragView的move()来绘制更新位置,调用checkTouchMove()来检测进入某个DropTarget,DropTarget有实现onDragExit,onDragEnter,onDragOver方法需要处理。比如说当拖动到文件夹上面会打开显示文件夹,拖动到删除区域改变颜色等,checkScrollState则是用来检测是否应当滑动到下一页。可以想到这个函数是需要不断调用,以更新显示拖拽图标的位置。其实跟踪下代码可以看到DragLayer的onTouchEvent被DragController接管了。主要代码如下:

packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragLayer.java
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...
        if (mActiveController != null) {
            return mActiveController.onTouchEvent(ev);
        }
        return false;
    }
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
(implements DragDriver.EventListener)
    public boolean onTouchEvent(MotionEvent ev) {
        ...
        return mDragDriver.onTouchEvent(ev);
    }
   @Override
    public void onDriverDragMove(float x, float y) {
        final int[] dragLayerPos = getClampedDragLayerPos(x, y);
        handleMoveEvent(dragLayerPos[0], dragLayerPos[1]);
    }
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragDriver.java
  public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

        switch (action) {
            case MotionEvent.ACTION_MOVE:
                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                break;
            case MotionEvent.ACTION_UP:
                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                mEventListener.onDriverDragEnd(ev.getX(), ev.getY(), null);
                break;
            case MotionEvent.ACTION_CANCEL:
                mEventListener.onDriverDragCancel();
                break;
        }

        return true;
    }

好了,我们已经快要分析完了。当然当我们停下来手指离开屏幕就会触发ACTION_UP事件,从而调用DragController的onDriverDragEnd()方法,这个方法简单只是找到DropTarget对象并调用drop()函数和endDrag()函数。drop函数代码如下:

packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
    void drop(DropTarget dropTarget, float x, float y, PointF flingVel) {
        ...
        // Drop onto the target.
        boolean accepted = false;
        if (dropTarget != null) {
            dropTarget.onDragExit(mDragObject);
            if (dropTarget.acceptDrop(mDragObject)) {
                if (flingVel != null) {
                    dropTarget.onFlingToDelete(mDragObject, flingVel);
                } else {
                    dropTarget.onDrop(mDragObject);
                }
                accepted = true;
            }
        }
        final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
        mDragObject.dragSource.onDropCompleted(
                dropTargetAsView, mDragObject, flingVel != null, accepted);
        mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView);
        if (mIsDragDeferred) {
            mOptions.deferDragCondition.onDropBeforeDeferredDrag();
        }
    }

可以看到drop()函数会调onFlingToDelete和onDrop函数,当一种情况是当我们快速地向上滑动的时候调用,第二种是普通的拖拽调用。之后会调用onDropCompleted函数。第一种情况的具体实现是在DeleteDropTarget中,我们来看下最后调用的代码:

packages\apps\Launcher3\src\com\android\launcher3\DeleteDropTarget.java
/**
 * Removes the item from the workspace. If the view is not null, it also removes the view.
 */
public static void removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) {
	// Remove the item from launcher and the db, we can ignore the containerInfo in this call
	// because we already remove the drag view from the folder (if the drag originated from
	// a folder) in Folder.beginDrag()
	launcher.removeItem(view, item, true /* deleteFromDb */);
	launcher.getWorkspace().stripEmptyScreens();    
	launcher.getDragLayer().announceForAccessibility(launcher.getString(R.string.item_removed));
}

代码很简单,直接删除掉拖拽对象,并且删除空页面。对于第二种情况,因为大多数的拖拽都是在Workspace上面,所以我们来看Workspace的onDrop的实现。

packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
    public void onDrop(final DragObject d) {
        ...
        if (d.dragSource != this) {
            final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1] };
            onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d);
        } else if (mDragInfo != null) {
            final View cell = mDragInfo.cell;

            if (dropTargetLayout != null && !d.cancelled) {
                ...
                mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int)
                        mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell);
                float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                        mDragViewVisualCenter[1], mTargetCell);

                // If the item being dropped is 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, distance, false, d.dragView, null)) {
                    return;
                }

                if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
                        distance, d, false)) {
                    return;
                }

                

                int[] resultSpan = new int[2];
                mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                        (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
                        mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);

                
                if (foundCell) {
                    final ItemInfo info = (ItemInfo) cell.getTag();
                    if (hasMovedLayouts) {
                        // Reparent the view
                        CellLayout parentCell = getParentCellLayoutForView(cell);
                        if (parentCell != null) {
                            parentCell.removeView(cell);
                        } else if (ProviderConfig.IS_DOGFOOD_BUILD) {
                            throw new NullPointerException("mDragInfo.cell has null parent");
                        }
                        addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
                                info.spanX, info.spanY);
                    }
                    // update the item's position after drop
                    CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
                    lp.cellX = lp.tmpCellX = mTargetCell[0];
                    lp.cellY = lp.tmpCellY = mTargetCell[1];
                    lp.cellHSpan = item.spanX;
                    lp.cellVSpan = item.spanY;
                    lp.isLockedToGrid = true;
                    ...
                    LauncherModel.modifyItemInDatabase(mLauncher, info, container, screenId, lp.cellX,
                            lp.cellY, item.spanX, item.spanY);
                } else {
                    // If we can't find a drop location, we return the item to its original position
                    CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
                    mTargetCell[0] = lp.cellX;
                    mTargetCell[1] = lp.cellY;
                    CellLayout layout = (CellLayout) cell.getParent().getParent();
                    layout.markCellsAsOccupiedForView(cell);
                }
            }

            final CellLayout parent = (CellLayout) cell.getParent().getParent();
            // Prepare it to be animated into its new position
            // This must be called after the view has been re-parented
            final Runnable onCompleteRunnable = new Runnable() {
                @Override
                public void run() {
                    mAnimatingViewIntoPlace = false;
                    updateChildrenLayersEnabled(false);
                }
            };
            mAnimatingViewIntoPlace = true;
            if (d.dragView.hasDrawn()) {
                ...
                if (isWidget) {
                    int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
                            ANIMATE_INTO_POSITION_AND_DISAPPEAR;
                    animateWidgetDrop(info, parent, d.dragView,
                            onCompleteRunnable, animationType, cell, false);
                } else {
                    int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION;
                    mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
                            onCompleteRunnable, this);
                }
            } else {
                d.deferDragViewCleanupPostAnimation = false;
                cell.setVisibility(VISIBLE);
            }
            parent.onDropChild(cell);
        }
        if (d.stateAnnouncer != null) {
            d.stateAnnouncer.completeAction(R.string.item_moved);
        }
    }

onDrop的代码很长,上述代码只是把主要的功能代码贴上去了。首先计算mTargetCell也就要drop的位置,然后判断是不是在另一个图标上面,如果是则创建一个文件夹;第二就是直接放入一个已经存在的文件夹里,但是Folder也是实现了onDrop的,所以这部份代码是不会执行的。第三就是拖拽对象是不是在另一页,如果是则调用addInScreen()方法加入到该页面,如果没有在其它页就更新位置,最后再更新数据库;第四如果找不到位置就放置到原位。onDrop执行完后会执行onDragEnd()函数,请允许我把最后一步的代码贴下:

packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
   @Override
    public void onDragEnd() {
        if (ENFORCE_DRAG_EVENT_ORDER) {
            enfoceDragParity("onDragEnd", 0, 0);
        }

        if (!mDeferRemoveExtraEmptyScreen) {
            removeExtraEmptyScreen(true, mDragSourceInternal != null);
        }

        updateChildrenLayersEnabled(false);
        mLauncher.unlockScreenOrientation(false);

        // Re-enable any Un/InstallShortcutReceiver and now process any queued items
        InstallShortcutReceiver.disableAndFlushInstallQueue(getContext());

        mDragSourceInternal = null;
        mLauncher.onInteractionEnd();
    }

代码比较好理解,删除掉空页面,取消锁定方向,允许安装应用程序。至此拖拽的流程就分析完了,由于知识水平有限,本文难免有写的不对的地方,还望大家指正。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值