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