// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.collide.client.util;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import elemental.css.CSSStyleDeclaration;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.html.Element;
/**
* Utility class for using CSS3 transitions.
*/
public class AnimationUtils {
public static final double ALERT_TRANSITION_DURATION = 2.0;
public static final double LONG_TRANSITION_DURATION = 0.7;
public static final double MEDIUM_TRANSITION_DURATION = 0.3;
public static final double SHORT_TRANSITION_DURATION = 0.2;
private static final String OLD_OVERFLOW_STYLE_KEY = "__old_overflow";
private static final String TRANSITION_PROPERTIES = "all";
/**
* Handles transition ends and optionally invokes an animation callback.
*/
private static class TransitionEndHandler implements EventListener {
private EventListener animationCallback;
private TransitionEndHandler(EventListener animationCallback) {
this.animationCallback = animationCallback;
}
private void handleEndFor(Element elem, String type) {
// An element should only have 1 transition end handler at a time. We
// remove in the handle callback, but we cannot depend on the handle
// callback being correctly invoked. Over eager removal is OK.
TransitionEndHandler oldListener = getOldListener(elem, type);
if (oldListener != null) {
elem.removeEventListener(type, oldListener, false);
oldListener.maybeDispatchAnimationCallback(null);
}
elem.addEventListener(type, this, false);
replaceOldListener(elem, type, this);
}
private native void replaceOldListener(
Element elem, String type, TransitionEndHandler transitionEndHandler) /*-{
elem["__" + type + "_h"] = transitionEndHandler;
}-*/;
private native TransitionEndHandler getOldListener(Element elem, String type) /*-{
return elem["__" + type + "_h"];
}-*/;
private void maybeDispatchAnimationCallback(Event evt) {
if (animationCallback != null) {
animationCallback.handleEvent(evt);
animationCallback = null;
}
}
@Override
public void handleEvent(Event evt) {
Element target = (Element) evt.getTarget();
target.removeEventListener(evt.getType(), this, false);
removeTransitions(target.getStyle());
maybeDispatchAnimationCallback(evt);
}
}
/**
* @see: {@link #animatePropertySet(Element, String, String, double,
* EventListener)}.
*/
public static void animatePropertySet(
final Element elem, String property, String value, double duration) {
animatePropertySet(elem, property, value, duration, null);
}
/**
* Enables animations prior to setting the value for the specified style
* property on the supplied element. The end result is that there property is
* transitioned to.
*
* @param elem the {@link Element} we want to set the style property on.
* @param property the name of the style property we want to set.
* @param value the target value of the style property.
* @param duration the time in seconds we want the transition to last.
* @param animationCallback callback that is invoked when the animation
* completes. It will be passed a {@code null} event if the animation
* was pre-empted by some other animation on the same element.
*/
public static void animatePropertySet(final Element elem, String property, String value,
double duration, final EventListener animationCallback) {
final CSSStyleDeclaration style = elem.getStyle();
enableTransitions(style, duration);
if (BrowserUtils.isFirefox()) {
// For FF4.
new TransitionEndHandler(animationCallback).handleEndFor(elem, "transitionend");
} else {
// For webkit based browsers.
// TODO: Keep an eye on whether or not webkit supports the
// vendor prefix free version. If they ever do we should remove this.
new TransitionEndHandler(animationCallback).handleEndFor(elem, Event.WEBKITTRANSITIONEND);
}
style.setProperty(property, value);
}
public static void backupOverflow(CSSStyleDeclaration style) {
style.setProperty(OLD_OVERFLOW_STYLE_KEY, style.getOverflow());
style.setOverflow("hidden");
}
/**
* Disables CSS3 transitions.
*
* If you want to reset transitions after enabling them, use
* {@link #removeTransitions(CSSStyleDeclaration)} instead.
*/
public static void disableTransitions(CSSStyleDeclaration style) {
style.setProperty("-webkit-transition-property", "none");
style.setProperty("-moz-transition-property", "none");
}
/**
* @see: {@link #enableTransitions(CSSStyleDeclaration, double)}
*/
public static void enableTransitions(CSSStyleDeclaration style) {
enableTransitions(style, SHORT_TRANSITION_DURATION);
}
/**
* Enables CSS3 transitions for a given element.
*
* If you want to reset transitions after disabling them, use
* {@link #removeTransitions(CSSStyleDeclaration)} instead.
*
* @param style the style object belonging to the element we want to animate.
* @param duration the length of time we want the animation to last.
*/
public static void enableTransitions(CSSStyleDeclaration style, double duration) {
style.setProperty("-webkit-transition-property", TRANSITION_PROPERTIES);
style.setProperty("-moz-transition-property", TRANSITION_PROPERTIES);
style.setProperty("-webkit-transition-duration", duration + "s");
style.setProperty("-moz-transition-duration", duration + "s");
}
/**
* Removes CSS3 transitions, returning them to their original state.
*/
public static void removeTransitions(CSSStyleDeclaration style) {
style.removeProperty("-webkit-transition-property");
style.removeProperty("-moz-transition-property");
style.removeProperty("-webkit-transition-duration");
style.removeProperty("-moz-transition-duration");
}
public static void fadeIn(final Element elem) {
elem.getStyle().setDisplay(CSSStyleDeclaration.Display.BLOCK);
// TODO: This smells like a chrome bug to me that we need to do a
// deferred command here.
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
animatePropertySet(elem, "opacity", "1.0", SHORT_TRANSITION_DURATION);
}
});
}
public static void fadeOut(final Element elem) {
animatePropertySet(elem, "opacity", "0", SHORT_TRANSITION_DURATION, new EventListener() {
@Override
public void handleEvent(Event evt) {
elem.getStyle().setDisplay("none");
}
});
}
/**
* Flashes an element to highlight that it has recently changed.
*/
public static void flash(final Element elem) {
/*
* If we interrupt a flash with another flash, we need to disable animations
* so the initial background color takes effect immediately. Animations are
* reenabled in animatePropertySet.
*/
removeTransitions(elem.getStyle());
elem.getStyle().setBackgroundColor("#f9edbe");
// Give the start color a chance to take effect.
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
animatePropertySet(elem, "background-color", "", ALERT_TRANSITION_DURATION);
}
});
}
public static void restoreOverflow(CSSStyleDeclaration style) {
style.setOverflow(style.getPropertyValue(OLD_OVERFLOW_STYLE_KEY));
}
}