All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kotcrab.vis.ui.widget.Draggable Maven / Gradle / Ivy

There is a newer version: 1.5.3
Show newest version
/*
 * 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 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);
			}
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy