Java tutorial
/* * Copyright 2008 Eckhart Arnold (eckhart_arnold@hotmail.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. */ package de.eckhartarnold.client; import java.lang.String; import java.lang.Math; import java.util.ArrayList; import java.util.HashMap; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.ErrorEvent; import com.google.gwt.event.dom.client.ErrorHandler; import com.google.gwt.event.dom.client.HasClickHandlers; import com.google.gwt.event.dom.client.HasMouseDownHandlers; import com.google.gwt.event.dom.client.HasMouseMoveHandlers; import com.google.gwt.event.dom.client.HasMouseUpHandlers; import com.google.gwt.event.dom.client.HasMouseWheelHandlers; import com.google.gwt.event.dom.client.HasTouchStartHandlers; //import com.google.gwt.event.dom.client.HasTouchEndHandlers; //import com.google.gwt.event.dom.client.HasTouchMoveHandlers; //import com.google.gwt.event.dom.client.HasTouchStartHandlers; import com.google.gwt.event.dom.client.LoadEvent; import com.google.gwt.event.dom.client.LoadHandler; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseDownHandler; import com.google.gwt.event.dom.client.MouseMoveEvent; import com.google.gwt.event.dom.client.MouseMoveHandler; import com.google.gwt.event.dom.client.MouseUpEvent; import com.google.gwt.event.dom.client.MouseUpHandler; import com.google.gwt.event.dom.client.MouseWheelEvent; import com.google.gwt.event.dom.client.MouseWheelHandler; import com.google.gwt.event.dom.client.TouchStartEvent; import com.google.gwt.event.dom.client.TouchStartHandler; //import com.google.gwt.event.dom.client.TouchEndEvent; //import com.google.gwt.event.dom.client.TouchEndHandler; //import com.google.gwt.event.dom.client.TouchMoveEvent; //import com.google.gwt.event.dom.client.TouchMoveHandler; //import com.google.gwt.event.dom.client.TouchStartEvent; //import com.google.gwt.event.dom.client.TouchStartHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.AbsolutePanel; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; /** * Displays a photo on an absolute panel. * * <p>The image is scaled so as to use as much space of the * panel as possible without distorting the aspect ratio of the image. * When the image is exchanged the old image is faded out while * the new image is faded in smoothly. * * @author eckhart */ public class ImagePanel extends Composite implements HasMouseMoveHandlers, HasMouseWheelHandlers, SourcesAttachmentEvents, ResizeListener, HasClickHandlers, HasMouseDownHandlers, HasMouseUpHandlers, HasTouchStartHandlers /* , HasTouchMoveHandlers, HasTouchEndHandlers */ { /** * Interface for event listeners of <code>ImagePanel</code>. */ public interface DisplayListener { /** * Called by the time a new image is * fully displayed (i.e. loaded and faded in). */ void onDisplay(); /** * Called when image starts fading. <code>onFade()</code> * will not be called if fading is disabled or skipped for some other * reason! */ void onFade(); } private class ImageLoadHandler implements LoadHandler { private int fading; private NotifyingFade fader = null; private Widget lastSender = null; private Duration loadDuration = new Duration(); public ImageLoadHandler(int fading) { this.fading = fading; } public boolean isLoaded() { return lastSender != null; } public void onLoad(LoadEvent event) { Widget sender = (Widget) event.getSource(); if (sender == active && sender != lastSender) { // for some reason LOAD events are sometimes fired twice (due to bad IE compatitibilty tests?)!? lastSender = sender; // adjustSize((Image) sender); <- already done by exchangeImage()! if (fading == 0) { Fade.setOpacity((Image) sender, 1.0); if (passive != null) Fade.setOpacity(passive, 0.0); fireDisplay(); } else if (fading > 0) { startFading(this); } else { // fading < 0 if (fader != null && fader.isComplete()) { fireDisplay(); } } // use smaller sized images in the future if connection is slow... int elapsed = loadDuration.elapsedMillis(); GWT.log("Duration : " + String.valueOf(elapsed)); if (elapsed > duration) { GWT.log(String.valueOf(elapsed) + "; " + String.valueOf(duration)); if (sizeBias < sizes.length) sizeBias++; } else { while (sizeBias > 0 && elapsed < duration / 3) { sizeBias--; elapsed = elapsed * 3; } } GWT.log("sizeBias : " + String.valueOf(sizeBias)); } } public void connectFader(NotifyingFade fader) { if (this.fader == null) { this.fader = fader; if (fader.isComplete() && isLoaded()) fireDisplay(); } } } private class ImageErrorHandler implements ErrorHandler { public void onError(ErrorEvent event) { Image img = (Image) event.getSource(); GWT.log("ImagePanel.ImageErrorHandler.onError:\n " + img.getUrl(), null); } } private class NotifyingFade extends Fade { private ImageLoadHandler loadListener; private boolean completed = false; NotifyingFade(Widget widget, ImageLoadHandler loadHandler) { super(widget, 0.0, 1.0, FADE_IN_STEPS); this.loadListener = loadHandler; loadHandler.connectFader(this); } public boolean isComplete() { return completed; } @Override protected void onComplete() { super.onComplete(); completed = true; if (loadListener.isLoaded()) fireDisplay(); } } /** * A Fade class for fading out the old image that triggers fading in the new * image only after the old image has faded out completely. */ private class ChainedFade extends Fade { private Fade nextFade; ChainedFade(Widget widget, Fade nextFade, double steps) { super(widget, 1.0, 0.0, steps); this.nextFade = nextFade; } @Override protected void onComplete() { super.onComplete(); nextFade.run(Math.abs(fading)); } } // possible anchors of the notifier overlay image public static int WEST = 1, CENTER = 2, EAST = 3; private static final double FADE_IN_STEPS = 0.10; private static final double FADE_OUT_STEPS = 0.13; /** * The currently active image. When a new photo is loaded the image that * was displayed previously is declared passive and a new active image * is created in the front that takes the new photo and is faded in. */ protected Image active; /** * An envelope panel that wraps the <code>AbsolutePanel</code>. This is * necessary for working around some browser differences in connection with * resizing. */ protected SimplePanel envelope; /** * A semi transparent notification image above the slide show images. The * notifier image can be used to display visual feed back for touch events. */ protected Image notifier; /** Flag storing the anchor of the notifier (WEST, CENTER or EAST) */ protected int notifierAnchor; /** * The main widget of the image panel. The images are placed on panel and * their size adjusted. */ protected AbsolutePanel panel; /** * The image that was displayed out and is either already invisible, * i.e. faded out, or just about to be faded out. */ protected Image passive; private int duration = 5000; // duration in msecs for which the an image will be displayed private int fading = -750; // a negative value indicates that fading in and fading out should take place in sequence private NotifyingFade fadeIn; private Fade fadeOut; private String[] imageNames; private int panelW, panelH; private int sizeStep = -1; // a negative value means: no multiple image sizes present private int sizeBias = 0; // bias in case of slow connection private int[][] sizes; private ArrayList<AttachmentListener> attachmentListeners; private DisplayListener displayListener; private ImageErrorHandler stdImageErrorHandler = new ImageErrorHandler(); /** * Constructor of class <code>ImagePanel</code>. Before the flip panel becomes * visible, it must be added somewhere to the DOM tree (e.g. to the root panel) * <em>and</em> the <code>onResized</code> method must be called. */ public ImagePanel(ImageCollectionInfo collection) { HashMap<String, String> info = collection.getInfo(); // for (String key: info.keySet()) { // GWT.log(key+": "+info.get(key)); // } String numStr = info.get("display duration"); if (numStr != null && !numStr.isEmpty()) { duration = Integer.parseInt(numStr); assert duration > 500; } numStr = info.get("image fading"); if (numStr != null && !numStr.isEmpty()) { fading = Integer.parseInt(numStr); } envelope = new SimplePanel(); panel = new AbsolutePanel(); panel.addStyleName("imageBackground"); envelope.setWidget(panel); // envelope.setSize("100%", "100%"); initWidget(envelope); setSize("100%", "100%"); sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.ONTOUCHSTART); } /* (non-Javadoc) * @see de.eckhartarnold.client.SourcesAttachmentEvents#addAttachmentListener(de.eckhartarnold.client.AttachmentListener) */ public void addAttachmentListener(AttachmentListener listener) { if (attachmentListeners == null) attachmentListeners = new ArrayList<AttachmentListener>(); attachmentListeners.add(listener); } @Override public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) { return addHandler(handler, MouseDownEvent.getType()); } @Override public HandlerRegistration addClickHandler(ClickHandler handler) { return addHandler(handler, ClickEvent.getType()); } /* (non-Javadoc) * @see com.google.gwt.event.dom.client.HasMouseMoveHandlers#addMouseMoveHandler(com.google.gwt.event.dom.client.MouseMoveHandler) */ @Override public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) { return addHandler(handler, MouseMoveEvent.getType()); } @Override public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) { return addHandler(handler, MouseUpEvent.getType()); } /* (non-Javadoc) * @see com.google.gwt.event.dom.client.HasMouseWheelHandlers#addMouseWheelHandler(com.google.gwt.event.dom.client.MouseWheelHandler) */ @Override public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) { return addHandler(handler, MouseWheelEvent.getType()); } // /* (non-Javadoc) // * @see com.google.gwt.event.dom.client.HasTouchEndHandlers#addTouchEndHandler(com.google.gwt.event.dom.client.TouchEndHandler) // */ // public HandlerRegistration addTouchEndHandler(TouchEndHandler handler) { // return addHandler(handler, TouchEndEvent.getType()); // } // // /* (non-Javadoc) // * @see com.google.gwt.event.dom.client.HasTouchMoveHandlers#addTouchStartHandler(com.google.gwt.event.dom.client.TouchMoveHandler) // */ // public HandlerRegistration addTouchMoveHandler(TouchMoveHandler handler) { // return addHandler(handler, TouchMoveEvent.getType()); // } // /* (non-Javadoc) * @see com.google.gwt.event.dom.client.HasTouchStartHandlers#addTouchStartHandler(com.google.gwt.event.dom.client.TouchStartHandler) */ public HandlerRegistration addTouchStartHandler(TouchStartHandler handler) { return addHandler(handler, TouchStartEvent.getType()); } /** * Removes the image from the panel so that an empty panel * is shown. */ public void clear() { cancelFading(false); if (active != null) { panel.remove(active); active = null; } if (passive != null) { panel.remove(passive); passive = null; } } /** * Returns the duration for which an image will be displayed without fading. * @return The duration for image display in milliseconds. */ public int getDuration() { return duration; } /** * Returns the fading duration. A negative value indicates that fading is * sequentially, only after the last image has faded out completely, * the new image fades in. In this case the actual fading time is twice * the absolute value of <code>fading</code> * @return The duration of fading in milliseconds. */ public int getFading() { return fading; } /** * Returns the URL of the <em>largest size version(!)</em> of the currently * displayed image. * @return the image's URL */ public String getImageURL() { return imageNames[imageNames.length - 1]; } // /* (non-Javadoc) // * @see com.google.gwt.user.client.ui.Composite#onBrowserEvent(com.google.gwt.user.client.Event) // */ // @Override // public void onBrowserEvent(Event event) { // switch (event.getTypeInt()) { // case Event.ONCLICK: { // if (clickListeners != null) { // clickListeners.fireClick(this); // } // break; // } // case Event.ONMOUSEDOWN: // case Event.ONMOUSEUP: // case Event.ONMOUSEMOVE: // case Event.ONMOUSEOVER: // case Event.ONMOUSEOUT: { // if (mouseListeners != null) { // mouseListeners.fireMouseEvent(this, event); // } // break; // } // case Event.ONMOUSEWHEEL: { // if (mouseWheelListeners != null) { // mouseWheelListeners.fireMouseWheelEvent(this, event); // } // break; // } // } // } /** * Returns the panel proper, i.e. the absolute panel on which the images * are placed. * @return An absolute panel inside the image panel. */ public AbsolutePanel getPanel() { return panel; } /** * Resizes the displayed image(s), if the containing * <code>FlipImagePanel</code> has been resized. * The <code>resize</code> method is called automatically when any of * the methods <code>setSize, setPixelSize, setWidth, setHeight</code> * is called. */ public void onResized() { assert envelope.getWidget() == null : "prepareResized() must be called before onResized()!"; int lastPanelW = panelW; int lastPanelH = panelH; // envelope.clear(); panelW = envelope.getOffsetWidth(); panelH = envelope.getOffsetHeight(); if (panelH <= 100) { // unlikely small height: non-quirks mode !? panelH = Window.getClientHeight(); if (lastPanelH == panelH) lastPanelH--; } // GWT.log("ImagePanel.onResized: panelH: "+String.valueOf(panelH)); envelope.setWidget(panel); if (lastPanelW != panelW || lastPanelH != panelH) { panel.setPixelSize(panelW, panelH); if (active != null) adjustSize(active); if (sizeStep >= 0) { int newStep = pickSize(sizes); if (newStep != sizeStep) { sizeStep = newStep; quickExchangeImage(imageNames[sizeStep]); return; } } if (passive != null) adjustSize(passive); positionNotifier(); } } /** * Removes the {@link AbsolutePanel} so that the image panels size * can be determined correctly by the browser before calling * <code>onResized</code>. The <code>AbsolutePanel</code> * is restored by method <code>onResized</code>. This method therefore * should always be used in conjunction with the method * <code>onResized</code>. */ public void prepareResized() { panel.setSize("100%", "100%"); // required for Internet Explorer 7 compatibility! envelope.clear(); } /* (non-Javadoc) * @see de.eckhartarnold.client.SourcesAttachmentEvents#removeAttachmentListener(de.eckhartarnold.client.AttachmentListener) */ public void removeAttachmentListener(AttachmentListener listener) { if (attachmentListeners != null) { attachmentListeners.remove(listener); if (attachmentListeners.isEmpty()) attachmentListeners = null; } } /** * Sets the duration for which an image will be displayed without fading. * @param msecs The duration for image display in milliseconds. */ public void setDuration(int msecs) { assert msecs > 500; duration = msecs; } /** * Sets the duration for fading in a new image. A negative value of the * parameter <code>msecs</code> indicates that fading in and fading out does * not occur synchronously but in sequence. Please observe that in this case * the total fading phase takes twice as long! * * @param msecs The duration of fading in or out in milliseconds. */ public void setFading(int msecs) { this.fading = msecs; if (msecs == 0) { cancelFading(true); } } /** * Sets the notifier image, i.e. an overlay image above the slides that may * be used to indicate feedback to touch events or the like. The notifier * images opacity will be set to fully transparent in the beginning. It is * the callers duty to change this so that it may become visible to the user. * Any previously set notifier will be removed from the panel. It is possible * to set the notifier to <code>null</code> thereby clearing the notifier. * @param notifier the notifier image to be added to the panel * @param anchor the anchor, i.e. position of the image, e.g. WEST, * CENTER or EAST. */ public void setNotifier(Image notifier, int anchor) { if (this.notifier != null) { panel.remove(this.notifier); } this.notifier = notifier; this.notifierAnchor = anchor; if (notifier != null) { notifier.addStyleName("notifier"); Fade.setOpacity(notifier, 0.0); panel.add(notifier); positionNotifier(); } } // @Override // public void setHeight(String height) { // super.setHeight(height); // onResized(); // } // // @Override // public void setPixelSize(int width, int height) { // super.setPixelSize(width, height); // onResized(); // } // // @Override // public void setSize(String width, String height) { // super.setSize(width, height); // onResized(); // } // // @Override // public void setWidth(String width) { // super.setWidth(width); // onResized(); // } /** * Shows the image that the given URl points to. * @param url the URL of the image to show * @param notifier a callback that is issued when the image is loaded and * has faded in */ public void showImage(String url, DisplayListener notifier) { sizeStep = -1; imageNames[imageNames.length - 1] = url; exchangeImage(url, notifier); } /** * Shows a picture for which representations with different image sizes * exist. The representation, the size of which best fits the * <code>FlipImagePanel</code> widget's size is picked. If the widget is * resized so that another representation would be more appropriate, * the image is automatically exchanged. * * @param urls the URL of different sized versions of the same image, * each of them corresponding to one of the sizes * @param sizes the sizes of the different versions of the images. * @param notifier a callback that is issued when the image is loaded and * has faded in */ public void showImage(String urls[], int[][] sizes, DisplayListener notifier) { assert sizes.length == urls.length; imageNames = urls; this.sizes = sizes; sizeStep = pickSize(sizes); exchangeImage(urls[sizeStep], notifier); } /** * Returns the currently selected size step, if image URLs for several * size steps have been specified by the last call of method * <code>showImage()</code>. Returns -1 otherwise. * * This method has been made package private, because the information about * the selected size step is needed for prefetching. See {@link Slideshow.show}. * * @return the currently selected size step or -1, if not several size steps * have been specified. */ int getSizeStep() { return sizeStep; } /** * Exchanges a currently displayed image with the image that the given URL * points to. If fading is enabled the old image will be faded out and * the new image faded in simultaneously. * @param url the URL of the image to show * @param notifier a callback that is issued when the image is loaded and * has faded in */ protected void exchangeImage(String url, DisplayListener notifier) { cancelFading(false); Image discard = passive; passive = active; active = new Image(); active.addStyleName("slide"); Fade.setOpacity(active, 0.0); displayListener = notifier; ImageLoadHandler loadHandler = new ImageLoadHandler(fading); active.addLoadHandler(loadHandler); active.addErrorHandler(stdImageErrorHandler); if (discard != null) { panel.remove(discard); discard = null; } active.setUrl(url); panel.add(active); adjustSize(active); if (fading < 0) startFading(loadHandler); Compatibility.fireLoadOnIE(active, url, loadHandler); } @Override protected void onLoad() { for (AttachmentListener a : attachmentListeners) a.onLoad(this); } @Override protected void onUnload() { cancelFading(false); for (AttachmentListener a : attachmentListeners) a.onUnload(this); } /** * Exchanges the current image quickly without fading. * * @param url the URL of the new image to be displayed */ protected void quickExchangeImage(String url) { int fading = getFading(); setFading(0); exchangeImage(url, displayListener); setFading(fading); // cancelFading(false); // Image discard = passive; // passive = active; // active = new Image(); // Fade.setOpacity(active, 0.0); // active.addLoadListener(quickLoadListener); // if (discard != null) panel.remove(discard); // active.setUrl(url); // panel.add(active); // // Fade.setOpacity(passive, 1.0); // Compatibility.fireLoadOnIE(active, url, quickLoadListener, null); } /** * Adjusts the size of an image in the panel so that it fits the size of the panel. * The aspect ration of the image is preserved. * @param img the image the size of which is to be adjusted. */ private void adjustSize(Image img) { int w, h, imgW, imgH; if (img == null) return; if (sizeStep >= 0) { imgW = sizes[sizeStep][0]; imgH = sizes[sizeStep][1]; } else { imgW = img.getWidth(); imgH = img.getHeight(); } if (imgW == 0 || imgH == 0) return; w = panelW; h = imgH * panelW / imgW; if (h > panelH) { h = panelH; w = imgW * panelH / imgH; panel.setWidgetPosition(img, (panelW - w) / 2, 0); } else { panel.setWidgetPosition(img, 0, (panelH - h) / 2); } img.setPixelSize(w, h); } /** * Calls the display listener. Makes sure that the display listener * is never called twice. */ private void fireDisplay() { if (displayListener != null) { displayListener.onDisplay(); displayListener = null; } } private void fireFade() { if (displayListener != null) { displayListener.onFade(); } } /** * Cancels any ongoing image fading process and sets the opacity to the * target value. * * @param complete if true, the opacity of the image will be set to its * target value, otherwise it will be left where it is * at the moment of canceling. */ private void cancelFading(boolean complete) { if (fadeOut != null) { fadeOut.setCompleteOnCancel(complete); fadeOut.cancel(); fadeOut = null; } if (fadeIn != null) { fadeIn.setCompleteOnCancel(complete); fadeIn.cancel(); fadeIn = null; } } /** * Picks the most suitable of several steps of image sizes for the current * size of the <code>FlipImagePanel</code>. The <code>sizes</code> must be * a two dimensional array of integers containing one or more discrete steps * of width,height values. * * @param sizes an array of discrete display size steps. Each step is * described by two integer values resembling the width and * height. The size steps must be ordered from smallest to * largest. * @return the size step which is most suitable */ private int pickSize(int[][] sizes) { for (int i = 0; i < sizes.length - sizeBias; i++) { if (sizes[i][0] >= panelW || sizes[i][1] >= panelH) { return i; } } return Math.max(0, sizes.length - sizeBias - 1); // // alternative algorithm: // int ret, diff = Integer.MAX_VALUE; // for (int i = 0; i <= sizes.length - sizeBias; i++) { // int dx = sizes[i][0] - panelW; // int dy = sizes[i][0] - panelH; // int cmp = dx*dx + dy*dy; // if (cmp < diff) { // ret = i; // diff = cmp; // } // } // return Math.max(0, ret); } /** * Positions the notifier image on the panel according to the the anchor of * the notifier (<code>notifierAnchor</code>). */ private void positionNotifier() { if (notifier != null) { int w = notifier.getWidth(); int h = notifier.getHeight(); int frameX = panelW / 10; int x = panelW / 2; int y = panelH / 2 - h / 2; if (notifierAnchor == WEST) { x = panelW - w - frameX; } else if (notifierAnchor == EAST) { x = frameX; } panel.setWidgetPosition(notifier, x, y); } } /** * Starts the fading out of the last ("passive") image and the fading in * of the current ("active") image. */ private void startFading(ImageLoadHandler loadListener) { cancelFading(true); fadeIn = new NotifyingFade(active, loadListener); if (passive != null) { if (fading > 0) { fadeOut = new Fade(passive, 1.0, 0.0, FADE_OUT_STEPS); fadeIn.run(fading); } else { fadeOut = new ChainedFade(passive, fadeIn, FADE_OUT_STEPS); } fadeOut.run(Math.abs(fading)); } else { fadeIn.run(Math.abs(fading)); } fireFade(); } }