/******************************************************************************* * Copyright 2011 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.badlogic.gdx.scenes.scene2d.ui; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Circle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Pools; /** An on-screen joystick. The movement area of the joystick is circular, centered on the touchpad, and its size determined by the * smaller touchpad dimension. * <p> * The preferred size of the touchpad is determined by the background. * <p> * {@link ChangeEvent} is fired when the touchpad knob is moved. Cancelling the event will move the knob to where it was * previously. * @author Josh Street */ public class Touchpad extends Widget { private TouchpadStyle style; boolean touched; boolean resetOnTouchUp = true; private float deadzoneRadius; private final Circle knobBounds = new Circle(0, 0, 0); private final Circle touchBounds = new Circle(0, 0, 0); private final Circle deadzoneBounds = new Circle(0, 0, 0); private final Vector2 knobPosition = new Vector2(); private final Vector2 knobPercent = new Vector2(); /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ public Touchpad (float deadzoneRadius, Skin skin) { this(deadzoneRadius, skin.get(TouchpadStyle.class)); } /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ public Touchpad (float deadzoneRadius, Skin skin, String styleName) { this(deadzoneRadius, skin.get(styleName, TouchpadStyle.class)); } /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ public Touchpad (float deadzoneRadius, TouchpadStyle style) { if (deadzoneRadius < 0) throw new IllegalArgumentException("deadzoneRadius must be > 0"); this.deadzoneRadius = deadzoneRadius; knobPosition.set(getWidth() / 2f, getHeight() / 2f); setStyle(style); setSize(getPrefWidth(), getPrefHeight()); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (touched) return false; touched = true; calculatePositionAndValue(x, y, false); return true; } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { calculatePositionAndValue(x, y, false); } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { touched = false; calculatePositionAndValue(x, y, resetOnTouchUp); } }); } void calculatePositionAndValue (float x, float y, boolean isTouchUp) { float oldPositionX = knobPosition.x; float oldPositionY = knobPosition.y; float oldPercentX = knobPercent.x; float oldPercentY = knobPercent.y; float centerX = knobBounds.x; float centerY = knobBounds.y; knobPosition.set(centerX, centerY); knobPercent.set(0f, 0f); if (!isTouchUp) { if (!deadzoneBounds.contains(x, y)) { knobPercent.set((x - centerX) / knobBounds.radius, (y - centerY) / knobBounds.radius); float length = knobPercent.len(); if (length > 1) knobPercent.scl(1 / length); if (knobBounds.contains(x, y)) { knobPosition.set(x, y); } else { knobPosition.set(knobPercent).nor().scl(knobBounds.radius).add(knobBounds.x, knobBounds.y); } } } if (oldPercentX != knobPercent.x || oldPercentY != knobPercent.y) { ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); if (fire(changeEvent)) { knobPercent.set(oldPercentX, oldPercentY); knobPosition.set(oldPositionX, oldPositionY); } Pools.free(changeEvent); } } public void setStyle (TouchpadStyle style) { if (style == null) throw new IllegalArgumentException("style cannot be null"); this.style = style; invalidateHierarchy(); } /** Returns the touchpad's style. Modifying the returned style may not have an effect until {@link #setStyle(TouchpadStyle)} is * called. */ public TouchpadStyle getStyle () { return style; } @Override public Actor hit (float x, float y, boolean touchable) { return touchBounds.contains(x, y) ? this : null; } @Override public void layout () { // Recalc pad and deadzone bounds float halfWidth = getWidth() / 2; float halfHeight = getHeight() / 2; float radius = Math.min(halfWidth, halfHeight); touchBounds.set(halfWidth, halfHeight, radius); if (style.knob != null) radius -= Math.max(style.knob.getMinWidth(), style.knob.getMinHeight()) / 2; knobBounds.set(halfWidth, halfHeight, radius); deadzoneBounds.set(halfWidth, halfHeight, deadzoneRadius); // Recalc pad values and knob position knobPosition.set(halfWidth, halfHeight); knobPercent.set(0, 0); } @Override public void draw (Batch batch, float parentAlpha) { validate(); Color c = getColor(); batch.setColor(c.r, c.g, c.b, c.a * parentAlpha); float x = getX(); float y = getY(); float w = getWidth(); float h = getHeight(); final Drawable bg = style.background; if (bg != null) bg.draw(batch, x, y, w, h); final Drawable knob = style.knob; if (knob != null) { x += knobPosition.x - knob.getMinWidth() / 2f; y += knobPosition.y - knob.getMinHeight() / 2f; knob.draw(batch, x, y, knob.getMinWidth(), knob.getMinHeight()); } } @Override public float getPrefWidth () { return style.background != null ? style.background.getMinWidth() : 0; } @Override public float getPrefHeight () { return style.background != null ? style.background.getMinHeight() : 0; } public boolean isTouched () { return touched; } public boolean getResetOnTouchUp () { return resetOnTouchUp; } /** @param reset Whether to reset the knob to the center on touch up. */ public void setResetOnTouchUp (boolean reset) { this.resetOnTouchUp = reset; } /** @param deadzoneRadius The distance in pixels from the center of the touchpad required for the knob to be moved. */ public void setDeadzone (float deadzoneRadius) { if (deadzoneRadius < 0) throw new IllegalArgumentException("deadzoneRadius must be > 0"); this.deadzoneRadius = deadzoneRadius; invalidate(); } /** Returns the x-position of the knob relative to the center of the widget. The positive direction is right. */ public float getKnobX () { return knobPosition.x; } /** Returns the y-position of the knob relative to the center of the widget. The positive direction is up. */ public float getKnobY () { return knobPosition.y; } /** Returns the x-position of the knob as a percentage from the center of the touchpad to the edge of the circular movement * area. The positive direction is right. */ public float getKnobPercentX () { return knobPercent.x; } /** Returns the y-position of the knob as a percentage from the center of the touchpad to the edge of the circular movement * area. The positive direction is up. */ public float getKnobPercentY () { return knobPercent.y; } /** The style for a {@link Touchpad}. * @author Josh Street */ public static class TouchpadStyle { /** Stretched in both directions. Optional. */ public Drawable background; /** Optional. */ public Drawable knob; public TouchpadStyle () { } public TouchpadStyle (Drawable background, Drawable knob) { this.background = background; this.knob = knob; } public TouchpadStyle (TouchpadStyle style) { this.background = style.background; this.knob = style.knob; } } }