/* * Copyright 2008-2009 Adam Tacy <adam.tacy AT gmail.com> * * 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. */ /* * Copyright 2011 Vancouver Ywebb Consulting Ltd * * 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 org.adamtacy.client.ui.effects; import java.util.Iterator; import java.util.Vector; import org.adamtacy.client.ui.NEffectPanel; import org.adamtacy.client.ui.effects.events.EffectCompletedEvent; import org.adamtacy.client.ui.effects.events.EffectCompletedHandler; import org.adamtacy.client.ui.effects.events.EffectInterruptedEvent; import org.adamtacy.client.ui.effects.events.EffectInterruptedHandler; import org.adamtacy.client.ui.effects.events.EffectPausedEvent; import org.adamtacy.client.ui.effects.events.EffectPausedHandler; import org.adamtacy.client.ui.effects.events.EffectResumedEvent; import org.adamtacy.client.ui.effects.events.EffectResumedHandler; import org.adamtacy.client.ui.effects.events.EffectStartingEvent; import org.adamtacy.client.ui.effects.events.EffectStartingHandler; import org.adamtacy.client.ui.effects.events.EffectSteppingEvent; import org.adamtacy.client.ui.effects.events.EffectSteppingHandler; import org.adamtacy.client.ui.effects.events.HasAllEffectEventHandlers; import org.adamtacy.client.ui.effects.impl.browsers.EffectImplementation; import org.adamtacy.client.ui.effects.transitionsphysics.EaseInOutTransitionPhysics; import org.adamtacy.client.ui.effects.transitionsphysics.TransitionPhysics; import com.google.gwt.animation.client.Animation; import com.google.gwt.dom.client.Element; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Timer; /** * A class that provides the basics of an effect. * * From v4 a new style of setting properties is introduced. * * From v3 of GWT-FX this class is based on the GWT Animation class to keep * divergence from GWT itself as minimal as possible. To maintain the previous * API, this class extends Animation with a number of fields and methods. * * It is an abstract class that provides a fair amount of functionality, but * leaves the implementation of the * * @author adam * */ public abstract class NEffect extends Animation implements HasAllEffectEventHandlers { /** * Determine if an NEffect has started. * @return boolean value indicating if NEffect has started or not. */ public boolean isStarted() { return effectStarted; } // Indicates if start > end in play parameters (different to inverse) protected boolean backwards = false; protected Vector<NEffect> chainedEffects; protected double currentEffectPosition = 0.0; protected double currentEffectPhysicalPosition = 0.0; protected final double DEFAULT_EFFECT_LENGTH = 2.0; // Removed by enhancement 104 in favour of a Vector effectElements //protected Element effectElement; protected Vector<Element> effectElements = new Vector<Element>(); protected boolean effectFinished = false; protected boolean effectStarted = false; protected double effectLengthSeconds = DEFAULT_EFFECT_LENGTH; /** * Need this as we're not extending a widget here and so have no access to that HandlerManager. */ HandlerManager handlerManager = new HandlerManager(DEFAULT_EFFECT_LENGTH); protected boolean interruptable = true; protected boolean inverted = false; /** * Details if the effect has been initialised yet or not. * Updated by either the init(NEffectPanel thePanel) method, or through the play() method. */ protected boolean isInitialised = false; // Store the real effect end point - usually this will be at position 1.0 // unless overridden by the user (see method interpolate for use of this variable). double requestedEffectEnd = 1.0; // Store the real effect start point - usually this will be at position 0.0 // unless overridden by the user (see method interpolate for use of this variable). double requestedEffectStart = 0.0; Timer startDelay; /** * The effectPanel the effect may be attached to. * From v0.5 this is not necessary for all effects. */ protected NEffectPanel thePanel; protected TransitionPhysics transition; public HandlerRegistration addEffectCompletedHandler( EffectCompletedHandler handler) { return addHandler(handler, EffectCompletedEvent.getType()); } public HandlerRegistration addEffectInterruptedHandler( EffectInterruptedHandler handler) { return addHandler(handler, EffectInterruptedEvent.getType()); } public HandlerRegistration addEffectStartingHandler( EffectStartingHandler handler) { return addHandler(handler, EffectStartingEvent.getType()); } public HandlerRegistration addEffectSteppingHandler( EffectSteppingHandler handler) { return addHandler(handler, EffectSteppingEvent.getType()); } public HandlerRegistration addEffectResumedHandler( EffectResumedHandler handler) { return addHandler(handler, EffectResumedEvent.getType()); } public HandlerRegistration addEffectPausedHandler( EffectPausedHandler handler) { return addHandler(handler, EffectPausedEvent.getType()); } /** * Adds an Element to be managed by this effect. * See enhancement 104: http://code.google.com/p/gwt-fx/issues/detail?id=104 */ public void addEffectElement(Element el){ effectElements.add(el); } /** * Adds this handler to the widget. * * @param handler the handler * @param type the event type * @return {@link HandlerRegistration} used to remove the handler */ protected final <H extends EventHandler> HandlerRegistration addHandler( final H handler, GwtEvent.Type<H> type) { return ensureHandlers().addHandler(type, handler); } /** * Cancels the effect only if it is interruptable. */ @Override public void cancel() { if (interruptable) super.cancel(); } boolean paused = false; HandlerRegistration chainedEffectsHandlers; /** * Adds an effect to the chain. Effects are linked together in * the init method. * * @param effect The effect to chain. */ public void chain(final NEffect effect) { if (chainedEffects == null){ chainedEffects = new Vector<NEffect>(); } chainedEffects.add(effect); } boolean looping = false; public void setLooping(boolean val) { looping = val; } public boolean isLooped(){ return looping; } /** * Unchains all effects that have previously been chained. */ public void unchain(){ if(chainedEffects != null){ if(chainedEffectsHandlers!=null)chainedEffectsHandlers.removeHandler(); chainedEffects = null; chainedEffectsHandlers = null; } isInitialised = false; } /** * Ensures the existence of the handler manager. * * @return the handler manager */ HandlerManager ensureHandlers() { return handlerManager == null ? handlerManager = new HandlerManager(this) : handlerManager; } /** * Fires an event. Usually used when passing an event from one source to * another. * * @param event the event */ public void fireEvent(GwtEvent<?> event) { if (handlerManager != null) { handlerManager.fireEvent(event); } } /** * Returns the effect duration in seconds. */ public double getDuration() { return effectLengthSeconds; } /** * Returns the first element this effect is applied to, unless this * effect is added to an {@link org.adamtacy.client.ui.NEffectPanel}, * in which case that panel's Element is returned. */ public Element getEffectElement(){ Element toReturn = effectElements.get(0); if (thePanel!=null) toReturn = thePanel.getElement(); return toReturn; } public NEffectPanel getEffectPanel() { return thePanel; } public HandlerManager getHandlers() { return handlerManager; } /** * Returns the current progress of this effect: a value somewhere * between 0.0 and 1.0. */ public double getProgress() { return currentEffectPhysicalPosition; } /** * Returns the interpolated progress of this effect. */ public double getProgressInterpolated() { return currentEffectPosition; } /** * Returns the current Transition Physics Model used by this effect. */ public TransitionPhysics getTransitionPhysics() { return transition; } public void init(){ if (transition == null) transition = new EaseInOutTransitionPhysics(); if (chainedEffects != null) { // Start chaining the effects together // First initialise them all if(thePanel!=null) for (Iterator<NEffect> it = chainedEffects.iterator(); it.hasNext();) { it.next().init(thePanel); } // Add an effect completed handler for this effect to fire all chained effects chainedEffectsHandlers = this.addEffectCompletedHandler(new EffectCompletedHandler() { public void onEffectCompleted(EffectCompletedEvent event) { for (Iterator<NEffect> it = chainedEffects.iterator(); it.hasNext();) { it.next().play(); } } }); } setUpEffect(); } /** * Initialises an effect on an {@link org.adamtacy.client.ui.NEffectPanel}. * * @param thePanel The NEffectPanel on which this effect is being applied. */ public void init(NEffectPanel thePanel) { this.thePanel = thePanel; init(); isInitialised = true; } /** * Interpolates the progress of this effect based on the Transition Physics. * Subclasses should fire {@link org.adamtacy.client.ui.effects.events.EffectSteppingEvent} * with the interpolated value. */ @Override protected double interpolate(double progress) { currentEffectPhysicalPosition = interpolateWithoutUpdate(progress); return interpolateFinish(currentEffectPhysicalPosition); } protected double interpolateWithoutUpdate(double progress) { if (transition == null) return 1.0; if (!backwards) { progress = progress + requestedEffectStart; if (progress > requestedEffectEnd) progress = requestedEffectEnd; } else { progress = requestedEffectStart - progress; if (progress < requestedEffectEnd) progress = requestedEffectEnd; } if ((progress == 1.0)) { if (inverted) return 0.0; else return 1.0; } if (progress == 0.0) { if (inverted) return 1.0; else return 0.0; } return progress; } /** * Completes the interpolate functionality. * Zak asked what is returned if transition is null - transition shouldn't be null, but good catch. * If transition is null at this point, just return the value passed in. * @param progress * @return */ private double interpolateFinish(double progress){ double val = progress; if (transition!=null){ val = transition.getAnimationPosition(progress); if (inverted) val = 1 - val; } return val; } /** * Inverts an effect. Currently not available for Sequential composite effects. */ public void invert() { assert (!(this instanceof SequentialCompositeEffect)); inverted = !inverted; } /** * Returns a boolean value indicating if the effect has finished. */ public boolean isFinished() { return effectFinished; } /** * Returns a boolean value to indicate if the effect is interruptable. */ public boolean isInterruptable() { return interruptable; } /** * Returns a boolean value to indicate if the effect is inverted. */ public boolean isInverted() { return inverted; } /** * Called immediately after the animation is cancelled. Fires * {@link org.adamtacy.client.ui.effects.events.EffectPausedEvent} or * {@link org.adamtacy.client.ui.effects.events.EffectInterruptedEvent} * as appropriate. * * Does not call <code>super.onCancel()</code>. See issues 82 and 83: * http://code.google.com/p/gwt-fx/issues/detail?id=82 * http://code.google.com/p/gwt-fx/issues/detail?id=83 */ @Override protected void onCancel() { if(paused) fireEvent(new EffectPausedEvent()); else fireEvent(new EffectInterruptedEvent()); } /** * Called immediately after the animation completes. Fires * {@link org.adamtacy.client.ui.effects.events.EffectCompletedEvent}. * If this effect is looping, calls {@link #play(double, double)}. */ @Override protected void onComplete() { super.onComplete(); effectFinished = true; fireEvent(new EffectCompletedEvent()); if(looping){ DeferredCommand.addCommand(new Command(){ public void execute() { play(0.0, 1.0); } }); } } /** * Called immediately before the animation starts. Fires * {@link org.adamtacy.client.ui.effects.events.EffectStartingEvent}. * * Does not call <code>super.onStart()</code>. See issue 95: * http://code.google.com/p/gwt-fx/issues/detail?id=95 */ @Override protected void onStart() { effectFinished = false; effectStarted = true; fireEvent(new EffectStartingEvent()); } /** * Called when the animation should be updated. * * The value of progress is between 0.0 and 1.0 inclusively (unless you * override the {@link #interpolate(double)} method to provide a wider range * of values). You can override {@link #onStart()} and {@link #onComplete()} * to perform setup and tear down procedures. * * Note that onUpdate events are now triggered by a call to the interpolate * method whereas before they were through the direct update methods - this * needs to be done since the update method in GWT Animation is private. */ @Override protected void onUpdate(double progress) { currentEffectPosition = progress; } /** * Pauses an effect and sets paused variable to true */ public void pause(){ cancel(); paused = true; } /** * Plays the effect after a specified delay, starting at position 0.0 * and going to 1.0. * * @param afterDelay The delay in milliseconds. */ public void play(int afterDelay){ startDelay = new Timer(){ @Override public void run() { play(); } }; startDelay.schedule(afterDelay); } /** * Plays the effect after a specified delay from specified start and end * positions. * * @param start Start position. * @param end End position. * @param afterDelay The delay in milliseconds. */ public void play(final double start, final double end, int afterDelay){ startDelay = new Timer(){ @Override public void run() { play(start, end); } }; startDelay.schedule(afterDelay); } /** * Plays the effect from specified start and end positions. If the start position * is greater than the end position, this effect is played in reverse. * * Note that the portion of effect selected will run at the same tempo as the * whole effect, but the firing of any postEffect handlers will not occur * until the whole duration is complete. * * For example, an effect with duration 2 seconds, but calling play(0.0, 0.5) * will appear to viewer to complete in 1 second, but will not fire any * postEvents until after 2 seconds. * * @param start The requested start position. * @param end The requested end position. */ public void play(double start, double end) { if (start > end) backwards = true; else backwards = false; requestedEffectStart = start; requestedEffectEnd = end; play(); } /** * Plays the effect using the registered start and end positions. If no positions * are set they are defaulted to 0.0 and 1.0 respectively. */ public void play() { if(!isInitialised){ init(); isInitialised = true; } double timeRatio = this.requestedEffectEnd - this.requestedEffectStart; if (timeRatio<0) timeRatio = -timeRatio; run((int) ((effectLengthSeconds * 1000) * timeRatio)); } protected void registerEffectElement(){ if(effectElements.isEmpty()){ Element el = thePanel.getWidget().getElement(); effectElements.add(el); } } /** * Cancels this effect and tears it down. */ public void remove() { cancel(); tearDownEffect(); } /** * Removes and tears down this effect. This should only be used when this * effect has been added to an Element directly. If this effect is added to * an {@link org.adamtacy.client.ui.NEffectPanel}, it must be removed using * {@link org.adamtacy.client.ui.NEffectPanel#removeEffect(NEffect)}. * * See issue 77: http://code.google.com/p/gwt-fx/issues/detail?id=77 */ public void removeEffect(){ removeEffect(true); } /** * Removes this effect, optionally tearing it down. This should only be used * when this effect has been added to an Element directly. If this effect is * added to an {@link org.adamtacy.client.ui.NEffectPanel}, it must be removed * using {@link org.adamtacy.client.ui.NEffectPanel#removeEffect(NEffect)}. * * See issue 77: http://code.google.com/p/gwt-fx/issues/detail?id=77 * * @param undoEffect Whether or not to tear down this effect. */ public void removeEffect(boolean undoEffect){ assert(thePanel == null); if(undoEffect) tearDownEffect(); effectElements = null; } /** * Resets this effect. */ // FIXME adam.tacy Shouldn't this work even if the panel is null? public void reset() { if (thePanel != null) onUpdate(0.0); } /** * Plays this effect backwards from its current progress to 0.0. Fires * {@link org.adamtacy.client.ui.effects.events.EffectResumedEvent}. */ public void resumeBackwards() { resumeBackwards(0.0); } /** * Plays this effect backwards from its current progress to a specified * point. Fires {@link org.adamtacy.client.ui.effects.events.EffectResumedEvent}. * * @param end Progress point at which to end. */ public void resumeBackwards(double end) { paused = false; play(getProgress(), end); fireEvent(new EffectResumedEvent()); } /** * Plays this effect from its current progress to 1.0. Fires * {@link org.adamtacy.client.ui.effects.events.EffectResumedEvent}. */ public void resumeForwards() { resumeForwards(1.0); } /** * Plays this effect from its current progress to a specified point. * Fires {@link org.adamtacy.client.ui.effects.events.EffectResumedEvent}. * * @param end Progress point at which to end. */ public void resumeForwards(double end) { paused = false; this.play(getProgress(), end); fireEvent(new EffectResumedEvent()); } /** * Sets this effect's duration in seconds. * * @param duration The duration in seconds. */ public void setDuration(double duration) { effectLengthSeconds = duration; } /** * Sets the Element to which this effect will be applied. This will remove * all ties this effect has to other Elements. If you would like to add * multiple Elements, use {@link #addEffectElement(Element)}. * * @param element The Element to which this effect should be applied. */ public void setEffectElement(Element element){ if (thePanel!=null)throw new RuntimeException("Trying to change effectElement when an EffectPanel is in place"); effectElements = new Vector<Element>(); effectElements.add(element); isInitialised = false; } /** * Sets the effect's position. Fires appropriate events. * * @param position The position to be set. */ public void setPosition(double position) { if (!this.isInitialised) { init(); isInitialised = true; } double val = interpolate(position); onUpdate(val); if (val == 0) fireEvent(new EffectStartingEvent()); else if (val == 0) fireEvent(new EffectCompletedEvent()); else fireEvent(new EffectSteppingEvent()); } /** * Sets the {@link org.adamtacy.client.ui.effects.transitionsphysics.TransitionPhysics} * to be used with this effect. * * @param transition The transition physics to use. */ public void setTransitionType(TransitionPhysics transition) { this.transition = transition; if(chainedEffects!=null){ for (Iterator<NEffect> it = chainedEffects.iterator(); it.hasNext();) { NEffect theEffect = it.next(); if(!theEffect.equals(this))theEffect.setTransitionType(transition); } } } /** * Gets the layout definition that IE requires to use filters. * Only has meaning for IE. */ public String getIELayoutDefinition(){ return EffectImplementation.getIELayoutDefinition(); } /** * Sets the layout definition that IE requires to use filters. * Only required for IE. */ public void setIELayoutDefinition(String id, String val){ EffectImplementation.setIELayoutDefinition(id,val); } /** * Sets up this effect. Implemented by subclasses, for example a Show effect * may wish to hide the Element before starting. */ public abstract void setUpEffect(); /** * Tears down this effect. Called when this effect is removed. Implemented by * subclasses, for example, the Move effect needs to fix the style properties * that it sets up. */ public abstract void tearDownEffect(); }