Home | History | Annotate | Download | only in ui
      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