All of the code in this example is available on GitHub.
This video shows the running application. Hovering over a square highlights it. Selecting the game piece makes it transparent. When I drag, the game piece moves with the mouse and the underlying square highlights. The game piece snaps to the square when the drag operation is finished. A mouse release or movement off the board terminates the dragging.
The application's UI is built with SceneBuilder. This video walks through the FXML file used in the application and shows some of the productivity features in the tool.
The application uses two Java files: WoodlandMain.java and WoodlandController.java. The class WoodlandMain is a trivial Application extension that loads the FXML, creates a Scene, applies a stylesheet, and creates the Stage. WoodlandController is the Controller class referenced in the FXML.
Program Structure
For business applications, I add event handlers in the FXML. That is, I create an @FXML-annotated method in the Java code and assign them directly to Nodes in SceneBuilder. The business application usually needs only standard Node behavior. For games, I usually drive the event processing through toplevel handlers registered with a toplevel container node (a Pane in this example). This consolidates the updates of the various nodes (ex, dragging the game piece and highlighting the square underneath).
So, my @FXML initialize() method makes the following calls to the board Pane component.
- setOnMousePressed
- setOnMouseReleased
- setOnMouseDragged
- setOnMouseMoved
These handlers manipulate the squares and the game piece that make up the application rather than relying on handlers registered on the Nodes themselves. Because the squares and the game piece are siblings, the capture or bubble event model doesn't propagate the events.
Hovering
Moving the mouse over a square outside of the drag operation will highlight it. The highlighting is implemented with a JavaFX DropShadow Effect. The inBoard() method determines if the mouse is inside of the board. If it's not in the board, I clear any selections as a precaution. Otherwise, I use the pickSquare() method to determine the selected square and apply the DropShadow. The program will also keep track of the last hovered square using a member variable (lastHoverSquare) to un-highlight squares.
board.setOnMouseMoved((evt) -> { if( !inBoard( evt.getSceneX(), evt.getSceneY() ) ) { clearSelection(); } Optional<Rectangle> hoverSquare = pickSquare( evt.getSceneX(), evt.getSceneY() ); if( lastHoverSquare.isPresent() ) { lastHoverSquare.get().setEffect( null ); } if( hoverSquare.isPresent() ) { hoverSquare.get().setEffect(dropShadow); lastHoverSquare = hoverSquare; } });
Selecting
Pressing the mouse within the game piece turns on the transparency. A few variables (selectInImageX, topleftSelectInImageX, etc) are set recording where the selection occurs within the ImageView. This is for applying offsets later in the relocate() and boundary-checking code.
board.setOnMousePressed((evt) -> { if( selectGamePiece(evt.getSceneX(), evt.getSceneY()) ) { Point2D selectInImageScenePt = new Point2D( evt.getSceneX(), evt.getSceneY() ); Point2D selectInImagePt = gamePiece.sceneToLocal( selectInImageScenePt ); selectInImageX = gpMidpointX - selectInImagePt.getX(); selectInImageY = gpMidpointY - selectInImagePt.getY(); topLeftSelectInImageX = selectInImagePt.getX(); topLeftSelectInImageY = selectInImagePt.getY(); gamePiece.setOpacity( 0.4d ); gpSelected = true; } });
The application works in several coordinate systems: Scene, Parent, and local. The program standardizes on the Scene coordinates when doing calculations. That's to make it easier to translate among nested components. Parent coordinates are needed to apply the relocate() method. Lastly, local coordinates are easy to extract from the Nodes themselves. All through can be readily convered using Node methods like "sceneToLocal()" and "localToParent()".
You should use the relocate() method rather than directly setting the layoutX and layoutY properties.
Releasing
The post-select drag operation will be presented in the next section. After the Node is selected and dragged, a mouse release or movement off the board ends the operation and centers the game piece on the square underneath.
If there isn't a piece selected -- say we're dragging in the tan border not associated with a square -- the method will defensively clear any selection.
board.setOnMouseReleased((evt) -> { if( gpSelected ) { Optional>Rectangle< onSquare = pickSquare( evt.getSceneX(), evt.getSceneY() ); snap(onSquare); } clearSelection(); // unconditionally clear in case missed event });
Dragging
Finally, the drag operation will relocate() the gamePiece and continue the highlighting behavior. The onMouseDragged method is also responsible for terminating the drag by creating an artificial MOUSE_RELEASED event. This allows me to reuse the onMouseReleasedCode and to continue with the remainder of the drag operation.
board.setOnMouseDragged( (evt) -> { if( !inBoard( evt.getSceneX(), evt.getSceneY() ) ) { // // End the drag operation if left the board // MouseEvent releaseEvent = new MouseEvent( MouseEvent.MOUSE_RELEASED, evt.getSceneX(), evt.getSceneY(), evt.getScreenX(), evt.getScreenY(), evt.getButton(), evt.getClickCount(), evt.isShiftDown(), evt.isControlDown(), evt.isAltDown(), evt.isMetaDown(), evt.isPrimaryButtonDown(), evt.isMiddleButtonDown(), evt.isSecondaryButtonDown(), true, evt.isPopupTrigger(), evt.isStillSincePress(), evt.getPickResult() ); board.fireEvent( releaseEvent ); } if( gpSelected ) { Point2D sceneEvtPt = new Point2D(evt.getSceneX(), evt.getSceneY()); Point2D parentEvtPt = board.sceneToLocal(sceneEvtPt); Insets padding = boardContainer.getPadding(); gamePiece.relocate( parentEvtPt.getX()-padding.getLeft()+selectInImageX, parentEvtPt.getY()-padding.getTop()+selectInImageY); } Optional<Rectangle> hoverSquare = pickSquare( evt.getSceneX(), evt.getSceneY() ); if( lastHoverSquare.isPresent() ) { lastHoverSquare.get().setEffect( null ); } if( hoverSquare.isPresent() ) { hoverSquare.get().setEffect(dropShadow); lastHoverSquare = hoverSquare; } });
All of the code presented at this point is in the @FXML initialize() method.
Utility Methods
This is the code for several methods referenced in the above handlers. For more detail, you should take a look at the code on GitHub (see top of post) to review the entire class including the member variables. There are some helpful comments associated with the fields.
private void snap(Optional<Rectangle> onSquare) { if( onSquare.isPresent() ) { Point2D onSquarePt = new Point2D(onSquare.get().getLayoutX(), onSquare.get().getLayoutY()); final Timeline timeline = new Timeline(); timeline.setCycleCount(1); timeline.setAutoReverse(false); timeline.getKeyFrames() .add(new KeyFrame(Duration.millis(200), new KeyValue(gamePiece.layoutXProperty(), onSquarePt.getX()+gpCenteringX), new KeyValue(gamePiece.layoutYProperty(), onSquarePt.getY()+gpCenteringY)) ); timeline.play(); } } private void clearSelection() { selectInImageX = 0.0d; selectInImageY = 0.0d; gamePiece.setOpacity(1.0d); gpSelected = false; } private boolean selectGamePiece(double sceneX, double sceneY) { return isPicked( gamePiece, sceneX, sceneY ); } private Optional<Rectangle> pickSquare(double sceneX, double sceneY) { Optional<Rectangle> sq =squares .stream() .filter((square) -> isPicked(square, sceneX, sceneY)) .findFirst(); return sq; } private boolean inBoard(double sceneX, double sceneY) { boolean retval = ( isPicked( board, sceneX-topLeftSelectInImageX, sceneY-topLeftSelectInImageY ) && isPicked( board, sceneX+(gpWidth-topLeftSelectInImageX), sceneY+(gpHeight-topLeftSelectInImageY)) ); return retval; } private boolean isPicked(Node n, double sceneX, double sceneY) { Bounds bounds = n.getLayoutBounds(); Bounds boundsScene = n.localToScene( bounds ); if( (sceneX >= boundsScene.getMinX()) && (sceneY >= boundsScene.getMinY()) && (sceneX <= boundsScene.getMaxX()) && ( sceneY <= boundsScene.getMaxY() ) ) { return true; } return false; }
I wish more authors of this type of content would take the time you did to research and write so well. I am very impressed with your vision and insight. los angeles moving companies
ReplyDeleteThanks for every other informative site. The place else may just I get that kind of information written in such an ideal means? I have a venture that I’m just now operating on, and I have been on the look out for such information. local venice moving company
ReplyDelete