com.kotcrab.vis.ui.widget.Draggable.java Source code

Java tutorial

Introduction

Here is the source code for com.kotcrab.vis.ui.widget.Draggable.java

Source

/*
 * Copyright 2014-2016 See AUTHORS file.
 *
 * 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.kotcrab.vis.ui.widget;

import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.utils.Disableable;
import com.kotcrab.vis.ui.layout.DragPane;

import java.util.Iterator;

/**
 * Draws copies of dragged actors which have this listener attached.
 * @author MJ
 * @since 0.9.3
 */
public class Draggable extends InputListener {
    private static final Vector2 MIMIC_COORDINATES = new Vector2();
    private static final Vector2 STAGE_COORDINATES = new Vector2();

    /**
     * Initial fading time value of dragged actors.
     * @see #setFadingTime(float)
     */
    public static float DEFAULT_FADING_TIME = 0.1f;
    /**
     * Initial moving time value of dragged actors.
     * @see #setMovingTime(float)
     */
    public static float DEFAULT_MOVING_TIME = 0.1f;
    /**
     * Initial invisibility setting of dragged actors.
     * @see #setInvisibleWhenDragged(boolean)
     */
    public static boolean INVISIBLE_ON_DRAG = false;
    /**
     * Initial setting of keeping the dragged widget within its parent's bounds.
     * @see #setKeepWithinParent(boolean)
     */
    public static boolean KEEP_WITHIN_PARENT = false;
    /**
     * Initial alpha setting of dragged actors.
     * @see #setAlpha(float)
     */
    public static float DEFAULT_ALPHA = 1f;
    /**
     * Initial listener of draggables, unless a different listener is specified in the constructor. By default,
     * {@link DragPane.DefaultDragListener} is used, which allows to drag actors into {@link DragPane} widgets.
     * @see #setListener(DragListener)
     * @see DragListener
     */
    public static DragListener DEFAULT_LISTENER = new DragPane.DefaultDragListener();
    /**
     * If true, other actors will not receive mouse events while the actor is dragged.
     * @see #setBlockInput(boolean)
     */
    public static boolean BLOCK_INPUT = true;
    /*** Blocks mouse input during dragging. */
    private static final Actor BLOCKER = new Actor();

    // Settings.
    private DragListener listener;
    private boolean blockInput = BLOCK_INPUT;
    private boolean invisibleWhenDragged = INVISIBLE_ON_DRAG;
    private boolean keepWithinParent = KEEP_WITHIN_PARENT;
    private float deadzoneRadius;
    private float fadingTime = DEFAULT_FADING_TIME;
    private float movingTime = DEFAULT_FADING_TIME;
    private float alpha = DEFAULT_ALPHA;
    private Interpolation fadingInterpolation = Interpolation.fade;
    private Interpolation movingInterpolation = Interpolation.sineOut;

    // Control variables.
    private final MimicActor mimic = new MimicActor();
    private float dragStartX;
    private float dragStartY;
    private float offsetX;
    private float offsetY;

    /** Creates a new draggable with default listener. */
    public Draggable() {
        this(DEFAULT_LISTENER);
    }

    /** @param listener is being notified of draggable events and can change its behavior. Can be null. */
    public Draggable(final DragListener listener) {
        this.listener = listener;
        mimic.setTouchable(Touchable.disabled);
    }

    static {
        // Blocks mouse input.
        BLOCKER.addListener(new InputListener() {
            @Override
            public boolean mouseMoved(final InputEvent event, final float x, final float y) {
                return true;
            }

            @Override
            public boolean touchDown(final InputEvent event, final float x, final float y, final int pointer,
                    final int button) {
                return true;
            }

            @Override
            public boolean scrolled(final InputEvent event, final float x, final float y, final int amount) {
                return true;
            }
        });
    }

    /**
     * @param actor will have this listener attached and all other {@link Draggable} listeners removed. If you want multiple
     * {@link Draggable} listeners or you are sure that the widget has no other {@link Draggable}s attached, you can add
     * the listener using the standard method: {@link Actor#addListener(EventListener)} - avoiding validation and
     * iteration over actor's listeners.
     */
    public void attachTo(final Actor actor) {
        for (final Iterator<EventListener> listeners = actor.getListeners().iterator(); listeners.hasNext();) {
            final EventListener listener = listeners.next();
            if (listener instanceof Draggable) {
                listeners.remove();
            }
        }
        actor.addListener(this);
    }

    /** @return during dragging, this is the offset value on X axis from the start of the dragged widget. */
    public float getOffsetX() {
        return offsetX;
    }

    /** @return during dragging, this is the offset value on Y axis from the start of the dragged widget. */
    public float getOffsetY() {
        return offsetY;
    }

    /** @return alpha color value of dragged actor copy. */
    public float getAlpha() {
        return alpha;
    }

    /** @param alpha alpha color value of dragged actor copy. */
    public void setAlpha(final float alpha) {
        this.alpha = alpha;
    }

    /** @return true if mouse input is blocked during dragging. */
    public boolean isBlockingInput() {
        return blockInput;
    }

    /**
     * @param blockInput true if mouse input should be blocked during actors dragging. If false, other actors might still receive
     * mouse events (for example, buttons might switch to "over" style).
     */
    public void setBlockInput(final boolean blockInput) {
        this.blockInput = blockInput;
    }

    /** @return if true, original actor is invisible while it's being dragged. */
    public boolean isInvisibleWhenDragged() {
        return invisibleWhenDragged;
    }

    /** @param invisibleWhenDragged if true, original actor is invisible while it's being dragged. */
    public void setInvisibleWhenDragged(final boolean invisibleWhenDragged) {
        this.invisibleWhenDragged = invisibleWhenDragged;
    }

    /** @return if true, widget cannot be dragged out of the bounds of its parent. */
    public boolean isKeptWithinParent() {
        return keepWithinParent;
    }

    /**
     * @param keepWithinParent if true, widget cannot be dragged out of the bounds of its parent. Stage coordinates in listener
     * will always be inside the parent. Note that for this setting to work properly, both actor and its parent have to
     * correctly return their sizes with {@link Actor#getWidth()} and {@link Actor#getHeight()} methods.
     */
    public void setKeepWithinParent(final boolean keepWithinParent) {
        this.keepWithinParent = keepWithinParent;
    }

    /** @return distance from the widget's parent in which the actor is not dragged out of parent bounds. */
    public float getDeadzoneRadius() {
        return deadzoneRadius;
    }

    /**
     * @param deadzoneRadius distance from the widget's parent in which the actor is not dragged out of parent bounds. Defaults to
     * 0f. Values lower or equal to 0 are ignored during dragging. If {@link #isKeptWithinParent()} returns true, this
     * value is ignored and actor is always kept within parent.
     */
    public void setDeadzoneRadius(float deadzoneRadius) {
        this.deadzoneRadius = deadzoneRadius;
    }

    /** @return time after which the dragged actor copy disappears. */
    public float getFadingTime() {
        return fadingTime;
    }

    /** @param fadingTime time after which the dragged actor copy disappears. */
    public void setFadingTime(final float fadingTime) {
        this.fadingTime = fadingTime;
    }

    /** @return time after which the dragged actor copy returns to its origin. */
    public float getMovingTime() {
        return movingTime;
    }

    /** @param movingTime time after which the dragged actor copy returns to its origin. */
    public void setMovingTime(final float movingTime) {
        this.movingTime = movingTime;
    }

    /** @param movingInterpolation used to move the dragged widgets to the original position when their drag was cancelled. */
    public void setMovingInterpolation(final Interpolation movingInterpolation) {
        this.movingInterpolation = movingInterpolation;
    }

    /** @param fadingInterpolation used to fade out dragged widgets after their drag was accepted. */
    public void setFadingInterpolation(final Interpolation fadingInterpolation) {
        this.fadingInterpolation = fadingInterpolation;
    }

    /**
     * @param listener is being notified of draggable events and can change its behavior. Can be null.
     * @see DragAdapter
     */
    public void setListener(final DragListener listener) {
        this.listener = listener;
    }

    /** @return listener notified of draggable events. Can be null. */
    public DragListener getListener() {
        return listener;
    }

    @Override
    public boolean touchDown(final InputEvent event, final float x, final float y, final int pointer,
            final int button) {
        final Actor actor = event.getListenerActor();
        if (!isValid(actor) || isDisabled(actor)) {
            return false;
        }
        if (listener == null || listener.onStart(this, actor, event.getStageX(), event.getStageY())) {
            attachMimic(actor, event, x, y);
            return true;
        }
        return false;
    }

    /**
     * @param actor might be already removed.
     * @return true if actor is not null and has a {@link Stage}.
     */
    protected boolean isValid(final Actor actor) {
        return actor != null && actor.getStage() != null;
    }

    /**
     * @param actor might be a {@link Disableable}.
     * @return true if actor is disabled.
     */
    protected boolean isDisabled(final Actor actor) {
        return actor instanceof Disableable && ((Disableable) actor).isDisabled();
    }

    /**
     * @param actor has the listener attached.
     * @param event touch down event which triggered mimic spawning.
     * @param x actor's relative X event position.
     * @param y actor's relative Y event position.
     */
    protected void attachMimic(final Actor actor, final InputEvent event, final float x, final float y) {
        mimic.clearActions();
        mimic.getColor().a = alpha;
        mimic.setActor(actor);
        offsetX = -x;
        offsetY = -y;
        getStageCoordinates(event);
        dragStartX = MIMIC_COORDINATES.x;
        dragStartY = MIMIC_COORDINATES.y;
        mimic.setPosition(dragStartX, dragStartY);
        actor.getStage().addActor(mimic);
        mimic.toFront();
        actor.setVisible(!invisibleWhenDragged);
        if (blockInput) {
            addBlocker(actor.getStage());
        }
    }

    /** @param stage will contain a mock-up blocker actor, which blocks all mouse input. */
    protected static void addBlocker(final Stage stage) {
        stage.addActor(BLOCKER);
        BLOCKER.setBounds(0f, 0f, stage.getWidth(), stage.getHeight());
        BLOCKER.toFront();
    }

    /** Removes mock-up blocker actor from the stage. */
    protected static void removeBlocker() {
        BLOCKER.remove();
    }

    /** @param event will extract stage coordinates from the event, respecting mimic offset and other dragging settings. */
    protected void getStageCoordinates(final InputEvent event) {
        if (keepWithinParent) {
            getStageCoordinatesWithinParent(event);
        } else if (deadzoneRadius > 0f) {
            getStageCoordinatesWithDeadzone(event);
        } else {
            getStageCoordinatesWithOffset(event);
        }
    }

    private void getStageCoordinatesWithDeadzone(final InputEvent event) {
        final Actor parent = mimic.getActor().getParent();
        if (parent != null) {
            MIMIC_COORDINATES.set(Vector2.Zero);
            parent.localToStageCoordinates(MIMIC_COORDINATES);
            final float parentX = MIMIC_COORDINATES.x;
            final float parentY = MIMIC_COORDINATES.y;
            final float parentEndX = parentX + parent.getWidth();
            final float parentEndY = parentY + parent.getHeight();
            if (isWithinDeadzone(event, parentX, parentY, parentEndX, parentEndY)) {
                // Keeping within parent bounds:
                MIMIC_COORDINATES.set(event.getStageX() + offsetX, event.getStageY() + offsetY);
                if (MIMIC_COORDINATES.x < parentX) {
                    MIMIC_COORDINATES.x = parentX;
                } else if (MIMIC_COORDINATES.x + mimic.getWidth() > parentEndX) {
                    MIMIC_COORDINATES.x = parentEndX - mimic.getWidth();
                }
                if (MIMIC_COORDINATES.y < parentY) {
                    MIMIC_COORDINATES.y = parentY;
                } else if (MIMIC_COORDINATES.y + mimic.getHeight() > parentEndY) {
                    MIMIC_COORDINATES.y = parentEndY - mimic.getHeight();
                }
                STAGE_COORDINATES.set(MathUtils.clamp(event.getStageX(), parentX, parentEndX - 1f),
                        MathUtils.clamp(event.getStageY(), parentY, parentEndY - 1f));
            } else {
                getStageCoordinatesWithOffset(event);
            }
        } else {
            getStageCoordinatesWithOffset(event);
        }
    }

    private boolean isWithinDeadzone(InputEvent event, float parentX, float parentY, float parentEndX,
            float parentEndY) {
        return parentX - deadzoneRadius <= event.getStageX() && parentEndX + deadzoneRadius >= event.getStageX()
                && parentY - deadzoneRadius <= event.getStageY()
                && parentEndY + deadzoneRadius >= event.getStageY();
    }

    private void getStageCoordinatesWithinParent(final InputEvent event) {
        final Actor parent = mimic.getActor().getParent();
        if (parent != null) {
            MIMIC_COORDINATES.set(Vector2.Zero);
            parent.localToStageCoordinates(MIMIC_COORDINATES);
            final float parentX = MIMIC_COORDINATES.x;
            final float parentY = MIMIC_COORDINATES.y;
            final float parentEndX = parentX + parent.getWidth();
            final float parentEndY = parentY + parent.getHeight();
            MIMIC_COORDINATES.set(event.getStageX() + offsetX, event.getStageY() + offsetY);
            if (MIMIC_COORDINATES.x < parentX) {
                MIMIC_COORDINATES.x = parentX;
            } else if (MIMIC_COORDINATES.x + mimic.getWidth() > parentEndX) {
                MIMIC_COORDINATES.x = parentEndX - mimic.getWidth();
            }
            if (MIMIC_COORDINATES.y < parentY) {
                MIMIC_COORDINATES.y = parentY;
            } else if (MIMIC_COORDINATES.y + mimic.getHeight() > parentEndY) {
                MIMIC_COORDINATES.y = parentEndY - mimic.getHeight();
            }
            STAGE_COORDINATES.set(MathUtils.clamp(event.getStageX(), parentX, parentEndX - 1f),
                    MathUtils.clamp(event.getStageY(), parentY, parentEndY - 1f));
        } else {
            getStageCoordinatesWithOffset(event);
        }
    }

    private void getStageCoordinatesWithOffset(final InputEvent event) {
        MIMIC_COORDINATES.set(event.getStageX() + offsetX, event.getStageY() + offsetY);
        STAGE_COORDINATES.set(event.getStageX(), event.getStageY());
    }

    @Override
    public void touchDragged(final InputEvent event, final float x, final float y, final int pointer) {
        if (isDragged()) {
            getStageCoordinates(event);
            mimic.setPosition(MIMIC_COORDINATES.x, MIMIC_COORDINATES.y);
            if (listener != null) {
                listener.onDrag(this, mimic.getActor(), STAGE_COORDINATES.x, STAGE_COORDINATES.y);
            }
        }
    }

    @Override
    public void touchUp(final InputEvent event, final float x, final float y, final int pointer, final int button) {
        if (isDragged()) {
            removeBlocker();
            getStageCoordinates(event);
            mimic.setPosition(MIMIC_COORDINATES.x, MIMIC_COORDINATES.y);
            if (listener == null || mimic.getActor().getStage() != null
                    && listener.onEnd(this, mimic.getActor(), STAGE_COORDINATES.x, STAGE_COORDINATES.y)) {
                // Drag end approved - fading out.
                addMimicHidingAction(Actions.fadeOut(fadingTime, fadingInterpolation), fadingTime);
            } else {
                // Drag end cancelled - returning to the original position.
                addMimicHidingAction(Actions.moveTo(dragStartX, dragStartY, movingTime, movingInterpolation),
                        movingTime);
            }
        }
    }

    /** @return true if some actor with this listener attached is currently dragged. */
    public boolean isDragged() {
        return mimic.getActor() != null;
    }

    /** @param hidingAction will be attached to the mimic actor. */
    protected void addMimicHidingAction(final Action hidingAction, final float delay) {
        mimic.addAction(Actions.sequence(hidingAction, Actions.removeActor()));
        mimic.getActor().addAction(Actions.delay(delay, Actions.visible(true)));
    }

    /**
     * Allows to control {@link Draggable} behavior.
     * @author MJ
     * @since 0.9.3
     */
    public static interface DragListener {
        /** Use in listner's method for code clarity. */
        boolean CANCEL = false, APPROVE = true;

        /**
         * @param draggable source of event.
         * @param actor is about to be dragged.
         * @param stageX stage coordinate on X axis where the drag started.
         * @param stageY stage coordinate on Y axis where the drag started.
         * @return if true, actor will not be dragged.
         */
        boolean onStart(Draggable draggable, Actor actor, float stageX, float stageY);

        /**
         * @param draggable source of event.
         * @param actor is being dragged.
         * @param stageX stage coordinate on X axis with current cursor position.
         * @param stageY stage coordinate on Y axis with current cursor position.
         */
        void onDrag(Draggable draggable, Actor actor, float stageX, float stageY);

        /**
         * @param draggable source of event.
         * @param actor is about to stop being dragged.
         * @param stageX stage coordinate on X axis where the drag ends.
         * @param stageY stage coordinate on X axis where the drag ends.
         * @return if true, "mirror" of the actor will quickly fade out. If false, mirror will return to the original actor's
         * position.
         */
        boolean onEnd(Draggable draggable, Actor actor, float stageX, float stageY);
    }

    /**
     * Default, empty implementation of {@link DragListener}. Approves all drag requests.
     * @author MJ
     * @since 0.9.3
     */
    public static class DragAdapter implements DragListener {
        @Override
        public boolean onStart(final Draggable draggable, final Actor actor, final float stageX,
                final float stageY) {
            return APPROVE;
        }

        @Override
        public void onDrag(final Draggable draggable, final Actor actor, final float stageX, final float stageY) {
        }

        @Override
        public boolean onEnd(final Draggable draggable, final Actor actor, final float stageX, final float stageY) {
            return APPROVE;
        }
    }

    /**
     * Draws the chosen actor with modified alpha value in a custom position. Clears mimicked actor upon removing from the stage.
     * @author MJ
     * @since 0.9.3
     */
    public static class MimicActor extends Actor {
        private static final Vector2 LAST_POSITION = new Vector2();
        private Actor actor;

        /** Has no actor to mimic. See {@link #setActor(Actor)}. */
        public MimicActor() {
        }

        /** @param actor will be mimicked. */
        public MimicActor(final Actor actor) {
            this.actor = actor;
        }

        @Override
        public boolean remove() {
            actor = null;
            return super.remove();
        }

        /** @return mimicked actor. */
        public Actor getActor() {
            return actor;
        }

        /** @param actor will be mimicked. */
        public void setActor(final Actor actor) {
            this.actor = actor;
        }

        @Override
        public float getWidth() {
            return actor == null ? 0f : actor.getWidth();
        }

        @Override
        public float getHeight() {
            return actor == null ? 0f : actor.getHeight();
        }

        @Override
        public void draw(final Batch batch, final float parentAlpha) {
            if (actor != null) {
                LAST_POSITION.set(actor.getX(), actor.getY());
                actor.setPosition(getX(), getY());
                actor.draw(batch, getColor().a * parentAlpha);
                actor.setPosition(LAST_POSITION.x, LAST_POSITION.y);
            }
        }
    }
}