net.dermetfan.gdx.scenes.scene2d.ui.ScrollPaneSnapAction.java Source code

Java tutorial

Introduction

Here is the source code for net.dermetfan.gdx.scenes.scene2d.ui.ScrollPaneSnapAction.java

Source

/** Copyright 2015 Robin Stumm (serverkorken@gmail.com, 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.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Event;
import com.badlogic.gdx.scenes.scene2d.EventListener;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Value;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Pools;
import net.dermetfan.gdx.scenes.scene2d.ui.ScrollPaneSnapAction.SnapEvent.Type;
import net.dermetfan.utils.math.MathUtils;

/** Lets a {@link ScrollPane} snap to certain {@link #slots slots}.
 *  Does nothing when added to other Actors. Stays until it is manually removed.
 *  @author dermetfan
 *  @since 0.10.0 */
public class ScrollPaneSnapAction extends Action {

    /** the target position all slots shall snap into */
    private Value targetX, targetY;

    /** the Indicator to notify (may be null) */
    private Indicator indicator;

    /** whether the y-axis should be indicated to the {@link #indicator} rather than the x-axis */
    private boolean indicateVertical;

    /** the slots to snap to
     *  @see #findClosestSlot(Vector2) */
    private final FloatArray slots = new FloatArray();

    /** the event used to search for slots
     *  @see #updateSlots() */
    private final SlotSearchEvent searchEvent = new SlotSearchEvent() {
        {
            this.setBubbles(false);
        }
    };

    /** the {@link #target} cast to a ScrollPane */
    private ScrollPane pane;

    /** whether a SnapEvent of the type Type.Out has already been fired */
    private boolean snapOutFired = true;

    /** whether snapping out was cancelled, needed to not snap after failing to snap out */
    private boolean snapOutCancelled;

    /** whether the ScrollPane is currently snapped */
    private boolean snapped;

    /** the last {@link ScrollPane#getVisualScrollX() visual scroll amount} of {@link #pane}, needed to know whether the {@link #indicator} must be notified */
    private float visualScrollX = Float.NaN, visualScrollY = Float.NaN;

    /** uses {@link Value#percentWidth(float) percentWidth(.5f)} and {@link Value#percentHeight(float) percentHeight(.5f)} as target */
    public ScrollPaneSnapAction() {
        this(Value.percentWidth(.5f), Value.percentHeight(.5f));
    }

    /** @param targetX the {@link #targetX}
     *  @param targetY the {@link #targetY} */
    public ScrollPaneSnapAction(Value targetX, Value targetY) {
        this.targetX = targetX;
        this.targetY = targetY;
    }

    @Override
    public boolean act(float delta) {
        if (pane == null)
            return false;

        boolean cancelSnapping = false;
        if (pane.isDragging() || pane.isPanning()) {
            snapped = false;
            if (!snapOutFired) {
                SnapEvent event = Pools.obtain(SnapEvent.class);
                event.init(this, Type.Out, getSnappedSlotX(), getSnappedSlotY());
                if (snapOutCancelled = pane.fire(event)) {
                    snap0(getSnappedSlotX(), getSnappedSlotY());
                    pane.cancel();
                }
                Pools.free(event);
                snapOutFired = true;
            }
            cancelSnapping = true;
        }
        if (!cancelSnapping && (snapped |= snapOutCancelled)) {
            snapOutCancelled = false;
            snapOutFired = false;
            cancelSnapping = true;
        }
        boolean slotsUpdated = false;
        if (!cancelSnapping) {
            updateSlots();
            slotsUpdated = true;
            snapClosest();
        }

        if (indicator != null
                && (pane.getVisualScrollX() != visualScrollX || pane.getVisualScrollY() != visualScrollY)) {
            visualScrollX = pane.getVisualScrollX();
            visualScrollY = pane.getVisualScrollY();
            if (!slotsUpdated)
                updateSlots();
            float currentSlot = indicateVertical ? getSnappedSlotY() : getSnappedSlotX();
            int page = 0;
            float closestSmaller = Float.NEGATIVE_INFINITY, closestGreater = Float.POSITIVE_INFINITY;
            for (int i = indicateVertical ? 1 : 0; i < slots.size; i += 2) {
                float slot = slots.get(i), diff = currentSlot - slot;
                if (diff >= 0) {
                    if (diff <= currentSlot - closestSmaller)
                        closestSmaller = slot;
                } else if (diff >= currentSlot - closestGreater)
                    closestGreater = slot;
                if (slot <= currentSlot)
                    page++;
            }
            indicator.indicate(this, page, slots.size / 2,
                    MathUtils.replaceNaN((currentSlot - closestSmaller) / (closestGreater - closestSmaller), 1));
        }

        return false;
    }

    @Override
    public void setTarget(Actor target) {
        super.setTarget(target);
        if (target instanceof ScrollPane) {
            pane = (ScrollPane) target;
            dirtyIndicator();
        } else if (target == null) {
            pane = null;
            if (indicator != null)
                indicator.indicate(this, 0, 0, 0);
        }
    }

    @Override
    public void reset() {
        super.reset();
        targetX = Value.percentWidth(.5f);
        targetY = Value.percentHeight(.5f);
        indicator = null;
        indicateVertical = false;
        slots.clear();
        searchEvent.reset();
        pane = null;
        snapOutFired = true;
        snapOutCancelled = false;
        snapped = false;
        visualScrollX = Float.NaN;
        visualScrollY = Float.NaN;
    }

    /** clears and fills {@link #slots} with slots on the given ScrollPane */
    public void updateSlots() {
        slots.clear();
        findSlots(pane.getWidget());
    }

    /** @param root the Actor from which to start searching downward recursively */
    private void findSlots(Actor root) {
        searchEvent.setTarget(pane.getWidget());
        root.notify(searchEvent, false);
        if (root instanceof Group)
            for (Actor child : ((Group) root).getChildren())
                findSlots(child);
    }

    /** @param x the x coordinate of the Slot in the {@link ScrollPane#getWidget() widget}'s coordinates
     *  @param y the y coordinate of the Slot in the {@link ScrollPane#getWidget() widget}'s coordinates */
    public void reportSlot(float x, float y) {
        slots.ensureCapacity(2);
        slots.add(x);
        slots.add(y);
        dirtyIndicator();
    }

    /** @param slot is set to the Slot closest to the target with the visual scroll amount
     *  @return true if a slot was found */
    public boolean findClosestSlot(Vector2 slot) {
        float targetX = this.targetX.get(pane) + pane.getVisualScrollX(),
                targetY = this.targetY.get(pane) + pane.getVisualScrollY();
        float closestDistance = Float.POSITIVE_INFINITY;
        boolean found = false;
        for (int i = 1; i < slots.size; i += 2) {
            float slotX = slots.get(i - 1), slotY = slots.get(i);
            float distance = Vector2.dst2(targetX, targetY, slotX, slotY);
            if (distance <= closestDistance) {
                closestDistance = distance;
                slot.set(slotX, slotY);
                found = true;
            }
        }
        return found;
    }

    /** {@link #snap(float, float) snaps} to the {@link #findClosestSlot(Vector2) closest} slot */
    private void snapClosest() {
        Vector2 vec2 = Pools.obtain(Vector2.class);
        if (findClosestSlot(vec2))
            snap(vec2.x, vec2.y);
        else
            snap(getSnappedSlotX(), getSnappedSlotY());
        Pools.free(vec2);
    }

    /** @param slotX the x coordinate of the slot to snap to
     *  @param slotY the y coordinate of the slot to snap to */
    public void snap(float slotX, float slotY) {
        SnapEvent event = Pools.obtain(SnapEvent.class);
        if (!snapOutFired) {
            event.init(this, Type.Out, getSnappedSlotX(), getSnappedSlotY());
            snapOutCancelled = pane.fire(event);
            snapOutFired = true;
            if (snapOutCancelled) {
                Pools.free(event);
                return;
            }
        }
        event.init(this, Type.In, slotX, slotY);
        if (!pane.fire(event)) {
            snap0(slotX, slotY);
            snapped = true;
            snapOutFired = false;
        }
        Pools.free(event);
    }

    private void snap0(float slotX, float slotY) {
        pane.fling(0, 0, 0);
        pane.setScrollX(slotX - targetX.get(pane));
        pane.setScrollY(slotY - targetY.get(pane));
    }

    /** forces the {@link #indicator} to be notified next time {@link #act(float) act} is called */
    public void dirtyIndicator() {
        visualScrollX = visualScrollY = Float.NaN;
    }

    /** @return the slot x the given pane is currently snapped to (assuming it is) */
    public float getSnappedSlotX() {
        return pane.getScrollX() + targetX.get(pane);
    }

    /** @return the slot y the given pane is currently snapped to (assuming it is) */
    public float getSnappedSlotY() {
        return pane.getScrollY() + targetY.get(pane);
    }

    // getters and setters

    /** @return the {@link #targetX} */
    public Value getTargetX() {
        return targetX;
    }

    /** @param targetX the {@link #targetX} to set */
    public void setTargetX(Value targetX) {
        this.targetX = targetX;
    }

    /** @return the {@link #targetY} */
    public Value getTargetY() {
        return targetY;
    }

    /** @param targetY the {@link #targetY} to set */
    public void setTargetY(Value targetY) {
        this.targetY = targetY;
    }

    /** @param targetX the {@link #targetX} to set
     *  @param targetY the {@link #targetY} to set */
    public void setTarget(Value targetX, Value targetY) {
        this.targetX = targetX;
        this.targetY = targetY;
    }

    /** @return the {@link #indicator} */
    public Indicator getIndicator() {
        return indicator;
    }

    /** @param indicator the {@link #indicator} to set */
    public void setIndicator(Indicator indicator) {
        this.indicator = indicator;
    }

    /** @return the {@link #indicateVertical} */
    public boolean isIndicateVertical() {
        return indicateVertical;
    }

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

    /** @return the {@link #slots} */
    public FloatArray getSlots() {
        return slots;
    }

    /** @return the {@link #snapped} */
    public boolean isSnapped() {
        return snapped;
    }

    /** Fired when the ScrollPane snaps into or out of a slot.
     *  Cancelling this event will cause the ScrollPane to not snap into/out of the slot.
     *  @author dermetfan
     *  @since 0.10.0 */
    public static class SnapEvent extends Event {

        /** the ScrollPaneSnapAction that fired this event */
        private ScrollPaneSnapAction action;

        /** the Type of this SnapEvent */
        private Type type;

        /** the slot position */
        private float slotX, slotY;

        private void init(ScrollPaneSnapAction action, Type type, float slotX, float slotY) {
            this.action = action;
            this.type = type;
            this.slotX = slotX;
            this.slotY = slotY;
        }

        @Override
        public void reset() {
            super.reset();
            action = null;
            type = null;
            slotX = 0;
            slotY = 0;
        }

        /** @return the ScrollPane that snapped */
        public ScrollPane getScrollPane() {
            assert getListenerActor() instanceof ScrollPane;
            return (ScrollPane) getListenerActor();
        }

        // getters and setters

        /** @return the {@link #action} */
        public ScrollPaneSnapAction getAction() {
            return action;
        }

        /** @return the {@link #type} */
        public Type getType() {
            return type;
        }

        /** @return the {@link #slotX} */
        public float getSlotX() {
            return slotX;
        }

        /** @return the {@link #slotY} */
        public float getSlotY() {
            return slotY;
        }

        /** whether the slot was snapped into or out of
         *  @author dermetfan
         *  @since 0.10.0 */
        public enum Type {

            /** the slot was snapped into */
            In,

            /** the slot was snapped out of */
            Out

        }

    }

    /** @author dermetfan
     *  @since 0.10.0 */
    private class SlotSearchEvent extends Event {

        /** @return the enclosing ScrollPaneSnapAction instance */
        public ScrollPaneSnapAction getAction() {
            return ScrollPaneSnapAction.this;
        }

        /** @return the ScrollPane (parent of the {@link #getListenerActor() listener actor}) */
        public ScrollPane getScrollPane() {
            assert getTarget()
                    .getParent() instanceof ScrollPane : "SlotSearchEvent#getTarget() must be ScrollPane#getWidget()";
            return (ScrollPane) getTarget().getParent();
        }

        /** converts the given coordinates and calls {@link ScrollPaneSnapAction#reportSlot(float, float)}
         *  @param x the x coordinate of the Slot in the {@link #getListenerActor() listener actor}'s coordinates
         *  @param y the y coordinate of the Slot in the {@link #getListenerActor() listener actor}'s coordinates */
        public void reportSlot(float x, float y) {
            Vector2 vec2 = Pools.obtain(Vector2.class);
            getListenerActor().localToAscendantCoordinates(getScrollPane().getWidget(), vec2.set(x, y));
            getAction().reportSlot(vec2.x, vec2.y);
            Pools.free(vec2);
        }

    }

    /** convenience class, calls {@link ScrollPaneSnapAction#reportSlot(float, float)} on SnapEvents
     *  @author dermetfan
     *  @since 0.10.0 */
    public static abstract class Slot implements EventListener {

        /** calls {@link ScrollPaneSnapAction#reportSlot(float, float) reportSlot} if the event is a {@link SlotSearchEvent} */
        @Override
        public boolean handle(Event e) {
            if (e instanceof SlotSearchEvent) {
                SlotSearchEvent event = (SlotSearchEvent) e;
                Vector2 vec2 = Pools.obtain(Vector2.class);
                getSlot(event.getListenerActor(), vec2);
                event.reportSlot(vec2.x, vec2.y);
                Pools.free(vec2);
            }
            return false;
        }

        /** @param actor the Actor which slot to get
         *  @param slot the Vector2 to store the slot position in */
        public abstract void getSlot(Actor actor, Vector2 slot);

    }

    /** a Slot determined by an {@link Align}
     *  @author dermetfan
     *  @since 0.10.0 */
    public static class AlignSlot extends Slot {

        /** the {@link Align} flag */
        private int align;

        /** @param align the {@link #align} to set */
        public AlignSlot(int align) {
            this.align = align;
        }

        @Override
        public void getSlot(Actor actor, Vector2 slot) {
            slot.set(actor.getX(align) - actor.getX(), actor.getY(align) - actor.getY());
        }

        // getters and setters

        /** @return the {@link #align} */
        public int getAlign() {
            return align;
        }

        /** @param align the {@link #align} to set */
        public void setAlign(int align) {
            this.align = align;
        }

    }

    /** a Slot determined by {@link Value Values}
     *  @author dermetfan
     *  @since 0.10.0 */
    public static class ValueSlot extends Slot {

        /** the Value determining this slot */
        private Value valueX, valueY;

        /** @param valueX the {@link #valueX} to set
         *  @param valueY the {@link #valueY} to set */
        public ValueSlot(Value valueX, Value valueY) {
            this.valueX = valueX;
            this.valueY = valueY;
        }

        @Override
        public void getSlot(Actor actor, Vector2 slot) {
            slot.set(valueX.get(actor), valueY.get(actor));
        }

        // getters and setters

        /** @return the {@link #valueX} */
        public Value getValueX() {
            return valueX;
        }

        /** @param valueX the {@link #valueX} to set */
        public void setValueX(Value valueX) {
            this.valueX = valueX;
        }

        /** @return the {@link #valueY} */
        public Value getValueY() {
            return valueY;
        }

        /** @param valueY the {@link #valueY} to set */
        public void setValueY(Value valueY) {
            this.valueY = valueY;
        }

    }

    /** indicates the position of the {@link #getSnappedSlotX() snapped} slot
     *  @author dermetfan
     *  @since 0.10.0 */
    public interface Indicator {

        /** called by {@link #act(float) if the {@link ScrollPane#getVisualScrollX() visual scroll amount} changed
         *  @param action the instance calling this method
         *  @param page the current slot index in a sorted sequence of all slots
         *  @param pages the number of slots
         *  @param progress how far the ScrollPane's visual scroll amount is to the next slot */
        void indicate(ScrollPaneSnapAction action, int page, int pages, float progress);

    }

}