javafx.scene.image.ImageView Maven / Gradle / Ivy
/*
* Copyright (c) 2008, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.image;
import com.sun.javafx.beans.event.AbstractNotifyListener;
import com.sun.javafx.css.StyleManager;
import javafx.css.converter.URLConverter;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.scene.DirtyBits;
import com.sun.javafx.scene.ImageViewHelper;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.sg.prism.NGImageView;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.javafx.tk.Toolkit;
import javafx.beans.DefaultProperty;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.css.StyleableStringProperty;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Rectangle2D;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* The {@code ImageView} is a {@code Node} used for painting images loaded with
* {@link Image} class.
*
*
* This class allows resizing the displayed image (with or without preserving
* the original aspect ratio) and specifying a viewport into the source image
* for restricting the pixels displayed by this {@code ImageView}.
*
*
*
*
* Example code for displaying images
*
*
*
* import javafx.application.Application;
* import javafx.geometry.Rectangle2D;
* import javafx.scene.Group;
* import javafx.scene.Scene;
* import javafx.scene.image.Image;
* import javafx.scene.image.ImageView;
* import javafx.scene.layout.HBox;
* import javafx.scene.paint.Color;
* import javafx.stage.Stage;
*
* public class HelloImageView extends Application {
*
* {@literal @Override} public void start(Stage stage) {
* // load the image
* Image image = new Image("flower.png");
*
* // simple displays ImageView the image as is
* ImageView iv1 = new ImageView();
* iv1.setImage(image);
*
* // resizes the image to have width of 100 while preserving the ratio and using
* // higher quality filtering method; this ImageView is also cached to
* // improve performance
* ImageView iv2 = new ImageView();
* iv2.setImage(image);
* iv2.setFitWidth(100);
* iv2.setPreserveRatio(true);
* iv2.setSmooth(true);
* iv2.setCache(true);
*
* // defines a viewport into the source image (achieving a "zoom" effect) and
* // displays it rotated
* ImageView iv3 = new ImageView();
* iv3.setImage(image);
* Rectangle2D viewportRect = new Rectangle2D(40, 35, 110, 110);
* iv3.setViewport(viewportRect);
* iv3.setRotate(90);
*
* Group root = new Group();
* Scene scene = new Scene(root);
* scene.setFill(Color.BLACK);
* HBox box = new HBox();
* box.getChildren().add(iv1);
* box.getChildren().add(iv2);
* box.getChildren().add(iv3);
* root.getChildren().add(box);
*
* stage.setTitle("ImageView");
* stage.setWidth(415);
* stage.setHeight(200);
* stage.setScene(scene);
* stage.sizeToScene();
* stage.show();
* }
*
* public static void main(String[] args) {
* Application.launch(args);
* }
* }
*
*
* The code above produces the following:
*
*
*
*
* @since JavaFX 2.0
*/
@DefaultProperty("image")
public class ImageView extends Node {
static {
// This is used by classes in different packages to get access to
// private and package private methods.
ImageViewHelper.setImageViewAccessor(new ImageViewHelper.ImageViewAccessor() {
@Override
public NGNode doCreatePeer(Node node) {
return ((ImageView) node).doCreatePeer();
}
@Override
public void doUpdatePeer(Node node) {
((ImageView) node).doUpdatePeer();
}
@Override
public BaseBounds doComputeGeomBounds(Node node,
BaseBounds bounds, BaseTransform tx) {
return ((ImageView) node).doComputeGeomBounds(bounds, tx);
}
@Override
public boolean doComputeContains(Node node, double localX, double localY) {
return ((ImageView) node).doComputeContains(localX, localY);
}
});
}
{
// To initialize the class helper at the begining each constructor of this class
ImageViewHelper.initHelper(this);
}
/**
* Allocates a new ImageView object.
*/
public ImageView() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setAccessibleRole(AccessibleRole.IMAGE_VIEW);
setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
}
/**
* Allocates a new ImageView object with image loaded from the specified
* URL.
*
* The {@code new ImageView(url)} has the same effect as
* {@code new ImageView(new Image(url))}.
*
*
* @param url the string representing the URL from which to load the image
* @throws NullPointerException if URL is null
* @throws IllegalArgumentException if URL is invalid or unsupported
* @since JavaFX 2.1
*/
public ImageView(String url) {
this(new Image(url));
}
/**
* Allocates a new ImageView object using the given image.
*
* @param image Image that this ImageView uses
*/
public ImageView(Image image) {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setAccessibleRole(AccessibleRole.IMAGE_VIEW);
setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
setImage(image);
}
/**
* The {@link Image} to be painted by this {@code ImageView}.
*
* @defaultValue null
*/
private ObjectProperty image;
public final void setImage(Image value) {
imageProperty().set(value);
}
public final Image getImage() {
return image == null ? null : image.get();
}
private Image oldImage;
public final ObjectProperty imageProperty() {
if (image == null) {
image = new ObjectPropertyBase() {
private boolean needsListeners = false;
@Override
public void invalidated() {
Image _image = get();
boolean dimensionChanged = _image == null || oldImage == null ||
(oldImage.getWidth() != _image.getWidth() ||
oldImage.getHeight() != _image.getHeight());
if (needsListeners) {
Toolkit.getImageAccessor().getImageProperty(oldImage).
removeListener(platformImageChangeListener.getWeakListener());
}
needsListeners = _image != null && (_image.isAnimation() || _image.getProgress() < 1);
oldImage = _image;
if (needsListeners) {
Toolkit.getImageAccessor().getImageProperty(_image).
addListener(platformImageChangeListener.getWeakListener());
}
if (dimensionChanged) {
invalidateWidthHeight();
NodeHelper.geomChanged(ImageView.this);
}
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_CONTENTS);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "image";
}
};
}
return image;
}
private StringProperty imageUrl = null;
/**
* The imageUrl property is set from CSS and then the image property is
* set from the invalidated method. This ensures that the same image isn't
* reloaded.
*/
private StringProperty imageUrlProperty() {
if (imageUrl == null) {
imageUrl = new StyleableStringProperty() {
@Override
protected void invalidated() {
final String imageUrl = get();
if (imageUrl != null) {
setImage(StyleManager.getInstance().getCachedImage(imageUrl));
} else {
setImage(null);
}
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "imageUrl";
}
@Override
public CssMetaData getCssMetaData() {
return StyleableProperties.IMAGE;
}
};
}
return imageUrl;
}
private final AbstractNotifyListener platformImageChangeListener =
new AbstractNotifyListener() {
@Override
public void invalidated(Observable valueModel) {
invalidateWidthHeight();
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_CONTENTS);
NodeHelper.geomChanged(ImageView.this);
}
};
/**
* The current x coordinate of the {@code ImageView} origin.
*
* @defaultValue 0
*/
private DoubleProperty x;
public final void setX(double value) {
xProperty().set(value);
}
public final double getX() {
return x == null ? 0.0 : x.get();
}
public final DoubleProperty xProperty() {
if (x == null) {
x = new DoublePropertyBase() {
@Override
protected void invalidated() {
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_GEOMETRY);
NodeHelper.geomChanged(ImageView.this);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "x";
}
};
}
return x;
}
/**
* The current y coordinate of the {@code ImageView} origin.
*
* @defaultValue 0
*/
private DoubleProperty y;
public final void setY(double value) {
yProperty().set(value);
}
public final double getY() {
return y == null ? 0.0 : y.get();
}
public final DoubleProperty yProperty() {
if (y == null) {
y = new DoublePropertyBase() {
@Override
protected void invalidated() {
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_GEOMETRY);
NodeHelper.geomChanged(ImageView.this);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "y";
}
};
}
return y;
}
/**
* The width of the bounding box within which the source image is resized as
* necessary to fit. If set to a value <= 0, then the intrinsic width of the
* image will be used as the {@code fitWidth}.
*
* See {@link #preserveRatioProperty() preserveRatio} for information on interaction between image
* view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio}
* attributes.
*
* @defaultValue 0
*/
private DoubleProperty fitWidth;
public final void setFitWidth(double value) {
fitWidthProperty().set(value);
}
public final double getFitWidth() {
return fitWidth == null ? 0.0 : fitWidth.get();
}
public final DoubleProperty fitWidthProperty() {
if (fitWidth == null) {
fitWidth = new DoublePropertyBase() {
@Override
protected void invalidated() {
invalidateWidthHeight();
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT);
NodeHelper.geomChanged(ImageView.this);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "fitWidth";
}
};
}
return fitWidth;
}
/**
* The height of the bounding box within which the source image is resized
* as necessary to fit. If set to a value <= 0, then the intrinsic height of
* the image will be used as the {@code fitHeight}.
*
* See {@link #preserveRatioProperty() preserveRatio} for information on interaction between image
* view's {@code fitWidth}, {@code fitHeight} and {@code preserveRatio}
* attributes.
*
*
* @defaultValue 0
*/
private DoubleProperty fitHeight;
public final void setFitHeight(double value) {
fitHeightProperty().set(value);
}
public final double getFitHeight() {
return fitHeight == null ? 0.0 : fitHeight.get();
}
public final DoubleProperty fitHeightProperty() {
if (fitHeight == null) {
fitHeight = new DoublePropertyBase() {
@Override
protected void invalidated() {
invalidateWidthHeight();
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT);
NodeHelper.geomChanged(ImageView.this);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "fitHeight";
}
};
}
return fitHeight;
}
/**
* Indicates whether to preserve the aspect ratio of the source image when
* scaling to fit the image within the fitting bounding box.
*
* If set to {@code true}, it affects the dimensions of this
* {@code ImageView} in the following way
*
* - If only {@code fitWidth} is set, height is scaled to preserve ratio
*
- If only {@code fitHeight} is set, width is scaled to preserve ratio
*
- If both are set, they both may be scaled to get the best fit in a
* width by height rectangle while preserving the original aspect ratio
*
*
* If unset or set to {@code false}, it affects the dimensions of this
* {@code ImageView} in the following way
*
* - If only {@code fitWidth} is set, image's view width is scaled to
* match and height is unchanged;
*
- If only {@code fitHeight} is set, image's view height is scaled to
* match and height is unchanged;
*
- If both are set, the image view is scaled to match both.
*
*
* Note that the dimensions of this node as reported by the node's bounds
* will be equal to the size of the scaled image and is guaranteed to be
* contained within {@code fitWidth x fitHeight} bonding box.
*
* @defaultValue false
*/
private BooleanProperty preserveRatio;
public final void setPreserveRatio(boolean value) {
preserveRatioProperty().set(value);
}
public final boolean isPreserveRatio() {
return preserveRatio == null ? false : preserveRatio.get();
}
public final BooleanProperty preserveRatioProperty() {
if (preserveRatio == null) {
preserveRatio = new BooleanPropertyBase() {
@Override
protected void invalidated() {
invalidateWidthHeight();
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT);
NodeHelper.geomChanged(ImageView.this);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "preserveRatio";
}
};
}
return preserveRatio;
}
/**
* Indicates whether to use a better quality filtering algorithm or a faster
* one when transforming or scaling the source image to fit within the
* bounding box provided by {@code fitWidth} and {@code fitHeight}.
*
*
* If set to {@code true} a better quality filtering will be used, if set to
* {@code false} a faster but lesser quality filtering will be used.
*
*
*
* The default value depends on platform configuration.
*
*
* @defaultValue platform-dependent
*/
private BooleanProperty smooth;
public final void setSmooth(boolean value) {
smoothProperty().set(value);
}
public final boolean isSmooth() {
return smooth == null ? SMOOTH_DEFAULT : smooth.get();
}
public final BooleanProperty smoothProperty() {
if (smooth == null) {
smooth = new BooleanPropertyBase(SMOOTH_DEFAULT) {
@Override
protected void invalidated() {
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_SMOOTH);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "smooth";
}
};
}
return smooth;
}
/**
* Platform-dependent default value of the {@link #smoothProperty() smooth} property.
*/
public static final boolean SMOOTH_DEFAULT = Toolkit.getToolkit()
.getDefaultImageSmooth();
/**
* The rectangular viewport into the image. The viewport is specified in the
* coordinates of the image, prior to scaling or any other transformations.
*
*
* If {@code viewport} is {@code null}, the entire image is displayed. If
* {@code viewport} is non-{@code null}, only the portion of the image which
* falls within the viewport will be displayed. If the image does not fully
* cover the viewport then any remaining area of the viewport will be empty.
*
*
* @defaultValue null
*/
private ObjectProperty viewport;
public final void setViewport(Rectangle2D value) {
viewportProperty().set(value);
}
public final Rectangle2D getViewport() {
return viewport == null ? null : viewport.get();
}
public final ObjectProperty viewportProperty() {
if (viewport == null) {
viewport = new ObjectPropertyBase() {
@Override
protected void invalidated() {
invalidateWidthHeight();
NodeHelper.markDirty(ImageView.this, DirtyBits.NODE_VIEWPORT);
NodeHelper.geomChanged(ImageView.this);
}
@Override
public Object getBean() {
return ImageView.this;
}
@Override
public String getName() {
return "viewport";
}
};
}
return viewport;
}
// Need to track changes to image width and image height and recompute
// bounds when changed.
// imageWidth = bind image.width on replace {
// NodeHelper.geomChanged(ImageView.this);
// }
//
// imageHeight = bind image.height on replace {
// NodeHelper.geomChanged(ImageView.this);
// }
private double destWidth, destHeight;
/*
* Note: This method MUST only be called via its accessor method.
*/
private NGNode doCreatePeer() {
return new NGImageView();
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
recomputeWidthHeight();
bounds = bounds.deriveWithNewBounds((float)getX(), (float)getY(), 0.0f,
(float)(getX() + destWidth), (float)(getY() + destHeight), 0.0f);
bounds = tx.transform(bounds, bounds);
return bounds;
}
private boolean validWH;
private void invalidateWidthHeight() {
validWH = false;
}
private void recomputeWidthHeight() {
if (validWH) {
return;
}
Image localImage = getImage();
Rectangle2D localViewport = getViewport();
double w = 0;
double h = 0;
if (localViewport != null && localViewport.getWidth() > 0 && localViewport.getHeight() > 0) {
w = localViewport.getWidth();
h = localViewport.getHeight();
} else if (localImage != null) {
w = localImage.getWidth();
h = localImage.getHeight();
}
double localFitWidth = getFitWidth();
double localFitHeight = getFitHeight();
if (isPreserveRatio() && w > 0 && h > 0 && (localFitWidth > 0 || localFitHeight > 0)) {
if (localFitWidth <= 0 || (localFitHeight > 0 && localFitWidth * h > localFitHeight * w)) {
w = w * localFitHeight / h;
h = localFitHeight;
} else {
h = h * localFitWidth / w;
w = localFitWidth;
}
} else {
if (localFitWidth > 0f) {
w = localFitWidth;
}
if (localFitHeight > 0f) {
h = localFitHeight;
}
}
// Store these values for use later in doComputeContains() to support
// Node.contains().
destWidth = w;
destHeight = h;
validWH = true;
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private boolean doComputeContains(double localX, double localY) {
if (getImage() == null) {
return false;
}
recomputeWidthHeight();
// Local Note bounds contain test is already done by the caller.
// (Node.contains()).
double dx = localX - getX();
double dy = localY - getY();
Image localImage = getImage();
double srcWidth = localImage.getWidth();
double srcHeight = localImage.getHeight();
double viewWidth = srcWidth;
double viewHeight = srcHeight;
double vw = 0;
double vh = 0;
double vminx = 0;
double vminy = 0;
Rectangle2D localViewport = getViewport();
if (localViewport != null) {
vw = localViewport.getWidth();
vh = localViewport.getHeight();
vminx = localViewport.getMinX();
vminy = localViewport.getMinY();
}
if (vw > 0 && vh > 0) {
viewWidth = vw;
viewHeight = vh;
}
// desWidth Note and destHeight are computed by NodeHelper.computeGeomBounds()
// via a call from Node.contains() before calling
// doComputeContains().
// Transform into image's coordinate system.
dx = vminx + dx * viewWidth / destWidth;
dy = vminy + dy * viewHeight / destHeight;
// test whether it's inside the original image AND inside of viewport
// (viewport may stick out from the image bounds)
if (dx < 0.0 || dy < 0.0 || dx >= srcWidth || dy >= srcHeight ||
dx < vminx || dy < vminy ||
dx >= vminx + viewWidth || dy >= vminy + viewHeight) {
return false;
}
// Do alpha test on the picked pixel.
return Toolkit.getToolkit().imageContains(
Toolkit.getImageAccessor().getPlatformImage(localImage), (float)dx, (float)dy);
}
/* *************************************************************************
* * Stylesheet Handling * *
**************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "image-view";
/*
* Super-lazy instantiation pattern from Bill Pugh.
*/
private static class StyleableProperties {
// TODO
// "preserve-ratio","smooth","viewport","fit-width","fit-height"
private static final CssMetaData IMAGE =
new CssMetaData("-fx-image",
URLConverter.getInstance()) {
@Override
public boolean isSettable(ImageView n) {
// Note that we care about the image, not imageUrl
return n.image == null || !n.image.isBound();
}
@Override
public StyleableProperty getStyleableProperty(ImageView n) {
return (StyleableProperty)n.imageUrlProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables =
new ArrayList>(Node.getClassCssMetaData());
styleables.add(IMAGE);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* Gets the {@code CssMetaData} associated with this class, which may include the
* {@code CssMetaData} of its superclasses.
* @return the {@code CssMetaData}
* @since JavaFX 8.0
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
* @return the CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
* @since JavaFX 8.0
*/
@Override
public List> getCssMetaData() {
return getClassCssMetaData();
}
void updateViewport() {
recomputeWidthHeight();
if (getImage() == null || Toolkit.getImageAccessor().getPlatformImage(getImage()) == null) {
return;
}
Rectangle2D localViewport = getViewport();
final NGImageView peer = NodeHelper.getPeer(this);
if (localViewport != null) {
peer.setViewport((float)localViewport.getMinX(), (float)localViewport.getMinY(),
(float)localViewport.getWidth(), (float)localViewport.getHeight(),
(float)destWidth, (float)destHeight);
} else {
peer.setViewport(0, 0, 0, 0, (float)destWidth, (float)destHeight);
}
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private void doUpdatePeer() {
final NGImageView peer = NodeHelper.getPeer(this);
if (NodeHelper.isDirty(this, DirtyBits.NODE_GEOMETRY)) {
peer.setX((float)getX());
peer.setY((float)getY());
}
if (NodeHelper.isDirty(this, DirtyBits.NODE_SMOOTH)) {
peer.setSmooth(isSmooth());
}
if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) {
peer.setImage(getImage() != null
? Toolkit.getImageAccessor().getPlatformImage(getImage()) : null);
}
// The NG part expects this to be called when image changes
if (NodeHelper.isDirty(this, DirtyBits.NODE_VIEWPORT) || NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) {
updateViewport();
}
}
}