com.google.blockly.android.ui.Dragger.java Source code

Java tutorial

Introduction

Here is the source code for com.google.blockly.android.ui.Dragger.java

Source

/*
 *  Copyright 2015 Google Inc. All Rights Reserved.
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.google.blockly.android.ui;

import android.content.ClipData;
import android.graphics.Rect;
import android.os.Handler;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.MotionEventCompat;
import android.util.Log;
import android.util.Pair;
import android.view.DragEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import com.google.blockly.android.control.BlocklyController;
import com.google.blockly.android.control.ConnectionManager;
import com.google.blockly.model.Block;
import com.google.blockly.model.Connection;
import com.google.blockly.model.Workspace;
import com.google.blockly.model.WorkspacePoint;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;

/**
 * Controller for dragging blocks and groups of blocks within a workspace.
 */
// TODO(#233): Rename to BlockViewTouchManager or similar
public class Dragger {
    private static final String TAG = "Dragger";
    private static final boolean LOG_TOUCH_EVENTS = false;
    private static final boolean LOG_DRAG_EVENTS = false;

    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();

    // Modes of finishDragging()
    private static final int FINISH_BEHAVIOR_DROP = 1;
    private static final int FINISH_BEHAVIOR_REVERT = 2;
    private static final int FINISH_BEHAVIOR_DELETED = 3;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ FINISH_BEHAVIOR_DROP, FINISH_BEHAVIOR_REVERT, FINISH_BEHAVIOR_DELETED })
    public @interface FinishDragBehavior {
    }

    /**
     * Interface for processing a drag behavior.
     */
    // TODO(#233): Include clicks. Rename. BlockTouchHandler sounds good, but...
    public interface DragHandler {
        /**
         * This method checks whether the pending drag maps to a valid draggable {@link BlockGroup}
         * on the workspace.  If it does, it should return a {@link Runnable} that will perform (at
         * a later time) the necessary {@link Block} and {@link BlockView} manipulations to
         * construct that drag group, and assign it to the {@link PendingDrag}.  Such manipulations
         * must not occur immediately, because this can result in recursive touch events.  The
         * {@link Dragger} is designed to catch these calls and forcibly crash.  Just don't do it.
         * <p/>
         * When the {@link Runnable} is called, it should proceed with the {@code Block} and
         * {@code BlockView} manipulations, and call {@link PendingDrag#setDragGroup(BlockGroup)} to
         * assign the draggable {@link BlockGroup}, which must contain a root block on the
         * {@link Workspace} and be added to the {@link WorkspaceView}.
         * <p/>
         * If pending drag does not map to a draggable, this method should return null.
         *
         * @param pendingDrag The pending drag state in question.
         */
        @Nullable
        Runnable maybeGetDragGroupCreator(PendingDrag pendingDrag);

        /**
         * Handles click events on blocks.
         *
         * @param pendingDrag The pending drag state in question.
         * @return True if click was processed and event should be captured.
         */
        boolean onBlockClicked(PendingDrag pendingDrag);

        // TODO(#202): onDragCancel(BlockGroup dragGroup) to support invalid drop locations.
        //     For instance, returning a block to the trash. Currently drops at the last move
        //     location.
    }

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ DRAG_MODE_IMMEDIATE, DRAG_MODE_SLOPPY })
    public @interface DragMode {
    }

    @VisibleForTesting
    static final int DRAG_MODE_IMMEDIATE = 0;
    @VisibleForTesting
    static final int DRAG_MODE_SLOPPY = 1;

    private final ArrayList<Connection> mDraggedConnections = new ArrayList<>();
    // For use in bumping neighbours; instance variable only to avoid repeated allocation.
    private final ArrayList<Connection> mTempConnections = new ArrayList<>();
    // Rect for finding the bounding box of the trash can view.
    private final Rect mTrashRect = new Rect();
    // For use in getting location on screen.
    private final int[] mTempScreenCoord1 = new int[2];
    private final int[] mTempScreenCoord2 = new int[2];
    private final ViewPoint mTempViewPoint = new ViewPoint();
    private final WorkspacePoint mTempWorkspacePoint = new WorkspacePoint();

    private Handler mMainHandler;
    private final BlocklyController mController;
    private final WorkspaceHelper mHelper;
    private final Workspace mWorkspace;
    private final ConnectionManager mConnectionManager;

    /**
     * This flags helps check {@link #onTouchBlockImpl} is not called recursively, which can occur
     * when the view hierarchy is manipulated during event handling.
     */
    private boolean mWithinOnTouchBlockImpl = false;

    private PendingDrag mPendingDrag;
    private Runnable mLogPending = (LOG_TOUCH_EVENTS || LOG_DRAG_EVENTS) ? new Runnable() {
        @Override
        public void run() {
            Log.d(TAG,
                    (mPendingDrag == null ? "\tnot pending"
                            : (mPendingDrag.getDragGroup() == null
                                    ? "\tpending: touched = " + mPendingDrag.getTouchedBlockView()
                                    : "\tdragging: " + mPendingDrag.getDragGroup())));
        }
    } : null;

    // Which {@link BlockView} was touched, and possibly may be being dragged.
    private WorkspaceView mWorkspaceView;
    private BlockView mHighlightedBlockView;
    // The view for the trash can.
    private View mTrashView;
    //The square of the required touch slop before starting a drag, precomputed to avoid
    // square root operations at runtime.
    private float mTouchSlopSquared = 0.0f;

    private View.OnDragListener mDragEventListener = new View.OnDragListener() {
        @Override
        public boolean onDrag(View workspaceView, DragEvent event) {
            final int action = event.getAction();

            if (LOG_DRAG_EVENTS) {
                String actionName = (action == DragEvent.ACTION_DRAG_STARTED) ? "DRAG_STARTED"
                        : (action == DragEvent.ACTION_DRAG_LOCATION) ? "DRAG_LOCATION"
                                : (action == DragEvent.ACTION_DRAG_ENDED) ? "DRAG_ENDED"
                                        : (action == DragEvent.ACTION_DROP) ? "DROP" : "UNKNOWN ACTION #" + action;
                Log.d(TAG, "onDrag: " + actionName + ", " + event);

                mMainHandler.removeCallbacks(mLogPending); // Only log once per event tick
                mMainHandler.post(mLogPending);
            }

            if (mPendingDrag != null && mPendingDrag.isDragging()) {
                switch (action) {
                case DragEvent.ACTION_DRAG_STARTED:
                    // Triggered in maybeStartDrag(..).
                    // The rest of the drag data is already captured in mPendingDrag.
                    // NOTE: This event position does not respect view scale.

                    BlockView rootDraggedBlockView = mPendingDrag.getRootDraggedBlockView();
                    if (rootDraggedBlockView.getBlock().isMovable()) {
                        // TODO(#35): This might be better described as "selected".
                        ((View) rootDraggedBlockView).setPressed(true);
                        return true; // We want to keep listening for drag events
                    } else {
                        return false; // We don't want to keep listening for drag events
                    }
                case DragEvent.ACTION_DRAG_LOCATION:
                    // If we're still finishing up a previous drag we may have missed the
                    // start of the drag, in which case we shouldn't do anything.
                    continueDragging(event);
                    break;
                case DragEvent.ACTION_DRAG_ENDED:
                    // TODO(#202): Cancel pending drag?
                    if (event.getResult()) {
                        break;
                    }
                    // Otherwise fall through
                case DragEvent.ACTION_DROP:
                    // Finalize dragging and reset dragging state flags.
                    // These state flags are still used in the initial phase of figuring out if a
                    // drag has started.
                    int finishBehavior;
                    if (touchingTrashView(event)) {
                        if (dropInTrash()) {
                            finishBehavior = FINISH_BEHAVIOR_DELETED;
                        } else {
                            finishBehavior = FINISH_BEHAVIOR_REVERT;
                        }
                    } else {
                        maybeConnectDragGroup();
                        finishBehavior = FINISH_BEHAVIOR_DROP;
                    }
                    finishDragging(finishBehavior);
                    return true; // The drop succeeded.
                default:
                    break;
                }
            }
            return false; // In every case that gets here, the return value won't be checked.
        }
    };

    /**
     * @param blocklyController The {@link BlocklyController} managing Blocks in this activity.
     */
    public Dragger(BlocklyController blocklyController) {
        mController = blocklyController;
        mWorkspace = blocklyController.getWorkspace();
        mHelper = blocklyController.getWorkspaceHelper();
        mConnectionManager = mWorkspace.getConnectionManager();

        mMainHandler = new Handler();
    }

    /**
     * @param slop The required touch slop before starting a drag.
     */
    public void setTouchSlop(float slop) {
        mTouchSlopSquared = slop * slop;
    }

    /**
     * Remove all the connections in a blocks tree from the list of connections being dragged. This
     * is used when removing shadow blocks from a block tree during a drag. If there's no drag
     * in progress this has no effects.
     *
     * @param rootBlock The start of the block tree to remove connections for.
     */
    public void removeFromDraggingConnections(Block rootBlock) {
        if (mPendingDrag == null) {
            return;
        }
        mTempConnections.clear();
        rootBlock.getAllConnectionsRecursive(mTempConnections);
        for (int i = 0; i < mTempConnections.size(); i++) {
            Connection conn = mTempConnections.get(i);
            mDraggedConnections.remove(conn);
            conn.setDragMode(false);
        }
    }

    /**
     * Creates a BlockTouchHandler that will initiate a drag only after the user has dragged
     * beyond some touch threshold.
     *
     * @param dragHandler The {@link DragHandler} to handle gestures for the constructed
     *                    {@link BlockTouchHandler}.
     * @return A newly constructed {@link BlockTouchHandler}.
     */
    public BlockTouchHandler buildSloppyBlockTouchHandler(final DragHandler dragHandler) {
        return new BlockTouchHandler() {
            @Override
            public boolean onTouchBlock(BlockView blockView, MotionEvent motionEvent) {
                return onTouchBlockImpl(DRAG_MODE_SLOPPY, dragHandler, blockView, motionEvent,
                        /* interceptMode */ false);
            }

            @Override
            public boolean onInterceptTouchEvent(BlockView blockView, MotionEvent motionEvent) {
                // Intercepted move events might still be handled but the child view, such as
                // a drop down field.
                return onTouchBlockImpl(DRAG_MODE_SLOPPY, dragHandler, blockView, motionEvent,
                        /* interceptMode */ true);
            }
        };
    };

    /**
     * Creates a BlockTouchHandler that will initiate a drag as soon as the BlockView receives a
     * {@link MotionEvent} directly (not via interception).
     *
     * @param dragHandler The {@link DragHandler} to handle gestures for the constructed
     *                    {@link BlockTouchHandler}.
     * @return A newly constructed {@link BlockTouchHandler}.
     */
    public BlockTouchHandler buildImmediateDragBlockTouchHandler(final DragHandler dragHandler) {
        return new BlockTouchHandler() {
            @Override
            public boolean onTouchBlock(BlockView blockView, MotionEvent motionEvent) {
                return onTouchBlockImpl(DRAG_MODE_IMMEDIATE, dragHandler, blockView, motionEvent,
                        /* interceptMode */ false);
            }

            @Override
            public boolean onInterceptTouchEvent(BlockView blockView, MotionEvent motionEvent) {
                return onTouchBlockImpl(DRAG_MODE_IMMEDIATE, dragHandler, blockView, motionEvent,
                        /* interceptMode */ true);
            }
        };
    };

    /**
     * Continue dragging the currently moving block.  Called during ACTION_DRAG_LOCATION.
     *
     * @param event The next drag event to handle, as received by the {@link WorkspaceView}.
     */
    private void continueDragging(DragEvent event) {
        updateBlockPosition(event);

        // highlight as we go
        if (mHighlightedBlockView != null) {
            mHighlightedBlockView.setHighlightedConnection(null);
        }
        Pair<Connection, Connection> connectionCandidate = findBestConnection(mPendingDrag.getRootDraggedBlock());
        if (connectionCandidate != null) {
            mHighlightedBlockView = mHelper.getView(connectionCandidate.second.getBlock());
            mHighlightedBlockView.setHighlightedConnection(connectionCandidate.second);
        }

        mPendingDrag.getDragGroup().requestLayout();
    }

    /**
     * Attempts to connect a dropped drag group with nearby connections
     */
    void maybeConnectDragGroup() {
        Block dragRoot = mPendingDrag.getRootDraggedBlock();

        // Maybe snap to connections.
        Pair<Connection, Connection> connectionCandidate = findBestConnection(dragRoot);
        if (connectionCandidate != null) {
            mController.connect(connectionCandidate.first, connectionCandidate.second);
            // .connect(..) includes bumping block within snap distance of the new location.
        } else {
            // Even if no connection is found, still bump any neighbors within snap distance of the
            // new location.
            mController.bumpNeighbors(dragRoot);
        }
    }

    /**
     * Finish a drag gesture and clear pending drag info.  Called by event handlers for ACTION_UP,
     * ACTION_CANCEL, ACTION_DROP, and ACTION_DRAG_ENDED.
     */
    // TODO(305): Revert actions when behavior == FINISH_BEHAVIOR_REVERT
    private void finishDragging(@FinishDragBehavior int behavior) {
        if (behavior == FINISH_BEHAVIOR_DROP || behavior == FINISH_BEHAVIOR_REVERT) {
            // Update the drag group so that everything that has been changed will be properly
            // invalidated. Also, update the positions of all of the connections that were impacted
            // by the move and add them back to the manager. All of the connection locations will be
            // set relative to their block views immediately after this loop.  For now we just want
            // to unset drag mode and add the connections back to the list; 0, 0 is a cheap place to
            // put them.
            // Dragged connections may be empty, especially if the
            for (int i = 0; i < mDraggedConnections.size(); i++) {
                Connection cur = mDraggedConnections.get(i);
                cur.setPosition(0, 0);
                cur.setDragMode(false);
                mConnectionManager.addConnection(cur);
            }
        }
        mDraggedConnections.clear();

        if (mHighlightedBlockView != null) {
            mHighlightedBlockView.setHighlightedConnection(null);
            mHighlightedBlockView = null;
        }

        if (mPendingDrag != null) {
            BlockView blockView = mPendingDrag.getRootDraggedBlockView();
            if (blockView != null) {
                ((View) blockView).setPressed(false);
            } // else, trashing or similar manipulation made the view disappear.
            mPendingDrag = null;
        }
    }

    public void setWorkspaceView(WorkspaceView view) {
        mWorkspaceView = view;
    }

    // TODO(#210): Generalize this to other possible block drop targets.
    public void setTrashView(View trashView) {
        mTrashView = trashView;
    }

    /**
     * Let the Dragger know that a block was touched. This will be called when the block in the
     * workspace has been touched, but a drag has not yet been started.
     *
     * This method handles both regular touch events and intercepted touch events, with the latter
     * identified with the {@code interceptMode} parameter.  The only difference is that intercepted
     * events only return true (indicating they are handled) when a drag has been initiated. This
     * allows any underlying View, such as a field to handle the MotionEvent normally.
     *
     * @param dragMode The mode (immediate or sloppy) for handling this touch event.
     * @param dragHandler The {@link DragHandler} attached to this view.
     * @param touchedView The {@link BlockView} that detected a touch event.
     * @param event The touch event.
     * @param interceptMode When true forces all {@link MotionEvent#ACTION_MOVE} events
     *                                   that match {@link #mPendingDrag} to return true / handled.
     *                                   Otherwise, it only returns true if a drag is started.
     *
     * @return True if the event was handled by this touch implementation.
     */
    @VisibleForTesting
    boolean onTouchBlockImpl(@DragMode int dragMode, DragHandler dragHandler, BlockView touchedView,
            MotionEvent event, boolean interceptMode) {
        if (mWithinOnTouchBlockImpl) {
            throw new IllegalStateException("onTouchBlockImpl() called recursively. Make sure OnDragHandler."
                    + "maybeGetDragGroupCreator() is not manipulating the View hierarchy.");
        }
        mWithinOnTouchBlockImpl = true;

        final int action = MotionEventCompat.getActionMasked(event);

        boolean matchesPending = false;
        if (mPendingDrag != null) {
            matchesPending = mPendingDrag.isMatchAndProcessed(event, touchedView);
            if (!matchesPending && !mPendingDrag.isAlive()) {
                mPendingDrag = null; // Was a part of previous gesture. Delete.
            }
        }

        if (LOG_TOUCH_EVENTS) {
            Log.d(TAG,
                    "onTouchBlockImpl: " + (dragMode == DRAG_MODE_IMMEDIATE ? "IMMEDIATE" : "SLOPPY")
                            + (interceptMode ? " intercept" : " direct") + "\n\t" + event + "\n\tMatches pending? "
                            + matchesPending);

            mMainHandler.removeCallbacks(mLogPending); // Only call once per event 'tick'
            mMainHandler.post(mLogPending);
        }

        final boolean result;
        if (action == MotionEvent.ACTION_DOWN) {
            if (mPendingDrag == null) {
                mPendingDrag = new PendingDrag(mController, touchedView, event);
                if (interceptMode) {
                    // Do not handle intercepted down events. Allow child views (particularly
                    // fields) to handle the touch normally.
                    result = false;
                } else {
                    // The user touched the block directly.
                    if (dragMode == DRAG_MODE_IMMEDIATE) {
                        result = maybeStartDrag(dragHandler);
                    } else {
                        result = true;
                    }
                }
            } else if (matchesPending && !interceptMode) {
                // The Pending Drag was created during intercept, but the child did not handle it
                // and the event has bubbled down to here.
                if (dragMode == DRAG_MODE_IMMEDIATE) {
                    result = maybeStartDrag(dragHandler);
                } else {
                    result = true;
                }
            } else {
                result = false; // Pending drag already started with a different view / pointer id.
            }
        } else if (matchesPending) {
            // This touch is part of the current PendingDrag.
            if (action == MotionEvent.ACTION_MOVE) {
                if (mPendingDrag.isDragging()) {
                    result = false; // We've already cancelled or started dragging.
                } else {
                    // Mark all direct move events as handled, but only intercepted events if they
                    // initiate a new drag.
                    boolean isDragGesture = (!interceptMode && dragMode == DRAG_MODE_IMMEDIATE
                            && event.getDownTime() > TAP_TIMEOUT) || isBeyondSlopThreshold(event);
                    boolean isNewDrag = isDragGesture && maybeStartDrag(dragHandler);
                    result = isNewDrag || !interceptMode;
                }
            }
            // Handle the case when the user releases before moving far enough to start a drag.
            else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                if (!mPendingDrag.isDragging()) {
                    if (!interceptMode && mPendingDrag.isClick()) {
                        dragHandler.onBlockClicked(mPendingDrag);
                    }
                    finishDragging(FINISH_BEHAVIOR_REVERT);
                }
                result = !interceptMode;
            } else {
                result = false; // Unrecognized event action
            }
        } else {
            result = false; // Doesn't match existing drag.
        }

        if (LOG_TOUCH_EVENTS)
            Log.d(TAG, "\treturn " + result);
        mWithinOnTouchBlockImpl = false;
        return result;
    }

    /**
     * @return The listener to use for {@link DragEvent}'s in the {@link WorkspaceView}.
     */
    public View.OnDragListener getDragEventListener() {
        return mDragEventListener;
    }

    /**
     * Checks whether {@code actionMove} is beyond the allowed slop (i.e., unintended) drag motion
     * distance.
     *
     * @param actionMove The {@link MotionEvent#ACTION_MOVE} event.
     * @return True if the motion is beyond the allowed slop threshold
     */
    private boolean isBeyondSlopThreshold(MotionEvent actionMove) {
        BlockView touchedView = mPendingDrag.getTouchedBlockView();

        // Not dragging yet - compute distance from Down event and start dragging if far enough.
        @Size(2)
        int[] touchDownLocation = mTempScreenCoord1;
        mPendingDrag.getTouchDownScreen(touchDownLocation);

        @Size(2)
        int[] curScreenLocation = mTempScreenCoord2;
        touchedView.getTouchLocationOnScreen(actionMove, curScreenLocation);

        final int deltaX = touchDownLocation[0] - curScreenLocation[0];
        final int deltaY = touchDownLocation[1] - curScreenLocation[1];

        // Dragged far enough to start a drag?
        return (deltaX * deltaX + deltaY * deltaY > mTouchSlopSquared);
    }

    /**
     * Handle motion events while starting to drag a block.  This keeps track of whether the block
     * has been dragged more than {@code mTouchSlop} and starts a drag if necessary. Once the drag
     * has been started, all following events will be handled through the {@link
     * #mDragEventListener}.
     */
    private boolean maybeStartDrag(DragHandler dragHandler) {
        // Check with the pending drag handler to select or create the dragged group.
        final PendingDrag pendingDrag = mPendingDrag; // Stash for async callback
        final Runnable dragGroupCreator = dragHandler.maybeGetDragGroupCreator(pendingDrag);
        final boolean foundDragGroup = (dragGroupCreator != null);
        if (foundDragGroup) {
            mMainHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mPendingDrag != null && mPendingDrag.isDragging()) {
                        return; // Ignore.  Probably being handled by a child view.
                    }

                    dragGroupCreator.run();
                    boolean dragStarted = pendingDrag.isDragging();
                    if (dragStarted) {
                        mPendingDrag = pendingDrag;
                        final BlockGroup dragGroup = mPendingDrag.getDragGroup();
                        if (dragGroup.getParent() != mWorkspaceView) {
                            throw new IllegalStateException("dragGroup is root in WorkspaceView");
                        }

                        Block rootBlock = dragGroup.getFirstBlock();
                        removeDraggedConnectionsFromConnectionManager(rootBlock);
                        ClipData clipData = ClipData.newPlainText(WorkspaceView.BLOCK_GROUP_CLIP_DATA_LABEL, "");
                        dragGroup.startDrag(clipData, new View.DragShadowBuilder(), null, 0);
                    } else {
                        mPendingDrag = null;
                    }
                }
            });
        }

        return foundDragGroup;
    }

    /**
     * Check whether the given event occurred on top of the trash can button.  Should be called from
     * {@link WorkspaceView}.
     *
     * @param event The event whose location should be checked, with position in WorkspaceView
     * coordinates.
     * @return Whether the event was on top of the trash can button.
     */
    // TODO(#210): Generalize this to other possible block drop targets.
    private boolean touchingTrashView(DragEvent event) {
        if (mTrashView == null) {
            return false;
        }

        mTrashView.getLocationOnScreen(mTempScreenCoord1);
        mTrashView.getHitRect(mTrashRect);

        mTrashRect.offset((mTempScreenCoord1[0] - mTrashRect.left), (mTempScreenCoord1[1] - mTrashRect.top));
        // Get the touch location on the screen
        mTempViewPoint.set((int) event.getX(), (int) event.getY());
        mHelper.virtualViewToScreenCoordinates(mTempViewPoint, mTempViewPoint);

        // Check if the touch location was on the trash
        return mTrashRect.contains(mTempViewPoint.x, mTempViewPoint.y);
    }

    /**
     * Ends a drag in the trash can, clearing state and deleting blocks as needed.
     */
    private boolean dropInTrash() {
        if (mHighlightedBlockView != null) {
            mHighlightedBlockView.setHighlightedConnection(null);
            mHighlightedBlockView = null;
        }
        mDraggedConnections.clear();
        return mController.trashRootBlock(mPendingDrag.getRootDraggedBlock());
    }

    /**
     * Removes all connections of block and its descendants from the {@link }ConnectionManager}, so
     * these connections are not considered as potential connections when looking from connections
     * during dragging.
     *
     * @param block The root block of connections that should be removed.
     */
    private void removeDraggedConnectionsFromConnectionManager(Block block) {
        mDraggedConnections.clear();
        // Don't track any of the connections that we're dragging around.
        block.getAllConnectionsRecursive(mDraggedConnections);
        for (int i = 0; i < mDraggedConnections.size(); i++) {
            Connection conn = mDraggedConnections.get(i);
            mConnectionManager.removeConnection(conn);
            conn.setDragMode(true);
        }
    }

    /**
     * Move the currently dragged block in response to a new {@link MotionEvent}.
     * <p/>
     * All of the child blocks move with the root block based on its position during layout.
     *
     * @param event The {@link MotionEvent} to react to.
     */
    private void updateBlockPosition(DragEvent event) {
        // The event is relative to the WorkspaceView. Grab the pixel offset within.
        ViewPoint curDragLocationPixels = mTempViewPoint;
        curDragLocationPixels.set((int) event.getX(), (int) event.getY());
        WorkspacePoint curDragPositionWorkspace = mTempWorkspacePoint;
        mHelper.virtualViewToWorkspaceCoordinates(curDragLocationPixels, curDragPositionWorkspace);

        WorkspacePoint touchDownWorkspace = mPendingDrag.getTouchDownWorkspaceCoordinates();
        // Subtract original drag location from current location to get delta
        int workspaceDeltaX = curDragPositionWorkspace.x - touchDownWorkspace.x;
        int workspaceDeltaY = curDragPositionWorkspace.y - touchDownWorkspace.y;

        WorkspacePoint blockOrigPosition = mPendingDrag.getOriginalBlockPosition();
        mPendingDrag.getRootDraggedBlock().setPosition(blockOrigPosition.x + workspaceDeltaX,
                blockOrigPosition.y + workspaceDeltaY);
        mPendingDrag.getDragGroup().requestLayout();
    }

    private Pair<Connection, Connection> findBestConnection(Block block) {
        return mConnectionManager.findBestConnection(block, mHelper.getMaxSnapDistance());
    }
}