/*******************************************************************************
* 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;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Matrix3;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.utils.Cullable;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.SnapshotArray;
/**
* 2D scene graph node that may contain other actors.
* <p>
* Actors have a z-order equal to the order they were inserted into the group. Actors inserted later will be drawn on
* top of actors added earlier. Touch events that hit more than one actor are distributed to topmost actors first.
*
* @author mzechner
* @author Nathan Sweet
*/
public class Group extends Actor implements Cullable {
protected final SnapshotArray<Actor> children = new SnapshotArray(true, 4, Actor.class);
private final Matrix3 localTransform = new Matrix3();
private final Matrix3 worldTransform = new Matrix3();
private final Matrix4 batchTransform = new Matrix4();
private final Matrix4 oldBatchTransform = new Matrix4();
public boolean transform = true;
private Rectangle cullingArea;
protected final Vector2 point = new Vector2();
public void act(float delta) {
super.act(delta);
Actor[] actors = children.begin();
for (int i = 0, n = children.size; i < n; i++)
actors[i].act(delta);
children.end();
}
/**
* Draws the group and its children. The default implementation calls {@link #applyTransform(SpriteBatch, Matrix4)}
* if needed, then {@link #drawChildren(SpriteBatch, float)}, then {@link #resetTransform(SpriteBatch)} if needed.
*/
public void draw(SpriteBatch batch, float parentAlpha) {
if (transform)
applyTransform(batch, computeTransform());
drawChildren(batch, parentAlpha);
if (transform)
resetTransform(batch);
}
/**
* Draws all children. {@link #applyTransform(SpriteBatch, Matrix4)} should be called before and
* {@link #resetTransform(SpriteBatch)} after this method if {@link #setTransform(boolean) transform} is true. If
* {@link #setTransform(boolean) transform} is false these methods don't need to be called, children positions are
* temporarily offset by the group position when drawn. This method avoids drawing children completely outside the
* {@link #setCullingArea(Rectangle) culling area}, if set.
*/
protected void drawChildren(SpriteBatch batch, float parentAlpha) {
parentAlpha *= getColor().a;
SnapshotArray<Actor> children = this.children;
Actor[] actors = children.begin();
Rectangle cullingArea = this.cullingArea;
if (cullingArea != null) {
// Draw children only if inside culling area.
float cullLeft = cullingArea.x;
float cullRight = cullLeft + cullingArea.width;
float cullBottom = cullingArea.y;
float cullTop = cullBottom + cullingArea.height;
if (transform) {
for (int i = 0, n = children.size; i < n; i++) {
Actor child = actors[i];
if (!child.isVisible())
continue;
float x = child.getX(), y = child.getY();
if (x <= cullRight && y <= cullTop && x + child.getWidth() >= cullLeft
&& y + child.getHeight() >= cullBottom)
child.draw(batch, parentAlpha);
}
batch.flush();
} else {
// No transform for this group, offset each child.
float offsetX = getX(), offsetY = getY();
setX(0);
setY(0);
for (int i = 0, n = children.size; i < n; i++) {
Actor child = actors[i];
if (!child.isVisible())
continue;
float x = child.getX(), y = child.getY();
if (x <= cullRight && y <= cullTop && x + child.getWidth() >= cullLeft
&& y + child.getHeight() >= cullBottom) {
child.setX(x + offsetX);
child.setY(y + offsetY);
child.draw(batch, parentAlpha);
child.setX(x);
child.setY(y);
}
}
setX(offsetX);
setY(offsetY);
}
} else {
// No culling, draw all children.
if (transform) {
for (int i = 0, n = children.size; i < n; i++) {
Actor child = actors[i];
if (!child.isVisible())
continue;
child.draw(batch, parentAlpha);
}
batch.flush();
} else {
// No transform for this group, offset each child.
float offsetX = getX(), offsetY = getY();
setX(0);
setY(0);
for (int i = 0, n = children.size; i < n; i++) {
Actor child = actors[i];
if (!child.isVisible())
continue;
float x = child.getX(), y = child.getY();
child.setX(x + offsetX);
child.setY(y + offsetY);
child.draw(batch, parentAlpha);
child.setX(x);
child.setY(y);
}
setX(offsetX);
setY(offsetY);
}
}
children.end();
}
/**
* Set the SpriteBatch's transformation matrix, often with the result of {@link #computeTransform()}. Note this
* causes the batch to be flushed. {@link #resetTransform(SpriteBatch)} will restore the transform to what it was
* before this call.
*/
protected void applyTransform(SpriteBatch batch, Matrix4 transform) {
oldBatchTransform.set(batch.getTransformMatrix());
batch.setTransformMatrix(transform);
}
/** Returns the transform for this group's coordinate system. */
protected Matrix4 computeTransform() {
Matrix3 temp = worldTransform;
float originX = getOriginX();
float originY = getOriginY();
float rotation = getRotation();
float scaleX = getScaleX();
float scaleY = getScaleY();
if (originX != 0 || originY != 0)
localTransform.setToTranslation(originX, originY);
else
localTransform.idt();
if (rotation != 0)
localTransform.rotate(rotation);
if (scaleX != 1 || scaleY != 1)
localTransform.scale(scaleX, scaleY);
if (originX != 0 || originY != 0)
localTransform.translate(-originX, -originY);
localTransform.trn(getX(), getY());
// Find the first parent that transforms.
Group parentGroup = getParent();
while (parentGroup != null) {
if (parentGroup.transform)
break;
parentGroup = parentGroup.getParent();
}
if (parentGroup != null) {
worldTransform.set(parentGroup.worldTransform);
worldTransform.mul(localTransform);
} else {
worldTransform.set(localTransform);
}
batchTransform.set(worldTransform);
return batchTransform;
}
/**
* Restores the SpriteBatch transform to what it was before {@link #applyTransform(SpriteBatch, Matrix4)}. Note this
* causes the batch to be flushed.
*/
protected void resetTransform(SpriteBatch batch) {
batch.setTransformMatrix(oldBatchTransform);
}
/**
* Children completely outside of this rectangle will not be drawn. This is only valid for use with unrotated and
* unscaled actors!
*/
public void setCullingArea(Rectangle cullingArea) {
this.cullingArea = cullingArea;
}
public Actor hit(float x, float y, boolean touchable) {
if (touchable && getTouchable() == Touchable.disabled)
return null;
Array<Actor> children = this.children;
for (int i = children.size - 1; i >= 0; i--) {
Actor child = children.get(i);
if (!child.isVisible())
continue;
child.parentToLocalCoordinates(point.set(x, y));
Actor hit = child.hit(point.x, point.y, touchable);
if (hit != null)
return hit;
}
return super.hit(x, y, touchable);
}
/** Called when actors are added to or removed from the group. */
protected void childrenChanged() {
}
/**
* Adds an actor as a child of this group. The actor is first removed from its parent group, if any.
*
* @see #remove()
*/
public void addActor(Actor actor) {
actor.remove();
children.add(actor);
actor.setParent(this);
actor.setStage(getStage());
childrenChanged();
}
/**
* Adds an actor as a child of this group, at a specific index. The actor is first removed from its parent group, if
* any.
*
* @param index
* May be greater than the number of children.
*/
public void addActorAt(int index, Actor actor) {
actor.remove();
if (index >= children.size)
children.add(actor);
else
children.insert(index, actor);
actor.setParent(this);
actor.setStage(getStage());
childrenChanged();
}
/**
* Adds an actor as a child of this group, immediately before another child actor. The actor is first removed from
* its parent group, if any.
*/
public void addActorBefore(Actor actorBefore, Actor actor) {
actor.remove();
int index = children.indexOf(actorBefore, true);
children.insert(index, actor);
actor.setParent(this);
actor.setStage(getStage());
childrenChanged();
}
/**
* Adds an actor as a child of this group, immediately after another child actor. The actor is first removed from
* its parent group, if any.
*/
public void addActorAfter(Actor actorAfter, Actor actor) {
actor.remove();
int index = children.indexOf(actorAfter, true);
if (index == children.size)
children.add(actor);
else
children.insert(index + 1, actor);
actor.setParent(this);
actor.setStage(getStage());
childrenChanged();
}
/**
* Removes an actor from this group. If the actor will not be used again and has actions, they should be
* {@link Actor#clearActions() cleared} so the actions will be returned to their
* {@link Action#setPool(com.badlogic.gdx.utils.Pool) pool}, if any. This is not done automatically.
*/
public boolean removeActor(Actor actor) {
if (!children.removeValue(actor, true))
return false;
Stage stage = getStage();
if (stage != null)
stage.unfocus(actor);
actor.setParent(null);
actor.setStage(null);
childrenChanged();
return true;
}
/** Removes all actors from this group. */
public void clear() {
Actor[] actors = children.begin();
for (int i = 0, n = children.size; i < n; i++) {
Actor child = actors[i];
child.setStage(null);
child.setParent(null);
}
children.end();
children.clear();
childrenChanged();
}
/**
* Returns the first actor found with the specified name. Note this recursively compares the name of every actor in
* the group.
*/
public Actor findActor(String name) {
Array<Actor> children = this.children;
for (int i = 0, n = children.size; i < n; i++)
if (name.equals(children.get(i).getName()))
return children.get(i);
for (int i = 0, n = children.size; i < n; i++) {
Actor child = children.get(i);
if (child instanceof Group) {
Actor actor = ((Group) child).findActor(name);
if (actor != null)
return actor;
}
}
return null;
}
protected void setStage(Stage stage) {
super.setStage(stage);
Array<Actor> children = this.children;
for (int i = 0, n = children.size; i < n; i++)
children.get(i).setStage(stage);
}
/** Swaps two actors by index. Returns false if the swap did not occur because the indexes were out of bounds. */
public boolean swapActor(int first, int second) {
int maxIndex = children.size;
if (first < 0 || first >= maxIndex)
return false;
if (second < 0 || second >= maxIndex)
return false;
children.swap(first, second);
return true;
}
/** Swaps two actors. Returns false if the swap did not occur because the actors are not children of this group. */
public boolean swapActor(Actor first, Actor second) {
int firstIndex = children.indexOf(first, true);
int secondIndex = children.indexOf(second, true);
if (firstIndex == -1 || secondIndex == -1)
return false;
children.swap(firstIndex, secondIndex);
return true;
}
/** Returns an ordered list of child actors in this group. */
public SnapshotArray<Actor> getChildren() {
return children;
}
/**
* When true (the default), the SpriteBatch is transformed so children are drawn in their parent's coordinate
* system. This has a performance impact because {@link SpriteBatch#flush()} must be done before and after the
* transform. If the actors in a group are not rotated or scaled, then the transform for the group can be set to
* false. In this case, each child's position will be offset by the group's position for drawing, causing the
* children to appear in the correct location even though the SpriteBatch has not been transformed.
*/
public void setTransform(boolean transform) {
this.transform = transform;
}
public boolean isTransform() {
return transform;
}
/**
* Converts coordinates for this group to those of a descendant actor. The descendant does not need to be a direct
* child.
*
* @throws IllegalArgumentException
* if the specified actor is not a descendant of this group.
*/
public Vector2 localToDescendantCoordinates(Actor descendant, Vector2 localCoords) {
Group parent = descendant.getParent();
if (parent == null)
throw new IllegalArgumentException("Child is not a descendant: " + descendant);
// First convert to the actor's parent coordinates.
if (parent != this)
localToDescendantCoordinates(parent, localCoords);
// Then from each parent down to the descendant.
descendant.parentToLocalCoordinates(localCoords);
return localCoords;
}
}