diff --git a/masonry-addon/src/main/java/org/vaadin/alump/masonry/MasonryLayout.java b/masonry-addon/src/main/java/org/vaadin/alump/masonry/MasonryLayout.java index 40a6f9d..a5eb8e2 100644 --- a/masonry-addon/src/main/java/org/vaadin/alump/masonry/MasonryLayout.java +++ b/masonry-addon/src/main/java/org/vaadin/alump/masonry/MasonryLayout.java @@ -40,6 +40,19 @@ */ @JavaScript({ "masonry.pkgd.min.js" }) public class MasonryLayout extends AbstractLayout implements LayoutEvents.LayoutClickNotifier { + + public enum EffectType { + OPACITY(1), MOVE_UP(2), SCALE_UP(3), FALL_PERSPECTIVE(4), FLY(5), FLIP(6), HELIX(7), POP(8); + private final int value; + + private EffectType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } /** * Wrapper style name for components added. Will tell wrapper to take double width. @@ -90,6 +103,52 @@ public MasonryLayout(int columnWidth) { this(); getState().columnWidth = columnWidth; } + + /** + * Create new masonry layout with defined column width + * @param columnWidth Column width used to calculate left positions of items. Can not be changed after start. + * Remember to update horizontal values of CSS rules to match with this. + */ + + /** + * Create new masonry layout with defined column width and gutter width + * @param columnWidth Column width used to calculate left positions of items. Can not be changed after start. + * @param gutter Gutter width (default 10). To set vertical space use margin CSS rule. (margin-bottom:10px). Can not be changed after start. + */ + public MasonryLayout(int columnWidth, int gutter) { + this(); + getState().columnWidth = columnWidth; + getState().gutter = gutter; + } + + /** + * Create new masonry layout with defined column width, gutter width and transition duration + * @param columnWidth columnWidth Column width used to calculate left positions of items. Can not be changed after start. + * @param gutter gutter Gutter width (default 10). To set vertical space use margin CSS rule. (margin-bottom:10px). Can not be changed after start. + * @param transitionDuration Duration of the transition when items change position or appearance, set in a CSS time format. Default 0.4s. Can not be changed after start. + */ + public MasonryLayout(int columnWidth, int gutter, String transitionDuration) { + this(); + getState().columnWidth = columnWidth; + getState().gutter = gutter; + getState().transitionDuration = transitionDuration; + } + + /** + * Create new masonry layout with defined column width, gutter width and transition duration + * @param columnWidth columnWidth Column width used to calculate left positions of items. Can not be changed after start. + * @param gutter gutter Gutter width (default 10). To set vertical space use margin CSS rule. (margin-bottom:10px). Can not be changed after start. + * @param EffectType Animation effect on scroll. Can not be changed after start. + */ + public MasonryLayout(int columnWidth, int gutter, EffectType effect) { + this(); + getState().columnWidth = columnWidth; + getState().gutter = gutter; + //!!! When using animation effect, masonry transition must be disabled! + getState().transitionDuration = "0s"; + getState().effect = effect.getValue(); + } + // We must override getState() to cast the state to MyComponentState @Override diff --git a/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutConnector.java b/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutConnector.java index a7ec6e0..3c4b858 100644 --- a/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutConnector.java +++ b/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutConnector.java @@ -89,14 +89,21 @@ public void onStateChanged(StateChangeEvent stateChangeEvent) { clickEventHandler.handleEventHandlerRegistration(); // call always, will be ignored after first time - getWidget().initialize(getState().columnWidth); + getWidget().initialize(getState().columnWidth, getState().gutter, getState().transitionDuration, getState().effect); + + if (getWidget().effect != getState().effect) { + getWidget().effect = getState().effect; + getWidget().addStyleName("animated effect" + getState().effect); + } + + } @Override public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { - getWidget().initialize(getState().columnWidth); + getWidget().initialize(getState().columnWidth, getState().gutter, getState().transitionDuration, getState().effect); for (ComponentConnector child : event.getOldChildren()) { if (child.getParent() != this) { diff --git a/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutState.java b/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutState.java index c913f96..a679a46 100644 --- a/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutState.java +++ b/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryLayoutState.java @@ -14,6 +14,13 @@ public class MasonryLayoutState extends AbstractLayoutState { public int columnWidth = 300; + + public int gutter = 10; + + public String transitionDuration = "0.4s"; + + //no effects + public int effect = 0; /** * Addtional stylenames for items (usually related to sizing) diff --git a/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryPanel.java b/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryPanel.java index 920c416..8bdc5e7 100644 --- a/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryPanel.java +++ b/masonry-addon/src/main/java/org/vaadin/alump/masonry/client/MasonryPanel.java @@ -20,12 +20,22 @@ package org.vaadin.alump.masonry.client; import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.event.dom.client.ScrollEvent; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.logical.shared.ResizeHandler; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.json.client.JSONNumber; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONString; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.ComplexPanel; import com.google.gwt.user.client.ui.Widget; @@ -48,18 +58,58 @@ public class MasonryPanel extends ComplexPanel { * Class name added while js library is performing layout */ public static final String RENDERING_CLASSNAME = "masonry-rendering"; + + /** + * Delaying animation + */ + private AnimationTimer animationTimer = new AnimationTimer(); + + /** + * For unregistering + */ + private HandlerRegistration scrollHandlerRegistration; + private HandlerRegistration resizeHandlerRegistration; + + private ScrollHandler scrollHandler = new ScrollHandler() { + + @Override + public void onScroll(ScrollEvent event) { + if (effect == 0) { + return; + } + animationTimer.cancel(); + animationTimer.schedule(); + + } + }; + + private ResizeHandler resizeHandler = new ResizeHandler() { + + @Override + public void onResize(ResizeEvent event) { + if (effect == 0) { + return; + } + animationTimer.cancel(); + animationTimer.schedule(); + + } + }; + + public int effect = 0; + public MasonryPanel() { setElement(Document.get().createDivElement()); } - public void initialize(int columnWidth) { + public void initialize(int columnWidth, int gutter, String transitionDuration, int effect) { if(msnry != null) { return; } - - msnry = initializeMasonry(getElement(), createMasonryProperties(columnWidth).getJavaScriptObject()); + + msnry = initializeMasonry(getElement(), createMasonryProperties(columnWidth, gutter, transitionDuration).getJavaScriptObject()); } public void setVisible(boolean visible) { @@ -72,6 +122,25 @@ public void setVisible(boolean visible) { layout(); } } + + + @Override + protected void onLoad() { + super.onLoad(); + scrollHandlerRegistration = getParent().addDomHandler(scrollHandler, ScrollEvent.getType()); + resizeHandlerRegistration = Window.addResizeHandler(resizeHandler); + } + + @Override + protected void onUnload() { + if (scrollHandlerRegistration != null) { + scrollHandlerRegistration.removeHandler(); + } + if (resizeHandlerRegistration != null) { + resizeHandlerRegistration.removeHandler(); + } + super.onUnload(); + } public void onDetach() { if(msnry != null) { @@ -113,9 +182,11 @@ public void layout() { if(isVisible() && isAttached()) { addStyleName(RENDERING_CLASSNAME); nativeLayout(msnry); + markElementsInViewport(); } } - + + protected static native void nativeLayout(JavaScriptObject msnry) /*-{ msnry.layout(); @@ -136,10 +207,12 @@ protected static native void nativeDestroy(JavaScriptObject msnry) msnry.destroy(); }-*/; - protected static JSONObject createMasonryProperties(int columnWidth) { + protected static JSONObject createMasonryProperties(int columnWidth, int gutter, String transitionDuration) { JSONObject obj = new JSONObject(); obj.put("columnWidth", new JSONNumber(columnWidth)); obj.put("itemSelector", new JSONString("." + ITEM_CLASSNAME)); + obj.put("gutter", new JSONNumber(gutter)); + obj.put("transitionDuration", new JSONString(transitionDuration)); return obj; } @@ -159,5 +232,114 @@ protected native JavaScriptObject initializeMasonry(Element element, JavaScriptO protected void onLayoutComplete() { removeStyleName(RENDERING_CLASSNAME); } + + private int getViewportHeight() { + return getParent().getOffsetHeight(); + } + + private int getViewportScroll() { + return getElement().getParentElement().getScrollHeight(); + } + + private int getViewportScrollTop() { + return getElement().getParentElement().getScrollTop(); + } + + // http://stackoverflow.com/a/5598797/989439 + private int getOffset(Element e ) { + int offsetTop = 0; + Element el = e; + do { + offsetTop += el.getOffsetTop(); + } while( el == el.getOffsetParent() ); + + return offsetTop; + } + + private boolean isInViewport(Element e, double percent) { + int elementHeight = e.getOffsetHeight(); + int scrolled = getViewportScrollTop(); + int viewed = scrolled + getViewportHeight(); + int elTop = getOffset(e); + int elBottom = elTop + elementHeight; + + // if 0, the element is considered in the viewport as soon as it enters. + // if 1, the element is considered in the viewport only when it's fully inside + // value in percentage (1 >= percent >= 0) + return (elTop + elementHeight * percent) <= viewed && (elBottom - elementHeight * percent) >=scrolled; + } + + private void markElementsInViewport() { + if (effect == 0) { + return; + } + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + NodeList childs = getNotAnimatedElements(getElement()); + if (childs != null) { + for (int i=0; i childs = getNewElements(getElement()); + if (childs != null) { + for (int i=0; i getElementsByClassName(String clazz, Node ctx) /*-{ + return ctx.getElementsByClassName(clazz); + }-*/; + + public static native NodeList getNotAnimatedElements(Node ctx) /*-{ + return ctx.querySelectorAll(":not(.animate)"); + }-*/; + + public static native NodeList getNewElements(Node ctx) /*-{ + //console.log(ctx.querySelectorAll(".masonry-item:not(.animate):not(.shown)")); + return ctx.querySelectorAll(".masonry-item:not(.animate):not(.shown)"); + }-*/; } \ No newline at end of file diff --git a/masonry-addon/src/main/resources/org/vaadin/alump/masonry/public/masonry/styles.css b/masonry-addon/src/main/resources/org/vaadin/alump/masonry/public/masonry/styles.css index 6bb5442..0b68f7c 100644 --- a/masonry-addon/src/main/resources/org/vaadin/alump/masonry/public/masonry/styles.css +++ b/masonry-addon/src/main/resources/org/vaadin/alump/masonry/public/masonry/styles.css @@ -111,4 +111,187 @@ .v-ddwrapper.dnd-masonry-layout { padding: 0px; border-radius: 0px; -} \ No newline at end of file +} + +/* Animation keyframes */ + +@-webkit-keyframes mfadeIn { + 0% { } + 100% { opacity: 1; } +} +@keyframes mfadeIn { + 0% { } + 100% { opacity: 1; } +} + +@-webkit-keyframes mmoveUp { + 0% { } + 100% { -webkit-transform: translateY(0); opacity: 1; } +} + +@keyframes mmoveUp { + 0% { } + 100% { -webkit-transform: translateY(0); transform: translateY(0); opacity: 1; } +} + +@-webkit-keyframes mscaleUp { + 0% { } + 100% { -webkit-transform: scale(1); opacity: 1; } +} + +@keyframes mscaleUp { + 0% { } + 100% { -webkit-transform: scale(1); transform: scale(1); opacity: 1; } +} +@-webkit-keyframes mfallPerspective { + 0% { } + 100% { -webkit-transform: translateZ(0px) translateY(0px) rotateX(0deg); opacity: 1; } +} + +@keyframes mfallPerspective { + 0% { } + 100% { -webkit-transform: translateZ(0px) translateY(0px) rotateX(0deg); transform: translateZ(0px) translateY(0px) rotateX(0deg); opacity: 1; } +} +@-webkit-keyframes mfly { + 0% { } + 100% { -webkit-transform: rotateX(0deg); opacity: 1; } +} +@keyframes mfly { + 0% { } + 100% { -webkit-transform: rotateX(0deg); transform: rotateX(0deg); opacity: 1; } +} +@-webkit-keyframes mflip { + 0% { } + 100% { -webkit-transform: rotateX(0deg); opacity: 1; } +} +@keyframes mflip { + 0% { } + 100% { -webkit-transform: rotateX(0deg); transform: rotateX(0deg); opacity: 1; } +} +@-webkit-keyframes mhelix { + 0% { } + 100% { -webkit-transform: rotateY(0deg); opacity: 1; } +} +@keyframes mhelix { + 0% { } + 100% { -webkit-transform: rotateY(0deg); transform: rotateY(0deg); opacity: 1; } +} +@-webkit-keyframes mpopUp { + 0% { } + 70% { -webkit-transform: scale(1.1); opacity: .8; -webkit-animation-timing-function: ease-out; } + 100% { -webkit-transform: scale(1); opacity: 1; } +} +@keyframes mpopUp { + 0% { } + 70% { -webkit-transform: scale(1.1); transform: scale(1.1); opacity: .8; -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } + 100% { -webkit-transform: scale(1); transform: scale(1); opacity: 1; } +} + +.masonry-layout.animated .masonry-item { + opacity: 0; +} + +/* Effect 1: opacity */ +.masonry-layout.animated.effect1 .masonry-item.animate { + -webkit-animation: mfadeIn 0.65s ease forwards; + animation: mfadeIn 0.65s ease forwards; +} + +/* Effect 2: Move Up */ +.masonry-layout.animated.effect2 .masonry-item.animate { + -webkit-transform: translateY(200px); + transform: translateY(200px); + -webkit-animation: mmoveUp 0.65s ease forwards; + animation: mmoveUp 0.65s ease forwards; +} + +/* Effect 3: Scale up */ +.masonry-layout.animated.effect3 .masonry-item.animate { + -webkit-transform: scale(0.6); + transform: scale(0.6); + -webkit-animation: mscaleUp 0.65s ease-in-out forwards; + animation: mscaleUp 0.65s ease-in-out forwards; +} + +/* Effect 4: fall perspective */ +.masonry-layout.animated.effect4 { + -webkit-perspective: 1300px; + perspective: 1300px; +} + +.masonry-layout.animated.effect4 .masonry-item.animate { + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform: translateZ(400px) translateY(300px) rotateX(-90deg); + transform: translateZ(400px) translateY(300px) rotateX(-90deg); + -webkit-animation: mfallPerspective .8s ease-in-out forwards; + animation: mfallPerspective .8s ease-in-out forwards; +} + +/* Effect 5: fly (based on http://lab.hakim.se/scroll-effects/ by @hakimel) */ +.masonry-layout.animated.effect5 { + -webkit-perspective: 1300px; + perspective: 1300px; +} + +.masonry-layout.animated.effect5 .masonry-item.animate { + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform-origin: 50% 50% -300px; + transform-origin: 50% 50% -300px; + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + -webkit-animation: mfly .8s ease-in-out forwards; + animation: mfly .8s ease-in-out forwards; +} + +/* Effect 6: flip (based on http://lab.hakim.se/scroll-effects/ by @hakimel) */ +.masonry-layout.animated.effect6 { + -webkit-perspective: 1300px; + perspective: 1300px; +} + +.masonry-layout.animated.effect6 .masonry-item.animate { + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform-origin: 0% 0%; + transform-origin: 0% 0%; + -webkit-transform: rotateX(-80deg); + transform: rotateX(-80deg); + -webkit-animation: mflip .8s ease-in-out forwards; + animation: mflip .8s ease-in-out forwards; +} + +/* Effect 7: helix (based on http://lab.hakim.se/scroll-effects/ by @hakimel) */ +.masonry-layout.animated.effect7 { + -webkit-perspective: 1300px; + perspective: 1300px; +} + +.masonry-layout.animated.effect7 .masonry-item.animate { + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform: rotateY(-180deg); + transform: rotateY(-180deg); + -webkit-animation: mhelix .8s ease-in-out forwards; + animation: mhelix .8s ease-in-out forwards; +} + +/* Effect 8: */ +.masonry-layout.animated.effect8 { + -webkit-perspective: 1300px; + perspective: 1300px; +} + +.masonry-layout.animated.effect8 .masonry-item.animate { + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform: scale(0.4); + transform: scale(0.4); + -webkit-animation: mpopUp .8s ease-in forwards; + animation: mpopUp .8s ease-in forwards; +} + + + +