com.linkbubble.ui.BubbleDraggable.java Source code

Java tutorial

Introduction

Here is the source code for com.linkbubble.ui.BubbleDraggable.java

Source

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package com.linkbubble.ui;

import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.AsyncTask;
import android.support.v4.content.LocalBroadcastManager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;

import com.linkbubble.Config;
import com.linkbubble.Constant;
import com.linkbubble.MainApplication;
import com.linkbubble.MainController;
import com.linkbubble.R;
import com.linkbubble.Settings;
import com.linkbubble.physics.Circle;
import com.linkbubble.physics.Draggable;
import com.linkbubble.physics.DraggableHelper;
import com.linkbubble.util.CrashTracking;
import com.linkbubble.util.Util;

import java.net.MalformedURLException;

public class BubbleDraggable extends BubbleView implements Draggable {

    private static final String TAG = "BubbleDraggable";

    private DraggableHelper mDraggableHelper;
    private OnUpdateListener mOnUpdateListener;
    public BadgeView mBadgeView;
    private CanvasView mCanvasView;
    private BubbleFlowDraggable mBubbleFlowDraggable;
    private Util.Point mTractorBeamIntersectionPoint = new Util.Point();

    private MainController.BeginBubbleDragEvent mBeginBubbleDragEvent = new MainController.BeginBubbleDragEvent();
    private MainController.DraggableBubbleMovedEvent mDraggableBubbleMovedEvent = new MainController.DraggableBubbleMovedEvent();
    private MainController.EndBubbleDragEvent mEndBubbleDragEvent = new MainController.EndBubbleDragEvent();
    private MainController.BeginCollapseTransitionEvent mBeginCollapseTransitionEvent = new MainController.BeginCollapseTransitionEvent();
    private MainController.EndCollapseTransitionEvent mEndCollapseTransitionEvent = new MainController.EndCollapseTransitionEvent();

    // Physics state
    public enum Mode {
        BubbleView, ContentView
    }

    private BubbleTargetView mCurrentSnapTarget;
    private boolean mHasMoved;
    private boolean mTouchDown;
    private int mTouchInitialX;
    private int mTouchInitialY;
    private boolean mAnimActive;
    private Mode mMode;
    private float mTimeOnSnapTarget;
    private Circle mCircle;

    public BubbleDraggable(Context context) {
        this(context, null);
    }

    public BubbleDraggable(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BubbleDraggable(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public boolean isDragging() {
        return mTouchDown;
    }

    public Mode getCurrentMode() {
        return mMode;
    }

    private void onAnimComplete() {
        try {
            // Sometimes there are two calls to animation, we will handle that later
            Util.Assert(mAnimActive, "mAnimActive=" + mAnimActive);
        } catch (AssertionError exc) {
            exc.printStackTrace();
        }
        mAnimActive = false;
    }

    private void doSnap() {
        int xp = (int) (0.5f + mDraggableHelper.getXPos() + Config.mBubbleWidth * 0.5f);
        int yp = mDraggableHelper.getYPos();

        if (xp < Config.mScreenCenterX) {
            xp = Config.mBubbleSnapLeftX;
        } else {
            xp = Config.mBubbleSnapRightX;
        }

        animate().alpha(Constant.BUBBLE_MODE_ALPHA).setDuration(Constant.BUBBLE_ANIM_TIME);
        mBadgeView.animate().alpha(Constant.BUBBLE_MODE_ALPHA).setDuration(Constant.BUBBLE_ANIM_TIME);

        setTargetPos(xp, yp, 0.5f, DraggableHelper.AnimationType.MediumOvershoot, false,
                new DraggableHelper.AnimationEventListener() {
                    @Override
                    public void onAnimationComplete() {
                        onAnimComplete();
                        Settings.get().setBubbleRestingPoint(mDraggableHelper.getXPos(),
                                mDraggableHelper.getYPos());
                    }

                    @Override
                    public void onCancel() {
                        onAnimComplete();
                    }
                });
    }

    public void switchToBubbleView() {
        doAnimateToBubbleView(0);
    }

    public void switchToExpandedView(MainController controller) {
        doAnimateToContentView(controller, false);
    }

    private void doSnapAction(Constant.BubbleAction action) {
        MainController mainController = MainController.get();

        float snapTime = mTimeOnSnapTarget - Config.ANIMATE_TO_SNAP_TIME;
        if (action == Constant.BubbleAction.Close && snapTime >= Config.CLOSE_ALL_BUBBLES_DELAY) {
            mainController.closeAllBubbles();
            mMode = Mode.BubbleView;
        } else {
            if (mainController.closeCurrentTab(action, false)) {
                if (mMode == Mode.ContentView && action == Constant.BubbleAction.Close) {
                    doAnimateToContentView(mainController, false);
                } else {
                    doAnimateToBubbleView(0);
                }
            } else {
                mMode = Mode.BubbleView;
            }
        }
    }

    private void doFlick(float vx, float vy) {
        DraggableHelper.AnimationType animType = DraggableHelper.AnimationType.Linear;
        BubbleTargetView.enableTractor();

        mCurrentSnapTarget = null;

        int initialX = mDraggableHelper.getXPos();
        int initialY = mDraggableHelper.getYPos();
        int targetX, targetY;

        if (Math.abs(vx) < 0.1f) {
            targetX = initialX;

            if (vy > 0.0f) {
                targetY = Config.mBubbleMaxY;
            } else {
                targetY = Config.mBubbleMinY;
            }
        } else {

            if (vx > 0.0f) {
                targetX = Config.mBubbleSnapRightX;
            } else {
                targetX = Config.mBubbleSnapLeftX;
            }

            float m = vy / vx;

            targetY = (int) (m * (targetX - initialX) + initialY);

            if (targetY < Config.mBubbleMinY) {
                targetY = Config.mBubbleMinY;
                targetX = (int) (initialX + (targetY - initialY) / m);
            } else if (targetY > Config.mBubbleMaxY) {
                targetY = Config.mBubbleMaxY;
                targetX = (int) (initialX + (targetY - initialY) / m);
            } else {
                animType = DraggableHelper.AnimationType.MediumOvershoot;
            }
        }

        float flickDistance = Util.distance(initialX, initialY, targetX, targetY);
        float flickVelocity = (float) Math.sqrt(vx * vx + vy * vy);
        float flickAnimPeriod = flickDistance / flickVelocity;
        flickAnimPeriod = Util.clamp(0.05f, flickAnimPeriod, 0.5f);

        // Check for tractor beam intercept

        // Get center line of flick
        float x0 = initialX + Config.mBubbleWidth * 0.5f;
        float y0 = initialY + Config.mBubbleHeight * 0.5f;
        float x1 = targetX + Config.mBubbleWidth * 0.5f;
        float y1 = targetY + Config.mBubbleHeight * 0.5f;

        // Get the closest (if any) snap target that will be able to grab the bubble.
        final BubbleTargetView tv = mCanvasView.getSnapTarget(x0, y0, x1, y1, mTractorBeamIntersectionPoint);
        if (tv != null) {
            float intBubbleX = mTractorBeamIntersectionPoint.x - Config.mBubbleWidth * 0.5f;
            float intBubbleY = mTractorBeamIntersectionPoint.y - Config.mBubbleHeight * 0.5f;

            float intersectionDistance = Util.distance(initialX, initialY, intBubbleX, intBubbleY);
            float intFraction = 0.0f;
            if (flickDistance > 0.0001f) {
                intFraction = intersectionDistance / flickDistance;
            }
            try {
                Util.Assert(intFraction >= 0.0f && intFraction <= 1.05f,
                        "intFraction:" + intFraction + ", flickDistance:" + flickDistance);
                float intTime = flickAnimPeriod * intFraction;

                animType = DraggableHelper.AnimationType.Linear;
                flickAnimPeriod = intTime;
                targetX = (int) intBubbleX;
                targetY = (int) intBubbleY;

                tv.setTargetCenter(mTractorBeamIntersectionPoint.x, mTractorBeamIntersectionPoint.y);
            } catch (AssertionError exc) {
                if (animType != DraggableHelper.AnimationType.Linear) {
                    flickAnimPeriod += 0.15f;
                }
            }

        } else {
            if (animType != DraggableHelper.AnimationType.Linear) {
                flickAnimPeriod += 0.15f;
            }
        }

        // #431 - Ensure there is always >0 time to animate the flick.
        flickAnimPeriod = Math.max(0.01f, flickAnimPeriod);

        animate().alpha(Constant.BUBBLE_MODE_ALPHA).setDuration(Constant.BUBBLE_ANIM_TIME);
        mBadgeView.animate().alpha(Constant.BUBBLE_MODE_ALPHA).setDuration(Constant.BUBBLE_ANIM_TIME);

        setTargetPos(targetX, targetY, flickAnimPeriod, animType, false,
                new DraggableHelper.AnimationEventListener() {
                    @Override
                    public void onAnimationComplete() {
                        BubbleTargetView.disableTractor();
                        onAnimComplete();

                        MainApplication.postEvent(getContext(), mEndBubbleDragEvent);

                        if (tv == null) {
                            int x = mDraggableHelper.getXPos();
                            if (x != Config.mBubbleSnapLeftX && x != Config.mBubbleSnapRightX) {
                                doSnap();
                            }
                        } else {
                            Constant.BubbleAction action = tv.getAction();
                            doSnapAction(action);
                        }
                    }

                    @Override
                    public void onCancel() {
                        onAnimComplete();
                    }
                });
    }

    public void snapToBubbleView() {
        mMode = Mode.BubbleView;
        mDraggableHelper.cancelAnimation();

        MainController.get().collapseBubbleFlow(0);

        Point bubbleRestingPoint = Settings.get().getBubbleRestingPoint();
        setTargetPos(bubbleRestingPoint.x, bubbleRestingPoint.y, 0, DraggableHelper.AnimationType.Linear, false,
                null);

        MainApplication.postEvent(getContext(), mEndCollapseTransitionEvent);
    }

    private void doAnimateToBubbleView(int animTimeMs) {
        if (mAnimActive) {
            if (mMode == Mode.BubbleView) {
                return;
            } else {
                mDraggableHelper.cancelAnimation();
            }
        }

        mTouchDown = false;
        mMode = Mode.BubbleView;

        if (MainController.get().getActiveTabCount() == 0) {
            return;
        }

        if (animTimeMs == 0) {
            animTimeMs = Constant.BUBBLE_ANIM_TIME;
        }

        animate().alpha(Constant.BUBBLE_MODE_ALPHA).setDuration(animTimeMs);

        float bubblePeriod = (float) animTimeMs / 1000.f;
        float contentPeriod = bubblePeriod * 0.666667f; // 0.66667 is the normalized t value when f = 1.0f for overshoot interpolator of 0.5 tension

        MainController mainController = MainController.get();
        setVisibility(View.VISIBLE);
        TabView currentTab = mBubbleFlowDraggable.getCurrentTab();
        if (currentTab != null) {
            // ensure imitator image is up to date, fixes #228
            mFavicon.clearImage();
            currentTab.setImitator(this);
        }

        Point bubbleRestingPoint = Settings.get().getBubbleRestingPoint();
        setTargetPos(bubbleRestingPoint.x, bubbleRestingPoint.y, bubblePeriod,
                DraggableHelper.AnimationType.SmallOvershoot, false, new DraggableHelper.AnimationEventListener() {
                    @Override
                    public void onAnimationComplete() {
                        MainApplication.postEvent(getContext(), mEndCollapseTransitionEvent);
                        onAnimComplete();
                    }

                    @Override
                    public void onCancel() {
                        onAnimComplete();
                    }
                });

        mBadgeView.animate().alpha(Constant.BUBBLE_MODE_ALPHA).setDuration(animTimeMs);
        mainController.endAppPolling();
        mainController.collapseBubbleFlow((long) (contentPeriod * 1000));

        mBeginCollapseTransitionEvent.mPeriod = contentPeriod;
        MainApplication.postEvent(getContext(), mBeginCollapseTransitionEvent);
    }

    public void doAnimateToContentView(MainController controller, boolean startDelay) {
        Intent intentActivity = new Intent(getContext(), BubbleFlowActivity.class);
        intentActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
        getContext().startActivity(intentActivity);
        doAnimateToContentView(true, startDelay, controller);
    }

    private void doAnimateToContentView(boolean saveBubbleRestingPoint, boolean startDelay,
            MainController controller) {
        CrashTracking.log("doAnimateToContentView()");
        if (mAnimActive) {
            if (mMode == Mode.ContentView) {
                CrashTracking.log("doAnimateToContentView() mMode == Mode.ContentView, early exit");
                return;
            } else {
                CrashTracking.log("doAnimateToContentView() cancelAnimation()");
                mDraggableHelper.cancelAnimation();
            }
        }

        if (mMode != Mode.ContentView && saveBubbleRestingPoint) {
            Settings.get().setBubbleRestingPoint(mDraggableHelper.getXPos(), mDraggableHelper.getYPos());
        }

        mTouchDown = false;
        mMode = Mode.ContentView;

        final float bubblePeriod = (float) Constant.BUBBLE_ANIM_TIME / 1000.f;
        final float contentPeriod = bubblePeriod * 0.666667f; // 0.66667 is the normalized t value when f = 1.0f for overshoot interpolator of 0.5 tension

        final MainController mainController = controller;
        setVisibility(View.VISIBLE);

        mainController.beginAppPolling();
        mainController.expandBubbleFlow((long) (contentPeriod * 1000), true);

        animate().alpha(1.0f).setDuration(Constant.BUBBLE_ANIM_TIME);
        mBadgeView.animate().alpha(1.0f).setDuration(Constant.BUBBLE_ANIM_TIME);

        int xp = (int) Config.getContentViewX(0, 1);
        int yp = Config.mContentViewBubbleY;

        setTargetPos(xp, yp, bubblePeriod, DraggableHelper.AnimationType.SmallOvershoot, startDelay,
                new DraggableHelper.AnimationEventListener() {
                    @Override
                    public void onAnimationComplete() {
                        onAnimComplete();
                        int activeCount = mainController.getActiveTabCount();
                        if (activeCount == 0) {
                            // Ensure we don't enter state where there are no tabs to display. Fix #448
                            MainApplication.postEvent(getContext(),
                                    new MainController.EndCollapseTransitionEvent());
                            //MainApplication.postEvent(getContext(), new ExpandedActivity.MinimizeExpandedActivityEvent());
                            CrashTracking
                                    .log("doAnimateToContentView(): onAnimationComplete(): getActiveTabCount()==0");
                        }
                    }

                    @Override
                    public void onCancel() {
                        onAnimComplete();
                        mainController.endAppPolling();
                        mainController.collapseBubbleFlow((long) (contentPeriod * 1000));
                    }
                });
    }

    public void configure(int x0, int y0, int targetX, int targetY, int targetTime, CanvasView cv) {

        try {
            super.configure("http://blerg.com"); // the URL is not actually used...
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }

        //setBackgroundColor(0xff00ff00);

        mMode = Mode.BubbleView;
        mAnimActive = false;
        mHasMoved = false;
        mCanvasView = cv;
        mBadgeView = (BadgeView) findViewById(R.id.badge_view);
        mBadgeView.hide();
        mBadgeView.setVisibility(View.GONE);
        mCircle = new Circle(0, 0, 1);

        int bubbleSize = getResources().getDimensionPixelSize(R.dimen.bubble_size);

        WindowManager.LayoutParams windowManagerParams = new WindowManager.LayoutParams();
        windowManagerParams.gravity = Gravity.TOP | Gravity.LEFT;
        windowManagerParams.x = x0;
        windowManagerParams.y = y0;
        windowManagerParams.height = bubbleSize;
        windowManagerParams.width = bubbleSize;
        windowManagerParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
        windowManagerParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
                | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        windowManagerParams.format = PixelFormat.TRANSPARENT;
        windowManagerParams.setTitle("LinkBubble: BubbleDraggable");
        final MainController controller = MainController.get();

        mDraggableHelper = new DraggableHelper(this, windowManagerParams, true,
                new DraggableHelper.OnTouchActionEventListener() {

                    @Override
                    public void onActionDown(DraggableHelper.TouchEvent e) {
                        if (!mAnimActive) {
                            mCurrentSnapTarget = null;
                            mHasMoved = false;
                            mTouchDown = true;
                            mTouchInitialX = e.posX;
                            mTouchInitialY = e.posY;

                            animate().alpha(1.0f).setDuration(Constant.BUBBLE_ANIM_TIME);
                            mBadgeView.animate().alpha(1.0f).setDuration(Constant.BUBBLE_ANIM_TIME);

                            MainController mainController = MainController.get();
                            if (mainController != null) {
                                mainController.scheduleUpdate();
                            }

                            MainApplication.postEvent(getContext(), mBeginBubbleDragEvent);
                            CrashTracking.log("BubbleDraggable.configure(): onActionDown() - start drag");
                        }
                    }

                    @Override
                    public void onActionMove(DraggableHelper.MoveEvent e) {
                        Log.d(TAG, "!!!!!Moving");
                        if (mTouchDown) {
                            setVisibility(View.VISIBLE);
                            int targetX = targetX = (int) (e.rawX - Config.mBubbleWidth * 0.5);
                            int targetY = (int) (e.rawY - Config.mBubbleHeight);

                            targetX = Util.clamp(Config.mBubbleSnapLeftX, targetX, Config.mBubbleSnapRightX);
                            targetY = Util.clamp(Config.mBubbleMinY, targetY, Config.mBubbleMaxY);

                            float d = (float) Math.sqrt((e.dx * e.dx) + (e.dy * e.dy));
                            if (d >= Config.dpToPx(10.0f)) {
                                mHasMoved = true;
                            }

                            if (!mAnimActive) {
                                // c = null, t = null           wasn't snapping, no snap -> move
                                // c = 1, t = null              was snapping, no snap -> anim out
                                // c = null, t = 1              wasn't snapping, is snap -> anim in
                                // c = 1, t = 1                 was snapping, is snapping -> NO move

                                mCircle.Update(targetX + Config.mBubbleWidth * 0.5f,
                                        targetY + Config.mBubbleHeight * 0.5f, Config.mBubbleWidth * 0.5f);
                                BubbleTargetView tv = mCanvasView.getSnapTarget(mCircle, 1.0f);

                                if (mCurrentSnapTarget == null) {
                                    if (tv == null) {
                                        setTargetPos(targetX, targetY, 0.0f,
                                                DraggableHelper.AnimationType.DistanceProportion, false, null);
                                    } else {
                                        tv.beginSnapping();
                                        mCurrentSnapTarget = tv;
                                        mTimeOnSnapTarget = 0.0f;

                                        Circle dc = tv.GetDefaultCircle();
                                        int xt = (int) (0.5f + dc.mX - Config.mBubbleWidth * 0.5f);
                                        int yt = (int) (0.5f + dc.mY - Config.mBubbleHeight * 0.5f);
                                        setTargetPos(xt, yt, Config.ANIMATE_TO_SNAP_TIME,
                                                DraggableHelper.AnimationType.Linear, false,
                                                new DraggableHelper.AnimationEventListener() {
                                                    @Override
                                                    public void onAnimationComplete() {
                                                        onAnimComplete();
                                                    }

                                                    @Override
                                                    public void onCancel() {
                                                        onAnimComplete();
                                                    }
                                                });
                                    }
                                } else {
                                    if (tv == null) {
                                        setTargetPos(targetX, targetY, 0.05f, DraggableHelper.AnimationType.Linear,
                                                false, new DraggableHelper.AnimationEventListener() {
                                                    @Override
                                                    public void onAnimationComplete() {
                                                        mCurrentSnapTarget.endSnapping();
                                                        mCurrentSnapTarget.endLongHovering();
                                                        mCurrentSnapTarget = null;
                                                        mTimeOnSnapTarget = 0.f;
                                                        onAnimComplete();
                                                    }

                                                    @Override
                                                    public void onCancel() {
                                                        onAnimComplete();
                                                    }
                                                });
                                    }
                                }
                            }
                        }
                    }

                    @Override
                    public void onActionUp(DraggableHelper.ReleaseEvent e, boolean startDelay) {
                        if (mTouchDown) {
                            CrashTracking.log("BubbleDraggable.configure(): onActionUp() - end drag");
                            mDraggableHelper.cancelAnimation();

                            if (mHasMoved) {
                                if (mCurrentSnapTarget == null) {
                                    float v = (float) Math.sqrt(e.vx * e.vx + e.vy * e.vy);
                                    float threshold = Config.dpToPx(900.0f);
                                    if (v > threshold) {
                                        doFlick(e.vx, e.vy);
                                        CrashTracking.log("BubbleDraggable.configure(): onActionUp() - doFlick()");
                                    } else {
                                        boolean doBubbleView = mMode == Mode.BubbleView
                                                || e.posX < Config.mScreenWidth * 0.2f
                                                || e.posX > Config.mScreenWidth * 0.8f
                                                || e.posY > Config.mScreenHeight * 0.5f;

                                        if (doBubbleView) {
                                            mMode = Mode.BubbleView;
                                        }
                                        MainApplication.postEvent(getContext(), mEndBubbleDragEvent);

                                        if (doBubbleView) {
                                            CrashTracking
                                                    .log("BubbleDraggable.configure(): onActionUp() - doSnap()");
                                            doSnap();
                                        } else {
                                            CrashTracking.log(
                                                    "BubbleDraggable.configure(): onActionUp() - doAnimateToContentView() [mHasMoved==true]");
                                            doAnimateToContentView(controller, startDelay);
                                        }
                                    }
                                } else {
                                    MainApplication.postEvent(getContext(), mEndBubbleDragEvent);
                                    CrashTracking.log("BubbleDraggable.configure(): onActionUp() - doSnapAction()");
                                    doSnapAction(mCurrentSnapTarget.getAction());
                                }
                            } else {
                                MainApplication.postEvent(getContext(), mEndBubbleDragEvent);

                                if (mMode == Mode.BubbleView) {
                                    CrashTracking.log(
                                            "BubbleDraggable.configure(): onActionUp() - doAnimateToContentView() [mMode == Mode.BubbleView]");
                                    doAnimateToContentView(controller, startDelay);
                                } else {
                                    if (mMode == Mode.ContentView && mBubbleFlowDraggable.isExpanded() == false) {
                                        CrashTracking.log(
                                                "BubbleDraggable.configure(): onActionUp() - doAnimateToContentView() [mMode == Mode.ContentView]");
                                        doAnimateToContentView(controller, startDelay);
                                    } else {
                                        CrashTracking.log(
                                                "BubbleDraggable.configure(): onActionUp() - doAnimateToBubbleView()");
                                        doAnimateToBubbleView(0);
                                    }
                                }
                            }

                            mTouchDown = false;
                        }
                    }
                });

        if (mDraggableHelper.isAlive()) {
            MainController.addRootWindow(this, windowManagerParams);

            slideOnScreen(x0, y0, targetX, targetY, targetTime);
        }
    }

    public void slideOnScreen(int x0, int y0, int targetX, int targetY, int targetTime) {
        setExactPos(x0, y0);
        if (targetX != x0 || targetY != y0) {
            setTargetPos(targetX, targetY, (float) targetTime / 1000.f,
                    DraggableHelper.AnimationType.LargeOvershoot, false, null);
        }
        CrashTracking.log("BubbleDraggable.slideOnScreen()");
    }

    public void setBubbleFlowDraggable(BubbleFlowDraggable bubbleFlowDraggable) {
        mBubbleFlowDraggable = bubbleFlowDraggable;
    }

    public void destroy() {
        //setOnTouchListener(null);
        setOnUpdateListener(null); // prevent memory leak
        mDraggableHelper.destroy();
    }

    public void setOnUpdateListener(OnUpdateListener onUpdateListener) {
        mOnUpdateListener = onUpdateListener;
    }

    @Override
    public DraggableHelper getDraggableHelper() {
        return mDraggableHelper;
    }

    @Override
    public void update(float dt) {
        if (mTouchDown) {
            if (mCurrentSnapTarget != null) {
                mTimeOnSnapTarget += dt;
                float snapTime = mTimeOnSnapTarget - Config.ANIMATE_TO_SNAP_TIME;
                if (mCurrentSnapTarget.isLongHovering() == false && snapTime >= Config.CLOSE_ALL_BUBBLES_DELAY) {
                    mCurrentSnapTarget.beginLongHovering();
                }
                if (!mAnimActive) {
                    Circle dc = mCurrentSnapTarget.GetDefaultCircle();
                    int xt = (int) (0.5f + dc.mX - Config.mBubbleWidth * 0.5f);
                    int yt = (int) (0.5f + dc.mY - Config.mBubbleHeight * 0.5f);
                    mDraggableHelper.setTargetPos(xt, yt, 0.02f, DraggableHelper.AnimationType.Linear, null);
                }
            }
            MainController.get().scheduleUpdate();
        }

        mDraggableHelper.update(dt);

        int x = mDraggableHelper.getXPos();
        int y = mDraggableHelper.getYPos();

        mDraggableBubbleMovedEvent.mX = x;
        mDraggableBubbleMovedEvent.mY = y;

        if (mOnUpdateListener != null) {
            mOnUpdateListener.onUpdate(BubbleDraggable.this, 0);
        }
    }

    @Override
    public void onOrientationChanged() {
        if (mMode == Mode.BubbleView) {
            doAnimateToBubbleView(1);
        } else {
            switchToExpandedView(MainController.get());
        }
    }

    public void setExactPos(int x, int y) {
        mDraggableHelper.setExactPos(x, y);
    }

    public void setTargetPos(int xp, int yp, float t, DraggableHelper.AnimationType type, boolean startDelay,
            DraggableHelper.AnimationEventListener listener) {
        try {
            Util.Assert(!mAnimActive, "mAnimActive:" + mAnimActive);
        } catch (AssertionError e) {
            mDraggableHelper.cancelAnimation();
            e.printStackTrace();
        }
        //Util.Assert(t > 0.0f, "t:" + t);      // Don't think this happens anymore - just to catch if it does happen and investigate why.
        mAnimActive = listener != null;
        if (!startDelay) {
            mDraggableHelper.setTargetPos(xp, yp, t, type, listener);
        } else {
            new SetTargetPosWithDelay(xp, yp, t, type, listener).execute();
        }
    }

    class SetTargetPosWithDelay extends AsyncTask<Void, Integer, Long> {
        int mxp;
        int myp;
        float mt;
        DraggableHelper.AnimationType mtype;
        DraggableHelper.AnimationEventListener mlistener;

        public SetTargetPosWithDelay(int xp, int yp, float t, DraggableHelper.AnimationType type,
                DraggableHelper.AnimationEventListener listener) {
            mxp = xp;
            myp = yp;
            mt = t;
            mtype = type;
            mlistener = listener;
        }

        protected Long doInBackground(Void... params) {
            try {
                Thread.sleep(Constant.BUBBLE_ANIMATION_EXPAND_DELAY);
            } catch (InterruptedException exc) {

            }

            publishProgress(0);

            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            mDraggableHelper.setTargetPos(mxp, myp, mt, mtype, mlistener);
        }
    }

}