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.ui; 18 19 import com.badlogic.gdx.graphics.Color; 20 import com.badlogic.gdx.graphics.g2d.Batch; 21 import com.badlogic.gdx.math.Circle; 22 import com.badlogic.gdx.math.Vector2; 23 import com.badlogic.gdx.scenes.scene2d.Actor; 24 import com.badlogic.gdx.scenes.scene2d.InputEvent; 25 import com.badlogic.gdx.scenes.scene2d.InputListener; 26 import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; 27 import com.badlogic.gdx.scenes.scene2d.utils.Drawable; 28 import com.badlogic.gdx.utils.Pools; 29 30 /** An on-screen joystick. The movement area of the joystick is circular, centered on the touchpad, and its size determined by the 31 * smaller touchpad dimension. 32 * <p> 33 * The preferred size of the touchpad is determined by the background. 34 * <p> 35 * {@link ChangeEvent} is fired when the touchpad knob is moved. Cancelling the event will move the knob to where it was 36 * previously. 37 * @author Josh Street */ 38 public class Touchpad extends Widget { 39 private TouchpadStyle style; 40 boolean touched; 41 boolean resetOnTouchUp = true; 42 private float deadzoneRadius; 43 private final Circle knobBounds = new Circle(0, 0, 0); 44 private final Circle touchBounds = new Circle(0, 0, 0); 45 private final Circle deadzoneBounds = new Circle(0, 0, 0); 46 private final Vector2 knobPosition = new Vector2(); 47 private final Vector2 knobPercent = new Vector2(); 48 49 /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ 50 public Touchpad (float deadzoneRadius, Skin skin) { 51 this(deadzoneRadius, skin.get(TouchpadStyle.class)); 52 } 53 54 /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ 55 public Touchpad (float deadzoneRadius, Skin skin, String styleName) { 56 this(deadzoneRadius, skin.get(styleName, TouchpadStyle.class)); 57 } 58 59 /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ 60 public Touchpad (float deadzoneRadius, TouchpadStyle style) { 61 if (deadzoneRadius < 0) throw new IllegalArgumentException("deadzoneRadius must be > 0"); 62 this.deadzoneRadius = deadzoneRadius; 63 64 knobPosition.set(getWidth() / 2f, getHeight() / 2f); 65 66 setStyle(style); 67 setSize(getPrefWidth(), getPrefHeight()); 68 69 addListener(new InputListener() { 70 @Override 71 public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { 72 if (touched) return false; 73 touched = true; 74 calculatePositionAndValue(x, y, false); 75 return true; 76 } 77 78 @Override 79 public void touchDragged (InputEvent event, float x, float y, int pointer) { 80 calculatePositionAndValue(x, y, false); 81 } 82 83 @Override 84 public void touchUp (InputEvent event, float x, float y, int pointer, int button) { 85 touched = false; 86 calculatePositionAndValue(x, y, resetOnTouchUp); 87 } 88 }); 89 } 90 91 void calculatePositionAndValue (float x, float y, boolean isTouchUp) { 92 float oldPositionX = knobPosition.x; 93 float oldPositionY = knobPosition.y; 94 float oldPercentX = knobPercent.x; 95 float oldPercentY = knobPercent.y; 96 float centerX = knobBounds.x; 97 float centerY = knobBounds.y; 98 knobPosition.set(centerX, centerY); 99 knobPercent.set(0f, 0f); 100 if (!isTouchUp) { 101 if (!deadzoneBounds.contains(x, y)) { 102 knobPercent.set((x - centerX) / knobBounds.radius, (y - centerY) / knobBounds.radius); 103 float length = knobPercent.len(); 104 if (length > 1) knobPercent.scl(1 / length); 105 if (knobBounds.contains(x, y)) { 106 knobPosition.set(x, y); 107 } else { 108 knobPosition.set(knobPercent).nor().scl(knobBounds.radius).add(knobBounds.x, knobBounds.y); 109 } 110 } 111 } 112 if (oldPercentX != knobPercent.x || oldPercentY != knobPercent.y) { 113 ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); 114 if (fire(changeEvent)) { 115 knobPercent.set(oldPercentX, oldPercentY); 116 knobPosition.set(oldPositionX, oldPositionY); 117 } 118 Pools.free(changeEvent); 119 } 120 } 121 122 public void setStyle (TouchpadStyle style) { 123 if (style == null) throw new IllegalArgumentException("style cannot be null"); 124 this.style = style; 125 invalidateHierarchy(); 126 } 127 128 /** Returns the touchpad's style. Modifying the returned style may not have an effect until {@link #setStyle(TouchpadStyle)} is 129 * called. */ 130 public TouchpadStyle getStyle () { 131 return style; 132 } 133 134 @Override 135 public Actor hit (float x, float y, boolean touchable) { 136 return touchBounds.contains(x, y) ? this : null; 137 } 138 139 @Override 140 public void layout () { 141 // Recalc pad and deadzone bounds 142 float halfWidth = getWidth() / 2; 143 float halfHeight = getHeight() / 2; 144 float radius = Math.min(halfWidth, halfHeight); 145 touchBounds.set(halfWidth, halfHeight, radius); 146 if (style.knob != null) radius -= Math.max(style.knob.getMinWidth(), style.knob.getMinHeight()) / 2; 147 knobBounds.set(halfWidth, halfHeight, radius); 148 deadzoneBounds.set(halfWidth, halfHeight, deadzoneRadius); 149 // Recalc pad values and knob position 150 knobPosition.set(halfWidth, halfHeight); 151 knobPercent.set(0, 0); 152 } 153 154 @Override 155 public void draw (Batch batch, float parentAlpha) { 156 validate(); 157 158 Color c = getColor(); 159 batch.setColor(c.r, c.g, c.b, c.a * parentAlpha); 160 161 float x = getX(); 162 float y = getY(); 163 float w = getWidth(); 164 float h = getHeight(); 165 166 final Drawable bg = style.background; 167 if (bg != null) bg.draw(batch, x, y, w, h); 168 169 final Drawable knob = style.knob; 170 if (knob != null) { 171 x += knobPosition.x - knob.getMinWidth() / 2f; 172 y += knobPosition.y - knob.getMinHeight() / 2f; 173 knob.draw(batch, x, y, knob.getMinWidth(), knob.getMinHeight()); 174 } 175 } 176 177 @Override 178 public float getPrefWidth () { 179 return style.background != null ? style.background.getMinWidth() : 0; 180 } 181 182 @Override 183 public float getPrefHeight () { 184 return style.background != null ? style.background.getMinHeight() : 0; 185 } 186 187 public boolean isTouched () { 188 return touched; 189 } 190 191 public boolean getResetOnTouchUp () { 192 return resetOnTouchUp; 193 } 194 195 /** @param reset Whether to reset the knob to the center on touch up. */ 196 public void setResetOnTouchUp (boolean reset) { 197 this.resetOnTouchUp = reset; 198 } 199 200 /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ 201 public void setDeadzone (float deadzoneRadius) { 202 if (deadzoneRadius < 0) throw new IllegalArgumentException("deadzoneRadius must be > 0"); 203 this.deadzoneRadius = deadzoneRadius; 204 invalidate(); 205 } 206 207 /** Returns the x-position of the knob relative to the center of the widget. The positive direction is right. */ 208 public float getKnobX () { 209 return knobPosition.x; 210 } 211 212 /** Returns the y-position of the knob relative to the center of the widget. The positive direction is up. */ 213 public float getKnobY () { 214 return knobPosition.y; 215 } 216 217 /** Returns the x-position of the knob as a percentage from the center of the touchpad to the edge of the circular movement 218 * area. The positive direction is right. */ 219 public float getKnobPercentX () { 220 return knobPercent.x; 221 } 222 223 /** Returns the y-position of the knob as a percentage from the center of the touchpad to the edge of the circular movement 224 * area. The positive direction is up. */ 225 public float getKnobPercentY () { 226 return knobPercent.y; 227 } 228 229 /** The style for a {@link Touchpad}. 230 * @author Josh Street */ 231 public static class TouchpadStyle { 232 /** Stretched in both directions. Optional. */ 233 public Drawable background; 234 235 /** Optional. */ 236 public Drawable knob; 237 238 public TouchpadStyle () { 239 } 240 241 public TouchpadStyle (Drawable background, Drawable knob) { 242 this.background = background; 243 this.knob = knob; 244 } 245 246 public TouchpadStyle (TouchpadStyle style) { 247 this.background = style.background; 248 this.knob = style.knob; 249 } 250 } 251 } 252