Android O Launcher3--拖拽

 

前言     

  在Android手机桌面,我们经常会把一个应用的图标从菜单里面,拖拽到桌面。或者把一个应用的图标移到自己更加喜欢的位置。拖拽能够让用户方便的把应用放到用户可记得易操作的位置,从而能够让用户快捷的打开高频使用的应用。同时,拖拽也可以让用户能够布置自己的桌面,能够把应用进行分类的存放。因此,Launcher拖拽让用户可自定义桌面。

拖拽的内容:

  1. 主屏幕(Workspace)上的图标和小部件
  2. 文件夹中的图标
  3. 抽屉中的图标和小部件

拖拽过程是怎样的,就像大象装冰箱是一样的,分三步长按(把冰箱门打开)拖拽(把大象放进冰箱)放开(带上冰箱门)。

第一步:长按处理及准备工作

 长按就要有监听器,首先看监听器是如何设置的。

在Workspace中addInScreen方法的最后,view类设置workspace长按监听器

    
        child.setHapticFeedbackEnabled(false);
        child.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
        if (child instanceof DropTarget) {
            mDragController.addDropTarget((DropTarget) child);
        }

 ItemLongClickListener 分为INSTANCE_WORKSPACE和INSTANCE_ALL_APPS两种;本次只分析workspace长按监听器,

长按事件传递过来的 View 便是我们长按的应用图标,v的实质便是 BubbleTextView。

ItemLongClickListener.java

 private static boolean onWorkspaceItemLongClick(View v) {
        Launcher launcher = Launcher.getLauncher(v.getContext());
        if (!canStartDrag(launcher)) return false;
        if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
        if (!(v.getTag() instanceof ItemInfo)) return false;

        launcher.setWaitingForResult(null);
        beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
        return true;
    }

    public static void beginDrag(View v, Launcher launcher, ItemInfo info,
            DragOptions dragOptions) {
        if (info.container >= 0) {
            Folder folder = Folder.getOpen(launcher);
            if (folder != null) {
                if (!folder.getItemsInReadingOrder().contains(v)) {
                    folder.close(true);
                } else {
                    folder.startDrag(v, dragOptions);
                    return;
                }
            }
        }

        CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info);
        launcher.getWorkspace().startDrag(longClickCellInfo, dragOptions);
    }

Workspace.startDrag

判断各种状态后,又回到workspace的startDrag.  其中child.setVisibility(INVISIBLE);  设置view即icon不可见;接着beginDragShared(child, this, options); 

beginDragShared的参数(View child, DragSource source, DragOptions options);

-->beginDragShared(child, source, (ItemInfo) dragObject, new DragPreviewProvider(child), options);

DragSource 表示本次拖拽的应用图标来自哪里,在 beginDragShared() 中传入的参数是 this,也即本次拖拽来自 Workspace;

DragPreviewProvider 预览位图和轮廓线相关的类。轮廓线就是用来指示你图标会被放在哪个位置的,你不断拖动,这个轮廓线的位置也在不断变化。
 

    public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
            DragPreviewProvider previewProvider, DragOptions dragOptions) {
         ............
        //放弃焦点,将按下的状态设置成false,轮廓线等
        child.clearFocus();
        child.setPressed(false);
        mOutlineProvider = previewProvider;

        // The drag bitmap follows the touch point around on the screen
        final Bitmap b = previewProvider.createDragBitmap();
        int halfPadding = previewProvider.previewPadding / 2;
        
        float scale = previewProvider.getScaleAndPosition(b, mTempXY);
        int dragLayerX = mTempXY[0];
        int dragLayerY = mTempXY[1];

        DeviceProfile grid = mLauncher.getDeviceProfile();
        Point dragVisualizeOffset = null;
        Rect dragRect = null;
        if (child instanceof BubbleTextView) {
            dragRect = new Rect();
            ((BubbleTextView) child).getIconBounds(dragRect);
            dragLayerY += dragRect.top;
            // Note: The dragRect 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);
        } 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);
        } else if (previewProvider instanceof ShortcutDragPreviewProvider) {
            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
        }

        // Clear the pressed state if necessary
        if (child instanceof BubbleTextView) {
            BubbleTextView icon = (BubbleTextView) child;
            icon.clearPressedBackground();
        }

        if (child.getParent() instanceof ShortcutAndWidgetContainer) {
            mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
        }

        if (child instanceof BubbleTextView && !dragOptions.isAccessibleDrag) {
            PopupContainerWithArrow popupContainer = PopupContainerWithArrow
                    .showForIcon((BubbleTextView) child);
            if (popupContainer != null) {
                dragOptions.preDragCondition = popupContainer.createPreDragCondition();

                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("dragging started");
            }
        }

        DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
                dragObject, dragVisualizeOffset, dragRect, scale * iconScale, scale, dragOptions);
        dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
        return dv;
    }

  DragController是桌面里面的拖拽控制器,一旦他的startDrag执行完了,就可以接收滑动的触摸消息了。 

DragController.startDrag

public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
            DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
            float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) {
        if (PROFILE_DRAWING_DURING_DRAG) {
            android.os.Debug.startMethodTracing("Launcher");
        }

        // Hide soft keyboard, if visible
     // 隐藏软件盘
        UiThreadHelper.hideKeyboardAsync(mLauncher, mWindowToken);

        mOptions = options;
        if (mOptions.systemDndStartPoint != null) {
            mMotionDownX = mOptions.systemDndStartPoint.x;
            mMotionDownY = mOptions.systemDndStartPoint.y;
        }

        final int registrationX = mMotionDownX - dragLayerX;
        final int registrationY = mMotionDownY - dragLayerY;

        final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
        final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;

        mLastDropTarget = null;

        //非常重要的对象
        mDragObject = new DropTarget.DragObject();

        mIsInPreDrag = mOptions.preDragCondition != null
                && !mOptions.preDragCondition.shouldStartDrag(0);

        final Resources res = mLauncher.getResources();
        final float scaleDps = mIsInPreDrag
                ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;

      // 创建DragView对象,bitmap化身view
        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
                registrationY, initialDragViewScale, dragViewScaleOnDrop, scaleDps);
        dragView.setItemInfo(dragInfo);
        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);

            //创建DragDriver,后面会用到
            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对象(将该DragView添加到DragLayer上)
        dragView.show(mMotionDownX, mMotionDownY);
        mDistanceSinceScroll = 0;

        //监听事件
        if (!mIsInPreDrag) {
            callOnDragStart();
        } else if (mOptions.preDragCondition != null) {
            mOptions.preDragCondition.onPreDragStart(mDragObject);
        }

        
        mLastTouch[0] = mMotionDownX;
        mLastTouch[1] = mMotionDownY;
        handleMoveEvent(mMotionDownX, mMotionDownY);//处理当前移动事件,是整个过程的一个很重要的方法
        mLauncher.getUserEventDispatcher().resetActionDurationMillis();
        return dragView;
    }

分析以上代码内容

DragObject:

 new DropTarget.DragObject() 是整个拖拽框架中最有“实权”的实例对象;

它包含了拖拽的视图代表 DragView;包含被拖拽的应用 icon(BubbleTextView) 数据 ItemInfo;包含拖拽的源头 DragSource;在整个拖拽过程中,接受控制中心 DragController 的执行指令,是指令的首要执行者,DragObject 会伴随整个拖拽过程,对 DragView、icon(BubbleTextView)、DragSource 有绝对“控制权”,如控制 DragView 的显示、消失和位置等。

DragDriver:

是驱动管理 Drag/Drop操作的基本类;还包含Driver拖拽事件接口;而DragController实现了事件接口,后面会用到  

public interface EventListener {
        void onDriverDragMove(float x, float y);
        void onDriverDragExitWindow();
        void onDriverDragEnd(float x, float y);
        void onDriverDragCancel();
    }


监听事件:

拖拽开始时,通过回调 onDragStart() 通知监听者拖拽已经开始,在 Launcher3 中,注册拖拽监听者的有 Folder 、Workspace 、ButtonDropTarget.java、 SecondDropTarget.java、 DeleteDropTarget.java、WidgetHostViewLoader??等.

DropTarget接口

  • onDragEnter是当拖动图标到某一DropTarget(蓝色),边缘,刚刚进入DropTarget范围内的时候所调用的内容。比如说我们拖动桌面的一个快捷方式,到桌面顶端的删除区域,“删除”两字和手中的图标会变红,这些动作都是在onDragEnter回调中完成的
  • onDragOver是在某一DropTarget内部移动的时候会调用的回调,比如我们把手上的图标移动到两个图标中间的时候,会发生挤位的情况(就是桌面已有图标让出空位),基本上每个ACTION_MOVE操作都会调用他。
  • onDragExit是从某一DropTarget拖出时候会进行的回调,比如onDragEnter时变红的“删除”和图标会在这个调用中恢复正常。
  • onDrop是松手时候发生的调用,做一些放下时候的操作,比如删除快捷方式的时候会在onDrop里面开始删除的操作。

handleMoveEvent:

直接调用 handleMoveEvent() 方法,这个方法很重要,它是整个拖拽过程实现的基本条件,因此,这个方法在拖拽过程中会被调用非常多次数,也就是处理我们拖拽的时候的手指移动事件;后面详细介绍。

到此,拖拽的开始和准别工作到这里已经彻底完成,下面将进入真正的拖拽过程。  

第二步:拖拽(触摸事件的拦截和分发

在上一章节中,bitmap 为何一个生成一个DragView ?我们知道,一个子 View 是不能越界到它的父控件的外面,而我们的 icon(BubbleTextView) 是要能拖到界面上得任何一个位置的。所以,Launcher 有一个覆盖整个 Laucher 界面的 DragLayer,而 DragView 便是依附在 DragLayer 作为它的直接子 View,便能在整个 Launcher 界面上移动。 

onInterceptTouchEvent

    DragLayer继承自ViewGroup,其onInterceptTouchEvent方法若返回true,说明需要拦截触屏事件,则后续的一系列事件将传递给自身的onTouchEvent方法,而不再向其子控件传递。DragController的onInterceptTouchEvent由DragLayer的onInterceptTouchEvent调用,用于拦截触屏事件的处理。当用户点击屏幕时,触发ACTION_DOWN事件,记录当前触摸位置。当抬起时,触发ACTION_UP事件,结束拖拽。若抬起时处于拖拽中,在当前位置释放被拖拽物。因此,若此时处于拖拽中,后续的触屏事件将只传递到DragLayer的onTouchEvent。


onTouchEvent

    onTouchEvent处理触屏事件,若返回true,则表示消费掉该事件,事件不再向父控件的onTouchEvent传递。DragController的onTouchEvent由DragLayer的onTouchEvent调用,用于处理被拖拽物的移动。当startDrag执行完毕,DragController设置拖拽状态为true,这样,触屏事件将最终转到onTouchEvent中,在此处调用handleMoveEvent进行物体的移动。

      DragLayer是Launcher上所有布局的父容器,主要进行事件的拦截和分发,但是具体工作都是交由DragController来处理的,我们直接看DragController的onInterceptTouchEvent的拦截处理。

   首先看DragLayer本身没有onInterceptTouchEvent,查看父类BaseDragLayer的;


public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (mTouchCompleteListener != null) {
                mTouchCompleteListener.onTouchComplete();
            }
            mTouchCompleteListener = null;
        } else if (action == MotionEvent.ACTION_DOWN) {
            mActivity.finishAutoCancelActionMode();
        }
        return findActiveController(ev);
    }

    //判断是哪一类touchcontroller
    protected boolean findActiveController(MotionEvent ev) {
        mActiveController = null;

        //浮动按钮
        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
        if (topView != null && topView.onControllerInterceptTouchEvent(ev)) {
            mActiveController = topView;
            return true;
        }

        //mController是DragControler()还是AllAppsSwipeController()
        for (TouchController controller : mControllers) {
            if (controller.onControllerInterceptTouchEvent(ev)) {
                mActiveController = controller;
                return true;
            }
        }
        return false;
    }

 onControllerInterceptTouchEvent 是TouchController的一个接口;   

 从代码可以看出首先判断是否浮动View,然后根据DragController的onControllerInterceptTouchEvent处理结果确定返回;

是否拦截看返回值。


    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        if (mOptions != null && mOptions.isAccessibleDrag) {
            return false;
        }

        // Update the velocity tracker
        mFlingToDeleteHelper.recordMotionEvent(ev);

        final int action = ev.getAction();
        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
        final int dragLayerX = dragLayerPos[0];
        final int dragLayerY = dragLayerPos[1];

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // Remember location of down touch
                mMotionDownX = dragLayerX;
                mMotionDownY = dragLayerY;
                break;
            case MotionEvent.ACTION_UP:
                mLastTouchUpTime = System.currentTimeMillis();
                break;
        }

        return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
    } 

DragDriver 

   public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

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

        return true;
    }

MotionEvent.ACTION_DOWN/MOVE/ATCION_UP/CANCEL 返回true;所以DragLayer对Touch事件进行了拦截在BaseDragController的onTouchEvent中进行处理。

public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (mTouchCompleteListener != null) {
                mTouchCompleteListener.onTouchComplete();
            }
            mTouchCompleteListener = null;
        }

        if (mActiveController != null) {
            return mActiveController.onControllerTouchEvent(ev);
        } else {
            // In case no child view handled the touch event, we may not get onIntercept anymore
            return findActiveController(ev);
        }
    }

DragController

public boolean onControllerTouchEvent(MotionEvent ev) {
        if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) {
            return false;
        }

        // Update the velocity tracker
        mFlingToDeleteHelper.recordMotionEvent(ev);

        final int action = ev.getAction();
        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
        final int dragLayerX = dragLayerPos[0];
        final int dragLayerY = dragLayerPos[1];

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // Remember where the motion event started
                mMotionDownX = dragLayerX;
                mMotionDownY = dragLayerY;
                break;
        }

        return mDragDriver.onTouchEvent(ev);
    }

 DragDriver 定义了事件接口,DragController实现了事件接口; 前面DragController.startDrag创建DragDriver时传递this;所以所有的事件处理又回到DragController实现.

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());
                break;
            case MotionEvent.ACTION_CANCEL:
                mEventListener.onDriverDragCancel();
                break;
        }

        return true;
    }

  DragController 又回到事件处理

 @Override
    public void onDriverDragMove(float x, float y) {
        final int[] dragLayerPos = getClampedDragLayerPos(x, y);

        handleMoveEvent(dragLayerPos[0], dragLayerPos[1]);
    }

handleMoveEvent:

直接调用 handleMoveEvent() 方法,这个方法很重要,它是整个拖拽过程实现的基本条件,因此,这个方法在拖拽过程中会被调用非常多次数,也就是处理我们拖拽的时候的手指移动事件;

1)移动DragView到手指的位置   

2) 查找拖拽目标DropTarget;DragView 放下的目标,即 icon(BubbleTextView) 移到的新的位置  

      DropTarget是一个可放置(drop)区域的抽象,也就是我们松开手的时候想要把图标放到某个东西上,这个东西就是DropTarget,实现他的都是View,比如说文件夹,Workspace,删除区等等,你可以通过“ Open Type Hierarchy”来查看哪些类继承了DropTarget接口。包含比较重要的几个接口

  • onDragEnter是当拖动图标到某一DropTarget(蓝色),边缘,刚刚进入DropTarget范围内的时候所调用的内容。比如说我们拖动桌面的一个快捷方式,到桌面顶端的删除区域,“删除”两字和手中的图标会变红,这些动作都是在onDragEnter回调中完成的
  • onDragOver是在某一DropTarget内部移动的时候会调用的回调,比如我们把手上的图标移动到两个图标中间的时候,会发生挤位的情况(就是桌面已有图标让出空位),基本上每个ACTION_MOVE操作都会调用他。
  • onDragExit是从某一DropTarget拖出时候会进行的回调,比如onDragEnter时变红的“删除”和图标会在这个调用中恢复正常。
  • onDrop是松手时候发生的调用,做一些放下时候的操作,比如删除快捷方式的时候会在onDrop里面开始删除的操作

3)  检查拖动时Target的状态- 4、翻页距离计算  5 满足条件回调

    private void handleMoveEvent(int x, int y) {
        mDragObject.dragView.move(x, y);

        // Drop non someoe?
        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;

        if (mIsInPreDrag && mOptions.preDragCondition != null
                && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) {
            callOnDragStart();
        }
    }

进入findDropTarget,有一个 mDropTargets 的变量列表,保存了所有的 Target,这些 Target 在 Launcher 启动的时候就通过方法 addDropTarget(DropTarget target) 添加到 DrageController 中来。包括 Workspace、Folder、ButtonDropTarget 等。在这么多 Target 中,如何判断当前是移到哪个 Target 呢?通过 target 的实例调用 getHitRectRelativeToDragLayer(r) 方法,获取到 target 所处的位置 r,通过 r.contains(x, y) 判断, x, y 是否包含在 target 内,如果是,则手指移到了该 target,同时把 x,y 保存到 target 实例中,返回该 target 对象。未找到DropTarget则交给Workspace 处理,找到合适celllayout 处理逻辑。
 

    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
        mDragObject.x = x;
        mDragObject.y = y;

        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.getHitRectRelativeToDragLayer(r);
            if (r.contains(x, y)) {
                dropCoordinates[0] = x;
                dropCoordinates[1] = y;
                mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates);
                return target;
            }
        }
        // Pass all unhandled drag to workspace. Workspace finds the correct
        // cell layout to drop to in the existing drag/drop logic.
        dropCoordinates[0] = x;
        dropCoordinates[1] = y;
        mLauncher.getDragLayer().mapCoordInSelfToDescendant(mLauncher.getWorkspace(),
                dropCoordinates);
        return mLauncher.getWorkspace();
    }

回到handleMoveEvent继续,

如果前后两次的 target 实例不一致,说明 target 发生变化,通过调用 onDragExit() 方法通知上一次的 target 拖拽已经移走;然后通过 onDragEnter() 方法通知当前 target,拖拽已经移动进来。同时,通过 onDragOver() 通知 target 拖拽已经移到你的上面,准确的说,是 DragView 移到了 target 的上面。

在这里,每个方法的参数都是 mDragObject 实例,在上文中我们知道,mDragObject 在拖拽中是最有“实权”的“人物”,拥有视图的化身 DragView,保存有 icon(BubbleTextView)的数据,持有拖拽的来源 DragSource,同时,mDragObject 到达了 target,也可以操作 target 对象实例本身,因此,在 target 可以发生一切事情。
 

    private void checkTouchMove(DropTarget dropTarget) {
        if (dropTarget != null) {
            if (mLastDropTarget != dropTarget) {
                if (mLastDropTarget != null) {
                    mLastDropTarget.onDragExit(mDragObject);
                }
                dropTarget.onDragEnter(mDragObject);
            }
            dropTarget.onDragOver(mDragObject);
        } else {
            if (mLastDropTarget != null) {
                mLastDropTarget.onDragExit(mDragObject);
            }
        }
        mLastDropTarget = dropTarget;
    }

 

拖拽结束

         当用户将被拖拽物移动到相应位置后,可以将手指从屏幕上移开。此时,将在onInterceptTouchEvent与onTouchEvent中调用drop方法释放被拖拽物。其主要功能,就是查找拖拽目的对象(DropTarget),若找到且接受释放,通知该对象被拖拽物的放入。最后,通知拖拽源(被拖拽物最初所在的容器)拖拽结果。

DragDrvier.java
 
  case MotionEvent.ACTION_UP:
                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());

DragController.java

   public void onDriverDragEnd(float x, float y) {
        DropTarget dropTarget;
        
        Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject);
        if (flingAnimation != null) {
            dropTarget = mFlingToDeleteHelper.getDropTarget();
        } else {
            dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp);
        }

        drop(dropTarget, flingAnimation);

        endDrag();
    }

事件处理进入onDriverDrayEnd后 ,判断是否快速甩向屏幕上方触发删除的事件。isFlingingToDelete检查你是否在执行甩的动作是通过VelocityTracker来计算的,他会记录每次事件,然后计算出速度,包括X方向和Y方向的。如果不是在“扔”,就会调用放下图标最重要的一个方法, drop方法(flingAnimation=null),不管哪种删除最后都是通过completeDrop(...)删除);

    进行不同的处理(如下代码)

    private void drop(DropTarget dropTarget, Runnable flingAnimation) {
        final int[] coordinates = mCoordinatesTemp;
        mDragObject.x = coordinates[0];
        mDragObject.y = coordinates[1];

        // Move dragging to the final target.
        if (dropTarget != mLastDropTarget) {
            if (mLastDropTarget != null) {
                mLastDropTarget.onDragExit(mDragObject);
            }
            mLastDropTarget = dropTarget;
            if (dropTarget != null) {
                dropTarget.onDragEnter(mDragObject);
            }
        }

        mDragObject.dragComplete = true;    //设置标志
        if (mIsInPreDrag) {
            if (dropTarget != null) {
                dropTarget.onDragExit(mDragObject);
            }
            return;
        }

        // Drop onto the target.
        boolean accepted = false;
        if (dropTarget != null) {
            dropTarget.onDragExit(mDragObject);  //通知拖拽目的对象已离开
            if (dropTarget.acceptDrop(mDragObject)) {  //支持放入
                if (flingAnimation != null) {
                    flingAnimation.run();   //fling效果并删除
                } else {
                    dropTarget.onDrop(mDragObject, mOptions);
                }
                accepted = true;
            }
        }
        final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
        mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView);
        dispatchDropComplete(dropTargetAsView, accepted);
    }

   drop 会通过当前的坐标找到需要放到的DropTarget,放下的操作首先得保证有DropTarget,然后DropTarget还得需要接受你手上的图标,比如你把图标拖到已经放满的Hotseat上去肯定不能被接受(DropTarget.acceptDrop返回false)。被接受了就可以调用当前DropTarget的onDrop方法了,Workspace folder  buttonDropTarget等都实现onDrop);以Workspace为例,它做了一下几件事:

public void onDrop(final DragObject d, DragOptions options) {
        mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);// 计算拖动View的视觉中心
        CellLayout dropTargetLayout = mDropToLayout;// Drop的Celllayout对象;DragOver时设置

        // 判断当前是否在Hotseat上,求出相对于dropTargetLayout的视觉中心坐标
        // We want the point to be mapped to the dragTarget.
        if (dropTargetLayout != null) {
            if (mLauncher.isHotseatLayout(dropTargetLayout)) {
                mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
            } else {
                mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter);
            }
        }

        boolean droppedOnOriginalCell = false;
     
        // 如果DragObject.dragSource!= Worspace,转而调用onDropExternal(),否则继续处理onDrop()的内容
        int snapScreen = -1;
        boolean resizeOnDrop = false;
        if (d.dragSource != this || mDragInfo == null) {
            final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1] };
            onDropExternal(touchXY, dropTargetLayout, d); 
        } else {
            final View cell = mDragInfo.cell;
            boolean droppedOnOriginalCellDuringTransition = false;
            Runnable onCompleteRunnable = null;

            if (dropTargetLayout != null && !d.cancelled) {
                // Move internally
                boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout);
                boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
                long container = hasMovedIntoHotseat ?
                        LauncherSettings.Favorites.CONTAINER_HOTSEAT :
                        LauncherSettings.Favorites.CONTAINER_DESKTOP;
                long screenId = (mTargetCell[0] < 0) ?
                        mDragInfo.screenId : getIdForScreen(dropTargetLayout);
                int spanX = mDragInfo != null ? mDragInfo.spanX : 1;
                int spanY = mDragInfo != null ? mDragInfo.spanY : 1;
                // First we find the cell nearest to point at which the item is
                // dropped, without any consideration to whether there is an item there.

                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 (createUserFolderIfNecessary(cell, container,
                        dropTargetLayout, mTargetCell, distance, false, d.dragView) ||
                        addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
                                distance, d, false)) {
                    mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
                    return;
                }

                // Aside from the special case where we're dropping a shortcut onto a shortcut,
                // we need to find the nearest cell location that is vacant
                ItemInfo item = d.dragInfo;
                int minSpanX = item.spanX;
                int minSpanY = item.spanY;
                if (item.minSpanX > 0 && item.minSpanY > 0) {
                    minSpanX = item.minSpanX;
                    minSpanY = item.minSpanY;
                }

                droppedOnOriginalCell = item.screenId == screenId && item.container == container
                        && item.cellX == mTargetCell[0] && item.cellY == mTargetCell[1];
                droppedOnOriginalCellDuringTransition = droppedOnOriginalCell && mIsSwitchingState;

                // When quickly moving an item, a user may accidentally rearrange their
                // workspace. So instead we move the icon back safely to its original position.
                boolean returnToOriginalCellToPreventShuffling = !isFinishedSwitchingState()
                        && !droppedOnOriginalCellDuringTransition && !dropTargetLayout
                        .isRegionVacant(mTargetCell[0], mTargetCell[1], spanX, spanY);
                int[] resultSpan = new int[2];
                if (returnToOriginalCellToPreventShuffling) {
                    mTargetCell[0] = mTargetCell[1] = -1;
                } else {
                    //处理拖动图标时,如果当前落点被占据时,挤开当前图标的效果
                    mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                            (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
                            mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);
                }

                boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;

                // if the widget resizes on drop //AppWidget可能在拖动时发生缩小
                if (foundCell && (cell instanceof AppWidgetHostView) &&
                        (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) {
                    resizeOnDrop = true;
                    item.spanX = resultSpan[0];
                    item.spanY = resultSpan[1];
                    AppWidgetHostView awhv = (AppWidgetHostView) cell;
                    AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
                            resultSpan[1]);
                }

                // 如果满足则更新位置,保存新的位置信息到数据库中,播放动画效果,否则弹回原来位置
                if (foundCell) {
                     // 拖动时可能落点在别的页面,所以还会有页面滑动的效果
                    if (getScreenIdForPageIndex(mCurrentPage) != screenId && !hasMovedIntoHotseat) {
                        snapScreen = getPageIndexForScreenId(screenId);
                        snapToPage(snapScreen);
                    }

 
                    final ItemInfo info = (ItemInfo) cell.getTag();
                    if (hasMovedLayouts) {
                        // Reparent the view
                        CellLayout parentCell = getParentCellLayoutForView(cell);
                        if (parentCell != null) {
                            parentCell.removeView(cell);
                        } else if (FeatureFlags.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;

                    if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
                            cell instanceof LauncherAppWidgetHostView) {
                        final CellLayout cellLayout = dropTargetLayout;
                        // We post this call so that the widget has a chance to be placed
                        // in its final location

                        final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                        AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
                        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
                                && !d.accessibleDrag) {
                            onCompleteRunnable = new Runnable() {
                                public void run() {
                                    if (!isPageInTransition()) {
                                        AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
                                    }
                                }
                            };
                        }
                    }

                    //修改数据库
                    mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
                            lp.cellX, lp.cellY, item.spanX, item.spanY);
                } else {
                    //没有找到cell
                    if (!returnToOriginalCellToPreventShuffling) {
                        onNoCellFound(dropTargetLayout);
                    }

                    // 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();
            if (d.dragView.hasDrawn()) {
                if (droppedOnOriginalCellDuringTransition) {
                    // Animate the item to its original position, while simultaneously exiting
                    // spring-loaded mode so the page meets the icon where it was picked up.
                    mLauncher.getDragController().animateDragViewToOriginalPosition(
                            onCompleteRunnable, cell, SPRING_LOADED_TRANSITION_MS);
                    mLauncher.getStateManager().goToState(NORMAL);
                    mLauncher.getDropTargetBar().onDragEnd();
                    parent.onDropChild(cell);
                    return;
                }
                final ItemInfo info = (ItemInfo) cell.getTag();
                boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
                        || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
                if (isWidget) {
                    int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
                            ANIMATE_INTO_POSITION_AND_DISAPPEAR;
                    animateWidgetDrop(info, parent, d.dragView, null, animationType, cell, false);
                } else {
                    int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION;
                    mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
                            this);
                }
            } else {
                d.deferDragViewCleanupPostAnimation = false;
                cell.setVisibility(VISIBLE);
            }
            parent.onDropChild(cell);
            
            //设置OVERVIEW-->NORMAL状态
            mLauncher.getStateManager().goToState(
                    NORMAL, SPRING_LOADED_EXIT_DELAY, onCompleteRunnable);
        }

        if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
            d.stateAnnouncer.completeAction(R.string.item_moved);
        }
    }
DropExternal() 功能,有点累了,不翻译了
 *Drop an item that didn't originate on one of the workspace screens.
* It may have come from Launcher (e.g. from all apps or customize), or it may have
* come from another app altogether.

 调用 onDropCompleted() 并传送拖拽结果通知拖拽源(被拖拽物最初所在的容器)拖拽结果 

    private void dispatchDropComplete(View dropTarget, boolean accepted) {
        if (!accepted) {
            // If it was not accepted, cleanup the state. If it was accepted, it is the
            // responsibility of the drop target to cleanup the state.
            mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
            mDragObject.deferDragViewCleanupPostAnimation = false;
        }

        mDragObject.dragSource.onDropCompleted(dropTarget, mDragObject, accepted);
    }
   /**
     * Called at the end of a drag which originated on the workspace.
     */
    public void onDropCompleted(final View target, final DragObject d,
            final boolean success) {

        if (success) {
            if (target != this && mDragInfo != null) {
                removeWorkspaceItem(mDragInfo.cell);
            }
        } else if (mDragInfo != null) {
            final CellLayout cellLayout = mLauncher.getCellLayout(
                    mDragInfo.container, mDragInfo.screenId);
            if (cellLayout != null) {
                cellLayout.onDropChild(mDragInfo.cell);
            } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
                throw new RuntimeException("Invalid state: cellLayout == null in "
                        + "Workspace#onDropCompleted. Please file a bug. ");
            }
        }
        View cell = getHomescreenIconByItemId(d.originalDragInfo.id);
        if (d.cancelled && cell != null) {
            cell.setVisibility(VISIBLE);
        }
        mDragInfo = null;
    }

大结局

回到OnDriverDragEnd. 结束拖拽DragController.endDrag(),这个方法会清理dragDriver/dragView,

如果不是 Deferred,立即执行把 DragView 从 DragLayer 中移除,然后通知监听者 onDragEnd()

调用DragListener的onDragEnd方法。最后释放VelocityTracker,等待下一轮触摸事件的来临。

    private void endDrag() {
        if (isDragging()) {
            mDragDriver = null;
            boolean isDeferred = false;
            if (mDragObject.dragView != null) {
                isDeferred = mDragObject.deferDragViewCleanupPostAnimation;
                if (!isDeferred) {
                    mDragObject.dragView.remove();
                } else if (mIsInPreDrag) {
                    animateDragViewToOriginalPosition(null, null, -1);
                }
                mDragObject.dragView = null;
            }

            // Only end the drag if we are not deferred
            if (!isDeferred) {
                callOnDragEnd();
            }
        }

        mFlingToDeleteHelper.releaseVelocityTracker();
    }

总结      

在 Launcher 的拖拽框架中,由 DragController 担当指挥中心,用 DragSource 抽象拖拽的来源,用 DragView 描绘拖拽视图,设置 DragListener 通知拖拽的开始和结束,整个拖拽过程中,由 DragObject 执行拖拽事务,与 DragSource 相对的 DropTarget 描述拖拽的目的地,DragSource、 DropTarget 代表“你从哪里来,你到哪里去”,是拖拽框架中核心问题的抽象。当长按应用 icon 触发后,DragLayer 把触摸事件拦截,再传递给控制中心 DragControlller 处理。拖拽结束后,在 DropTarget 的 onDrop() 方法中处理拖拽的结束的事务。拖拽以 DragListener 的 onStartDrag() 标志拖拽的开始,以 onDragEnd() 标志拖拽的完全结束。
 

拖拽控制框架图

以从 Workspace 拖拽一个 icon(BubbleTextView)到另外一个位置为例

拖拽时序图

 

 

参考:

Launcher进阶三——Launcher拖拽框架

Launcher3分析之拖动图标的流程

Launcher3--拖拽

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值