【Launcher开发】拖拽过程分析(下)

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

    


    








  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值