• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright 2011 See AUTHORS file.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  ******************************************************************************/
16 
17 package com.badlogic.gdx.scenes.scene2d.utils;
18 
19 import com.badlogic.gdx.Input.Buttons;
20 import com.badlogic.gdx.math.Vector2;
21 import com.badlogic.gdx.scenes.scene2d.Actor;
22 import com.badlogic.gdx.scenes.scene2d.InputEvent;
23 import com.badlogic.gdx.scenes.scene2d.Stage;
24 import com.badlogic.gdx.scenes.scene2d.Touchable;
25 import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
26 import com.badlogic.gdx.utils.Array;
27 import com.badlogic.gdx.utils.ObjectMap;
28 import com.badlogic.gdx.utils.ObjectMap.Entry;
29 
30 /** Manages drag and drop operations through registered drag sources and drop targets.
31  * @author Nathan Sweet */
32 public class DragAndDrop {
33 	static final Vector2 tmpVector = new Vector2();
34 
35 	Payload payload;
36 	Actor dragActor;
37 	Target target;
38 	boolean isValidTarget;
39 	Array<Target> targets = new Array();
40 	ObjectMap<Source, DragListener> sourceListeners = new ObjectMap();
41 	private float tapSquareSize = 8;
42 	private int button;
43 	float dragActorX = 14, dragActorY = -20;
44 	float touchOffsetX, touchOffsetY;
45 	long dragStartTime;
46 	int dragTime = 250;
47 	int activePointer = -1;
48 	boolean cancelTouchFocus = true;
49 	boolean keepWithinStage = true;
50 
addSource(final Source source)51 	public void addSource (final Source source) {
52 		DragListener listener = new DragListener() {
53 			public void dragStart (InputEvent event, float x, float y, int pointer) {
54 				if (activePointer != -1) {
55 					event.stop();
56 					return;
57 				}
58 
59 				activePointer = pointer;
60 
61 				dragStartTime = System.currentTimeMillis();
62 				payload = source.dragStart(event, getTouchDownX(), getTouchDownY(), pointer);
63 				event.stop();
64 
65 				if (cancelTouchFocus && payload != null) source.getActor().getStage().cancelTouchFocusExcept(this, source.getActor());
66 			}
67 
68 			public void drag (InputEvent event, float x, float y, int pointer) {
69 				if (payload == null) return;
70 				if (pointer != activePointer) return;
71 
72 				Stage stage = event.getStage();
73 
74 				Touchable dragActorTouchable = null;
75 				if (dragActor != null) {
76 					dragActorTouchable = dragActor.getTouchable();
77 					dragActor.setTouchable(Touchable.disabled);
78 				}
79 
80 				// Find target.
81 				Target newTarget = null;
82 				isValidTarget = false;
83 				float stageX = event.getStageX() + touchOffsetX, stageY = event.getStageY() + touchOffsetY;
84 				Actor hit = event.getStage().hit(stageX, stageY, true); // Prefer touchable actors.
85 				if (hit == null) hit = event.getStage().hit(stageX, stageY, false);
86 				if (hit != null) {
87 					for (int i = 0, n = targets.size; i < n; i++) {
88 						Target target = targets.get(i);
89 						if (!target.actor.isAscendantOf(hit)) continue;
90 						newTarget = target;
91 						target.actor.stageToLocalCoordinates(tmpVector.set(stageX, stageY));
92 						break;
93 					}
94 				}
95 				//if over a new target, notify the former target that it's being left behind.
96 				if (newTarget != target) {
97 					if (target != null) target.reset(source, payload);
98 					target = newTarget;
99 				}
100 				//with any reset out of the way, notify new targets of drag.
101 				if (newTarget != null) {
102 					isValidTarget = newTarget.drag(source, payload, tmpVector.x, tmpVector.y, pointer);
103 				}
104 
105 				if (dragActor != null) dragActor.setTouchable(dragActorTouchable);
106 
107 				// Add/remove and position the drag actor.
108 				Actor actor = null;
109 				if (target != null) actor = isValidTarget ? payload.validDragActor : payload.invalidDragActor;
110 				if (actor == null) actor = payload.dragActor;
111 				if (actor == null) return;
112 				if (dragActor != actor) {
113 					if (dragActor != null) dragActor.remove();
114 					dragActor = actor;
115 					stage.addActor(actor);
116 				}
117 				float actorX = event.getStageX() + dragActorX;
118 				float actorY = event.getStageY() + dragActorY - actor.getHeight();
119 				if (keepWithinStage) {
120 					if (actorX < 0) actorX = 0;
121 					if (actorY < 0) actorY = 0;
122 					if (actorX + actor.getWidth() > stage.getWidth()) actorX = stage.getWidth() - actor.getWidth();
123 					if (actorY + actor.getHeight() > stage.getHeight()) actorY = stage.getHeight() - actor.getHeight();
124 				}
125 				actor.setPosition(actorX, actorY);
126 			}
127 
128 			public void dragStop (InputEvent event, float x, float y, int pointer) {
129 				if (pointer != activePointer) return;
130 				activePointer = -1;
131 				if (payload == null) return;
132 
133 				if (System.currentTimeMillis() - dragStartTime < dragTime) isValidTarget = false;
134 				if (dragActor != null) dragActor.remove();
135 				if (isValidTarget) {
136 					float stageX = event.getStageX() + touchOffsetX, stageY = event.getStageY() + touchOffsetY;
137 					target.actor.stageToLocalCoordinates(tmpVector.set(stageX, stageY));
138 					target.drop(source, payload, tmpVector.x, tmpVector.y, pointer);
139 				}
140 				source.dragStop(event, x, y, pointer, payload, isValidTarget ? target : null);
141 				if (target != null) target.reset(source, payload);
142 				payload = null;
143 				target = null;
144 				isValidTarget = false;
145 				dragActor = null;
146 			}
147 		};
148 		listener.setTapSquareSize(tapSquareSize);
149 		listener.setButton(button);
150 		source.actor.addCaptureListener(listener);
151 		sourceListeners.put(source, listener);
152 	}
153 
removeSource(Source source)154 	public void removeSource (Source source) {
155 		DragListener dragListener = sourceListeners.remove(source);
156 		source.actor.removeCaptureListener(dragListener);
157 	}
158 
addTarget(Target target)159 	public void addTarget (Target target) {
160 		targets.add(target);
161 	}
162 
removeTarget(Target target)163 	public void removeTarget (Target target) {
164 		targets.removeValue(target, true);
165 	}
166 
167 	/** Removes all targets and sources. */
clear()168 	public void clear () {
169 		targets.clear();
170 		for (Entry<Source, DragListener> entry : sourceListeners.entries())
171 			entry.key.actor.removeCaptureListener(entry.value);
172 		sourceListeners.clear();
173 	}
174 
175 	/** Sets the distance a touch must travel before being considered a drag. */
setTapSquareSize(float halfTapSquareSize)176 	public void setTapSquareSize (float halfTapSquareSize) {
177 		tapSquareSize = halfTapSquareSize;
178 	}
179 
180 	/** Sets the button to listen for, all other buttons are ignored. Default is {@link Buttons#LEFT}. Use -1 for any button. */
setButton(int button)181 	public void setButton (int button) {
182 		this.button = button;
183 	}
184 
setDragActorPosition(float dragActorX, float dragActorY)185 	public void setDragActorPosition (float dragActorX, float dragActorY) {
186 		this.dragActorX = dragActorX;
187 		this.dragActorY = dragActorY;
188 	}
189 
190 	/** Sets an offset in stage coordinates from the touch position which is used to determine the drop location. Default is 0,0. */
setTouchOffset(float touchOffsetX, float touchOffsetY)191 	public void setTouchOffset (float touchOffsetX, float touchOffsetY) {
192 		this.touchOffsetX = touchOffsetX;
193 		this.touchOffsetY = touchOffsetY;
194 	}
195 
isDragging()196 	public boolean isDragging () {
197 		return payload != null;
198 	}
199 
200 	/** Returns the current drag actor, or null. */
getDragActor()201 	public Actor getDragActor () {
202 		return dragActor;
203 	}
204 
205 	/** Time in milliseconds that a drag must take before a drop will be considered valid. This ignores an accidental drag and drop
206 	 * that was meant to be a click. Default is 250. */
setDragTime(int dragMillis)207 	public void setDragTime (int dragMillis) {
208 		this.dragTime = dragMillis;
209 	}
210 
211 	/** When true (default), the {@link Stage#cancelTouchFocus()} touch focus} is cancelled if
212 	 * {@link Source#dragStart(InputEvent, float, float, int) dragStart} returns non-null. This ensures the DragAndDrop is the only
213 	 * touch focus listener, eg when the source is inside a {@link ScrollPane} with flick scroll enabled. */
setCancelTouchFocus(boolean cancelTouchFocus)214 	public void setCancelTouchFocus (boolean cancelTouchFocus) {
215 		this.cancelTouchFocus = cancelTouchFocus;
216 	}
217 
setKeepWithinStage(boolean keepWithinStage)218 	public void setKeepWithinStage (boolean keepWithinStage) {
219 		this.keepWithinStage = keepWithinStage;
220 	}
221 
222 	/** A target where a payload can be dragged from.
223 	 * @author Nathan Sweet */
224 	static abstract public class Source {
225 		final Actor actor;
226 
Source(Actor actor)227 		public Source (Actor actor) {
228 			if (actor == null) throw new IllegalArgumentException("actor cannot be null.");
229 			this.actor = actor;
230 		}
231 
232 		/** @return May be null. */
dragStart(InputEvent event, float x, float y, int pointer)233 		abstract public Payload dragStart (InputEvent event, float x, float y, int pointer);
234 
235 		/** @param payload null if dragStart returned null.
236 		 * @param target null if not dropped on a valid target. */
dragStop(InputEvent event, float x, float y, int pointer, Payload payload, Target target)237 		public void dragStop (InputEvent event, float x, float y, int pointer, Payload payload, Target target) {
238 		}
239 
getActor()240 		public Actor getActor () {
241 			return actor;
242 		}
243 	}
244 
245 	/** A target where a payload can be dropped to.
246 	 * @author Nathan Sweet */
247 	static abstract public class Target {
248 		final Actor actor;
249 
Target(Actor actor)250 		public Target (Actor actor) {
251 			if (actor == null) throw new IllegalArgumentException("actor cannot be null.");
252 			this.actor = actor;
253 			Stage stage = actor.getStage();
254 			if (stage != null && actor == stage.getRoot())
255 				throw new IllegalArgumentException("The stage root cannot be a drag and drop target.");
256 		}
257 
258 		/** Called when the object is dragged over the target. The coordinates are in the target's local coordinate system.
259 		 * @return true if this is a valid target for the object. */
drag(Source source, Payload payload, float x, float y, int pointer)260 		abstract public boolean drag (Source source, Payload payload, float x, float y, int pointer);
261 
262 		/** Called when the object is no longer over the target, whether because the touch was moved or a drop occurred. */
reset(Source source, Payload payload)263 		public void reset (Source source, Payload payload) {
264 		}
265 
drop(Source source, Payload payload, float x, float y, int pointer)266 		abstract public void drop (Source source, Payload payload, float x, float y, int pointer);
267 
getActor()268 		public Actor getActor () {
269 			return actor;
270 		}
271 	}
272 
273 	/** The payload of a drag and drop operation. Actors can be optionally provided to follow the cursor and change when over a
274 	 * target. Such Actors will be added and removed from the stage dynamically during the drag operation. Care should be taken
275 	 * when using the source Actor as a payload drag actor. */
276 	static public class Payload {
277 		Actor dragActor, validDragActor, invalidDragActor;
278 		Object object;
279 
setDragActor(Actor dragActor)280 		public void setDragActor (Actor dragActor) {
281 			this.dragActor = dragActor;
282 		}
283 
getDragActor()284 		public Actor getDragActor () {
285 			return dragActor;
286 		}
287 
setValidDragActor(Actor validDragActor)288 		public void setValidDragActor (Actor validDragActor) {
289 			this.validDragActor = validDragActor;
290 		}
291 
getValidDragActor()292 		public Actor getValidDragActor () {
293 			return validDragActor;
294 		}
295 
setInvalidDragActor(Actor invalidDragActor)296 		public void setInvalidDragActor (Actor invalidDragActor) {
297 			this.invalidDragActor = invalidDragActor;
298 		}
299 
getInvalidDragActor()300 		public Actor getInvalidDragActor () {
301 			return invalidDragActor;
302 		}
303 
getObject()304 		public Object getObject () {
305 			return object;
306 		}
307 
setObject(Object object)308 		public void setObject (Object object) {
309 			this.object = object;
310 		}
311 	}
312 }
313