All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.dermetfan.gdx.scenes.scene2d.ui.CircularGroup Maven / Gradle / Ivy

/** Copyright 2014 Robin Stumm ([email protected], http://dermetfan.net)
 *
 *  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 net.dermetfan.gdx.scenes.scene2d.ui;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.MathUtils;
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.ui.WidgetGroup;
import com.badlogic.gdx.scenes.scene2d.utils.DragListener;
import com.badlogic.gdx.scenes.scene2d.utils.Layout;
import com.badlogic.gdx.utils.SnapshotArray;
import net.dermetfan.gdx.math.GeometryUtils;

import static net.dermetfan.utils.math.MathUtils.replaceNaN;

/** a group that aligns its children in a circle
 *  @since 0.5.0
 *  @author dermetfan */
public class CircularGroup extends WidgetGroup {

	/** @since 0.5.0
	 *  @author dermetfan
	 *  @see #modifier */
	public static interface Modifier {

		/** @param angle the linearly calculated angle
		 *  @param index the index of the child which angle to calculate
		 *  @param numChildren the number of children
		 *  @param group the CircularGroup the child in question belongs to
		 *  @return the angle of the child at the given index ({@link #angleOffset} will be added to this) */
		float angle(float angle, int index, int numChildren, CircularGroup group);

		/** @param rotation the angle of the child (may be influenced by {@link #angle(float, int, int, CircularGroup)} which by default also is its rotation
		 *  @param index the index of the child which rotation to calculate
		 *  @param numChildren the number of children
		 *  @param group the CircularGroup the child in question belongs to
		 *  @return the rotation of the child at the given index */
		float rotation(float rotation, int index, int numChildren, CircularGroup group);

		/** Use this if you only want to override some of {@link Modifier}'s methods.
		 *  All implementations return the default value.
		 *  @since 0.5.0
		 *  @author dermetfan */
		public static abstract class Adapter implements Modifier {

			/** @return the given angle */
			@Override
			public float angle(float angle, int index, int numChildren, CircularGroup group) {
				return angle;
			}

			/** @return the given rotation */
			@Override
			public float rotation(float rotation, int index, int numChildren, CircularGroup group) {
				return rotation;
			}

		}

	}

	/** manages dragging and velocity of its enclosing CircularGroup instance
	 *  @since 0.5.0
	 *  @author dermetfan */
	public class DragManager extends DragListener {

		/** if the velocity should be applied */
		private boolean velocityActivated = true;

		/** if dragging should be possible */
		private boolean draggingActivated = true;

		/** the velocity of the rotation */
		private float velocity;

		/** the deceleration applied to {@link #velocity} */
		private float deceleration = 500;

		/** if this group is currently being dragged (internal use) */
		private boolean dragging;

		/** the previous angle for delta calculation (internal use) */
		private float previousAngle;

		/** The greatest absolute delta value allowed. Needed to avoid glitches. */
		private float maxAbsDelta = 350;

		/** inner class singleton */
		private DragManager() {}

		@Override
		public void dragStart(InputEvent event, float x, float y, int pointer) {
			if(!draggingActivated)
				return;
			velocity = 0;
			dragging = true;
			previousAngle = angle(x, y);
		}

		@Override
		public void drag(InputEvent event, float x, float y, int pointer) {
			if(!draggingActivated)
				return;
			float currentAngle = angle(x, y);
			float delta = currentAngle - previousAngle;
			previousAngle = currentAngle;
			if(Math.abs(delta) > maxAbsDelta)
				return;
			velocity = delta * Gdx.graphics.getFramesPerSecond();
			float newAngleOffset = angleOffset + delta;
			float oldAngleOffset = angleOffset;
			setAngleOffset(newAngleOffset);
			if(angleOffset != oldAngleOffset)
				invalidate();
		}

		@Override
		public void dragStop(InputEvent event, float x, float y, int pointer) {
			if(!draggingActivated)
				return;
			dragging = false;
		}

		/** changes {@link #angleOffset} according to {@link #velocity} and reduces {@link #velocity} according to {@link #deceleration}
		 *  @param delta see {@link com.badlogic.gdx.Graphics#getDeltaTime()} */
		public void act(float delta) {
			if(dragging || velocity == 0 || !velocityActivated)
				return;
			setAngleOffset(angleOffset + velocity * delta);
			invalidate();
			if(deceleration == 0)
				return;
			float oldVelocity = velocity;
			velocity -= (velocity > 0 ? deceleration : -deceleration) * delta;
			if(oldVelocity > 0 && velocity < 0 || oldVelocity < 0 && velocity > 0)
				velocity = 0;
		}

		/** @return the angle of the given x and y to the center of the group */
		private float angle(float x, float y) {
			return tmp.set(x, y).sub(getWidth() / 2, getHeight() / 2).angle();
		}

		/** @param angleOffset the {@link #angleOffset} to set so that if {@link #minAngleOffset} and {@link #maxAngleOffset} coincide on 360 degrees it doesn't get clamped */
		private void setAngleOffset(float angleOffset) {
			if(maxAngleOffset - minAngleOffset == 360)
				CircularGroup.this.angleOffset = net.dermetfan.utils.math.MathUtils.normalize(angleOffset, minAngleOffset, maxAngleOffset);
			else
				CircularGroup.this.setAngleOffset(angleOffset);
		}

		// getters and setters

		/** @return the {@link #velocityActivated} */
		public boolean isVelocityActivated() {
			return velocityActivated;
		}

		/** @param velocityActivated the {@link #velocityActivated} to set */
		public void setVelocityActivated(boolean velocityActivated) {
			this.velocityActivated = velocityActivated;
		}

		/** @return the {@link #draggingActivated} */
		public boolean isDraggingActivated() {
			return draggingActivated;
		}

		/** @param draggingActivated the {@link #draggingActivated} to set */
		public void setDraggingActivated(boolean draggingActivated) {
			this.draggingActivated = draggingActivated;
		}

		/** @return the {@link #velocity} */
		public float getVelocity() {
			return velocity;
		}

		/** @param velocity the {@link #velocity} to set */
		public void setVelocity(float velocity) {
			this.velocity = velocity;
		}

		/** @return the {@link #deceleration} */
		public float getDeceleration() {
			return deceleration;
		}

		/** @param deceleration the {@link #deceleration} to set */
		public void setDeceleration(float deceleration) {
			this.deceleration = deceleration;
		}

		/** @return the {@link #maxAbsDelta} */
		public float getMaxAbsDelta() {
			return maxAbsDelta;
		}

		/** @param maxAbsDelta the {@link #maxAbsDelta} to set */
		public void setMaxAbsDelta(float maxAbsDelta) {
			this.maxAbsDelta = maxAbsDelta;
		}

	}

	/** The preferred size. Default is 500, some arbitrary value. */
	float prefWidth = 500, prefHeight = 500;

	/** The max angle of all children (in degrees). Default is 360. */
	private float fullAngle = 360;

	/** The angle added to each child's angle (in degrees). Default is 0. */
	private float angleOffset;

	/** The smallest {@link #angleOffset} allowed. Default is 0. */
	private float minAngleOffset;

	/** The greatest {@link #angleOffset} allowed. Default is {@link #fullAngle}. */
	private float maxAngleOffset = fullAngle;

	/** If an additional, not existent child should be considered in the angle calculation for each child.
* Since {@link #fullAngle} describes the min and max angle for children of this group, two children will overlap at 360 degrees (because 360 degrees mean the min and max angle coincide). * In this case it would make sense to enable the virtual child. It will reserve the angle needed for one child and therefore overlap with another child at the min/max angle instead of two actual children overlapping.
* Default is true, as appropriate for the default of {@link #fullAngle}. */ private boolean virtualChild = true; /** allows advanced modification of each child's angle */ private Modifier modifier; /** Even if distanceFromCenter is greater, no child's unrotated bound will escape the group size. NaN values mean no fixed distance from the group center. Default is {@link Float#NaN}. */ private float distanceFromCenter = Float.NaN; /** the DragManager used to make this group rotatable by dragging and to apply velocity */ private final DragManager dragManager = new DragManager(); /** for internal use */ private final Vector2 tmp = new Vector2(), tmp2 = new Vector2(); /** @see WidgetGroup#WidgetGroup() */ public CircularGroup() {} /** @param draggable see {@link #setDraggable(boolean)} */ public CircularGroup(boolean draggable) { setDraggable(draggable); } @Override public void act(float delta) { dragManager.act(delta); super.act(delta); } @Override public void layout() { SnapshotArray children = getChildren(); for(int index = 0; index < children.size; index++) { Actor child = children.get(index); int numChildren = children.size - (virtualChild ? 0 : 1); float angle = fullAngle / numChildren * index; if(modifier != null) angle = modifier.angle(angle, index, numChildren, this); angle += angleOffset; child.setRotation(modifier != null ? modifier.rotation(angle, index, numChildren, this) : angle); float groupWidth = getWidth(), groupHeight = getHeight(); float width, height; if(child instanceof Layout) { Layout childLayout = (Layout) child; width = Math.min(childLayout.getPrefWidth(), groupWidth / 2 - replaceNaN(MathUtils.clamp(distanceFromCenter, 0, groupWidth / 2), 0)); width = Math.max(width, childLayout.getMinWidth()); if(childLayout.getMaxWidth() != 0) width = Math.min(width, childLayout.getMaxWidth()); height = Math.min(childLayout.getPrefHeight(), groupHeight / 2 - replaceNaN(MathUtils.clamp(distanceFromCenter, 0, groupHeight / 2), 0)); height = Math.max(height, childLayout.getMinHeight()); if(childLayout.getMaxHeight() != 0) height = Math.min(height, childLayout.getMaxHeight()); child.setSize(width, height); } else { width = child.getWidth(); height = child.getHeight(); } child.setOrigin(width / 2, height / 2); float realDistanceFromCenter = Float.isNaN(distanceFromCenter) ? groupWidth / 2 - width : Math.min(distanceFromCenter, groupWidth / 2 - width); GeometryUtils.rotate(tmp.set(groupWidth / 2 - width / 2 - realDistanceFromCenter, groupHeight / 2), tmp2.set(groupWidth / 2, groupHeight / 2), angle * MathUtils.degRad); child.setPosition(tmp.x - child.getWidth() / 2, tmp.y - child.getHeight() / 2); } } @Override public void drawDebug(ShapeRenderer shapes) { shapes.setColor(Color.CYAN); for(Actor child : getChildren()) shapes.line(getX() + getWidth() / 2 * getScaleX(), getY() + getHeight() / 2 * getScaleY(), getX() + (child.getX() + child.getWidth() / 2) * getScaleX(), getY() + (child.getY() + child.getHeight() / 2) * getScaleY()); super.drawDebug(shapes); } @Override public float getPrefWidth() { return prefWidth; } @Override public float getPrefHeight() { return prefHeight; } /** does not take rotation into account */ @Override public float getMinWidth() { SnapshotArray children = getChildren(); if(children.size == 0) return 0; float minWidth = Float.POSITIVE_INFINITY; Actor currentSmallest = null; for(Actor child : children) { float childMinWidth = child instanceof Layout ? ((Layout) child).getMinWidth() : child.getWidth(); if(childMinWidth < minWidth) { minWidth = childMinWidth; currentSmallest = child; } } float realDistanceFromCenter = replaceNaN(distanceFromCenter, 0); if(children.size == 1) return minWidth * 2 + realDistanceFromCenter * 2; float secondMinWidth = Float.POSITIVE_INFINITY; for(Actor child : children) { if(child == currentSmallest) continue; secondMinWidth = Math.min(secondMinWidth, child instanceof Layout ? ((Layout) child).getMinWidth() : child.getWidth()); } return minWidth * 2 + secondMinWidth * 2 + realDistanceFromCenter * 2; } /** does not take rotation into account */ @Override public float getMinHeight() { SnapshotArray children = getChildren(); if(children.size == 0) return 0; float minHeight = Float.POSITIVE_INFINITY; Actor currentSmallest = null; for(Actor child : children) { float childMinHeight = child instanceof Layout ? ((Layout) child).getMinHeight() : child.getHeight(); if(childMinHeight < minHeight) { minHeight = childMinHeight; currentSmallest = child; } } float realDistanceFromCenter = replaceNaN(distanceFromCenter, 0); if(children.size == 1) return minHeight * 2 + realDistanceFromCenter * 2; float secondMinHeight = Float.POSITIVE_INFINITY; for(Actor child : children) { if(child == currentSmallest) continue; secondMinHeight = Math.min(secondMinHeight, child instanceof Layout ? ((Layout) child).getMinHeight() : child.getHeight()); } return minHeight * 2 + secondMinHeight * 2 + realDistanceFromCenter * 2; } /** @param draggable if this group should be rotatable by dragging with the pointer */ public void setDraggable(boolean draggable) { dragManager.setDraggingActivated(draggable); // add/remove dragManager for performance if(draggable) addListener(dragManager); else removeListener(dragManager); } /** @return if this group is rotatable by dragging with the pointer */ public boolean isDraggable() { return dragManager.isDraggingActivated(); } // getters and setters /** {@link #setFullAngle(float, boolean)} with automatic estimation if a {@link #virtualChild} would make sense. * @param fullAngle the {@link #fullAngle} to set * @see #setFullAngle(float, boolean) */ public void setFullAngle(float fullAngle) { setFullAngle(fullAngle, fullAngle >= 360); } /** @param fullAngle the {@link #fullAngle} to set * @param virtualChild the {@link #virtualChild} to set */ public void setFullAngle(float fullAngle, boolean virtualChild) { this.fullAngle = fullAngle; this.virtualChild = virtualChild; invalidate(); } /** @return the {@link #fullAngle} */ public float getFullAngle() { return fullAngle; } /** @param angleOffset The {@link #angleOffset} to set. Will be clamped to {@link #minAngleOffset} and {@link #maxAngleOffset}. */ public void setAngleOffset(float angleOffset) { this.angleOffset = MathUtils.clamp(angleOffset, minAngleOffset, maxAngleOffset); invalidate(); } /** @param angleOffset The {@link #angleOffset} to set. Will be clamped to the given min and max. * @param minAngleOffset the {@link #minAngleOffset} to set * @param maxAngleOffset the {@link #maxAngleOffset} to set */ public void setAngleOffset(float angleOffset, float minAngleOffset, float maxAngleOffset) { this.angleOffset = angleOffset; setMinAngleOffset(minAngleOffset); setMaxAngleOffset(maxAngleOffset); } /** @return the {@link #angleOffset} */ public float getAngleOffset() { return angleOffset; } /** @param amount the amount by which to translate {@link #minAngleOffset} and {@link #maxAngleOffset} */ public void translateAngleOffsetLimits(float amount) { setMinAngleOffset(minAngleOffset + amount); setMaxAngleOffset(maxAngleOffset + amount); } /** clamps {@link #angleOffset} to the new bounds * @param minAngleOffset the {@link #minAngleOffset} to set */ public void setMinAngleOffset(float minAngleOffset) { if(minAngleOffset > maxAngleOffset) throw new IllegalArgumentException("minAngleOffset must not be > maxAngleOffset"); this.minAngleOffset = minAngleOffset; this.angleOffset = Math.max(minAngleOffset, angleOffset); } /** @return the {@link #minAngleOffset} */ public float getMinAngleOffset() { return minAngleOffset; } /** clamps {@link #angleOffset} to the new bounds * @param maxAngleOffset the {@link #maxAngleOffset} to set */ public void setMaxAngleOffset(float maxAngleOffset) { if(maxAngleOffset < minAngleOffset) throw new IllegalArgumentException("maxAngleOffset must not be < minAngleOffset"); this.maxAngleOffset = maxAngleOffset; this.angleOffset = Math.min(angleOffset, maxAngleOffset); } /** @return the {@link #maxAngleOffset} */ public float getMaxAngleOffset() { return maxAngleOffset; } /** @return the {@link #virtualChild} */ public boolean isVirtualChild() { return virtualChild; } /** @param virtualChild the {@link #virtualChild} to set */ public void setVirtualChild(boolean virtualChild) { this.virtualChild = virtualChild; } /** @param prefWidth the {@link #prefWidth} to set */ public void setPrefWidth(float prefWidth) { this.prefWidth = prefWidth; invalidateHierarchy(); } /** @param prefHeight the {@link #prefHeight} to set */ public void setPrefHeight(float prefHeight) { this.prefHeight = prefHeight; invalidateHierarchy(); } /** @return the {@link #modifier} */ public Modifier getModifier() { return modifier; } /** @param modifier the {@link #modifier} to set */ public void setModifier(Modifier modifier) { this.modifier = modifier; } /** @return the {@link #distanceFromCenter} */ public float getDistanceFromCenter() { return distanceFromCenter; } /** @param distanceFromCenter the {@link #distanceFromCenter} to set */ public void setDistanceFromCenter(float distanceFromCenter) { this.distanceFromCenter = distanceFromCenter; invalidate(); } /** @return the {@link #dragManager} */ public DragManager getDragManager() { return dragManager; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy