From 3875409d9422385a5e5746ab8f646a16a274d08c Mon Sep 17 00:00:00 2001 From: Dirk Lemmermann Date: Thu, 29 Aug 2024 16:06:50 +0200 Subject: [PATCH] More drawer greatness. --- .../META-INF/native-image/reflect-config.json | 3 + .../mobile/components/BottomMenuBar.java | 8 +- .../mobile/pages/MobileHomePage.java | 43 +----- .../jfxcentral2/mobile/skin/MainPageSkin.java | 140 ++++++++++++++++-- .../com/dlsc/jfxcentral2/mobile/mobile.css | 29 ++-- 5 files changed, 161 insertions(+), 62 deletions(-) diff --git a/app/src/main/resources/META-INF/native-image/reflect-config.json b/app/src/main/resources/META-INF/native-image/reflect-config.json index ea342a69..7fb3b753 100644 --- a/app/src/main/resources/META-INF/native-image/reflect-config.json +++ b/app/src/main/resources/META-INF/native-image/reflect-config.json @@ -651,6 +651,9 @@ { "name":"java.util.Date" }, +{ + "name":"javafx.animation.KeyValue" +}, { "name":"javafx.scene.Camera" }, diff --git a/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/components/BottomMenuBar.java b/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/components/BottomMenuBar.java index e21d57cb..122fe7f5 100644 --- a/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/components/BottomMenuBar.java +++ b/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/components/BottomMenuBar.java @@ -7,6 +7,7 @@ import com.dlsc.jfxcentral2.components.CustomToggleButton; import com.dlsc.jfxcentral2.components.SizeSupport; import com.dlsc.jfxcentral2.events.MobileLinkEvent; +import com.dlsc.jfxcentral2.mobile.pages.MainPage; import com.dlsc.jfxcentral2.mobile.pages.MobileHomePage; import com.dlsc.jfxcentral2.model.Size; import com.dlsc.jfxcentral2.utils.EventBusUtil; @@ -28,7 +29,7 @@ public class BottomMenuBar extends HBox { private final SizeSupport sizeSupport = new SizeSupport(this); private final ToggleGroup toggleGroup; - public BottomMenuBar() { + public BottomMenuBar(Runnable closeDrawerCallback) { getStyleClass().add("bottom-menu-bar"); EventBusUtil.register(this); @@ -39,6 +40,7 @@ public BottomMenuBar() { homeButton.setMaxWidth(Double.MAX_VALUE); homeButton.setUserData(PagePath.HOME); homeButton.setOnMousePressed(evt -> { + closeDrawerCallback.run(); if (homeButton.isSelected()) { // If the home page is being displayed, clicking homeButton will hide the search view and display the normal content. MobileHomePage.getInstance().setContentType(MobileHomePage.ContentType.NORMAL); @@ -54,6 +56,7 @@ public BottomMenuBar() { linksWeekButton.setGraphic(new FontIcon(IkonUtil.getModelIkon(LinksOfTheWeek.class))); linksWeekButton.setMaxWidth(Double.MAX_VALUE); linksWeekButton.setUserData(PagePath.LINKS); + linksWeekButton.setOnMouseClicked(evt -> closeDrawerCallback.run()); MobileLinkUtil.setLink(linksWeekButton, PagePath.LINKS); HBox.setHgrow(linksWeekButton, Priority.ALWAYS); @@ -63,6 +66,7 @@ public BottomMenuBar() { showcasesButton.setGraphic(new FontIcon(IkonUtil.getModelIkon(RealWorldApp.class))); showcasesButton.setMaxWidth(Double.MAX_VALUE); showcasesButton.setUserData(PagePath.SHOWCASES); + showcasesButton.setOnMouseClicked(evt -> closeDrawerCallback.run()); MobileLinkUtil.setLink(showcasesButton, PagePath.SHOWCASES); HBox.setHgrow(showcasesButton, Priority.ALWAYS); @@ -72,6 +76,7 @@ public BottomMenuBar() { libraryButton.setGraphic(new FontIcon(IkonUtil.getModelIkon(Library.class))); libraryButton.setMaxWidth(Double.MAX_VALUE); libraryButton.setUserData(PagePath.LIBRARIES); + libraryButton.setOnMouseClicked(evt -> closeDrawerCallback.run()); MobileLinkUtil.setLink(libraryButton, PagePath.LIBRARIES); HBox.setHgrow(libraryButton, Priority.ALWAYS); @@ -81,6 +86,7 @@ public BottomMenuBar() { peopleButton.setGraphic(new FontIcon(IkonUtil.getModelIkon(Person.class))); peopleButton.setMaxWidth(Double.MAX_VALUE); peopleButton.setUserData(PagePath.PEOPLE); + peopleButton.setOnMouseClicked(evt -> closeDrawerCallback.run()); MobileLinkUtil.setLink(peopleButton, PagePath.PEOPLE); HBox.setHgrow(peopleButton, Priority.ALWAYS); diff --git a/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/pages/MobileHomePage.java b/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/pages/MobileHomePage.java index 11935c11..a359e7cc 100644 --- a/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/pages/MobileHomePage.java +++ b/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/pages/MobileHomePage.java @@ -21,15 +21,15 @@ import com.dlsc.jfxcentral2.mobile.home.WeekLinksView; import com.dlsc.jfxcentral2.utils.PagePath; import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; import javafx.scene.control.ToggleButton; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; @@ -87,10 +87,11 @@ private MobileHomePage() { searchView.searchTextProperty().bindBidirectional(searchTextField.textProperty()); HBox.setHgrow(searchTextField, Priority.ALWAYS); + HBox searchWrapper = new HBox(searchTextField); searchWrapper.getStyleClass().add("search-wrapper"); - getChildren().addAll(header, searchWrapper, normalView, searchView); + getChildren().addAll(searchWrapper, normalView, searchView); setViewWillAppear(()-> setContentType(ContentType.NORMAL)); } @@ -104,6 +105,7 @@ private Button createSearchCancelButton() { return "Search"; } }, contentTypeProperty())); + button.setOnAction(event -> { if (getContentType() == ContentType.SEARCH) { searchTextField.clear(); @@ -112,6 +114,7 @@ private Button createSearchCancelButton() { setContentType(ContentType.SEARCH); } }); + return button; } @@ -163,38 +166,7 @@ private Node createNormalView() { scrollPane.getStyleClass().add("mobile"); VBox.setVgrow(scrollPane, Priority.ALWAYS); - Node drawerContent = createDrawerContent(); - StackPane.setAlignment(drawerContent, Pos.BOTTOM_CENTER); - - return new StackPane(scrollPane, drawerContent); - } - - private Node createDrawerContent() { - ToggleButton title = new ToggleButton("Content"); - title.setMaxWidth(Double.MAX_VALUE); - title.getStyleClass().add("title"); - - final int HEIGHT = 200; - - CategoriesPane content = new CategoriesPane(); - content.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - content.setAlignment(Pos.CENTER); - content.setPrefHeight(HEIGHT); - VBox.setVgrow(content, Priority.ALWAYS); - - VBox drawer = new VBox(title, content); - drawer.getStyleClass().add("drawer"); - drawer.setMaxHeight(Region.USE_PREF_SIZE); - - drawer.translateYProperty().bind(Bindings.createDoubleBinding(() -> title.isSelected() ? 0d: HEIGHT, title.selectedProperty())); - - Rectangle clip = new Rectangle(); - clip.widthProperty().bind(drawer.widthProperty()); - clip.heightProperty().bind(drawer.heightProperty().add(50)); - clip.setLayoutY(-50); - drawer.setClip(clip); - - return drawer; + return scrollPane; } private List getRandomSample(List list, int sampleSize) { @@ -222,5 +194,4 @@ public final ObjectProperty contentTypeProperty() { public final void setContentType(ContentType contentType) { this.contentType.set(contentType); } - } diff --git a/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/skin/MainPageSkin.java b/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/skin/MainPageSkin.java index 376aee26..2ea1181d 100644 --- a/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/skin/MainPageSkin.java +++ b/mobile/src/main/java/com/dlsc/jfxcentral2/mobile/skin/MainPageSkin.java @@ -1,52 +1,166 @@ package com.dlsc.jfxcentral2.mobile.skin; +import com.dlsc.gemsfx.GlassPane; import com.dlsc.jfxcentral2.components.MobilePageBase; import com.dlsc.jfxcentral2.events.MobileResponseEvent; import com.dlsc.jfxcentral2.events.RepositoryUpdatedEvent; import com.dlsc.jfxcentral2.mobile.components.BottomMenuBar; +import com.dlsc.jfxcentral2.mobile.pages.CategoriesPane; import com.dlsc.jfxcentral2.mobile.pages.MainPage; import com.dlsc.jfxcentral2.mobile.utils.PreferredFocusedNodeProvider; import com.dlsc.jfxcentral2.utils.EventBusUtil; import com.dlsc.jfxcentral2.utils.Subscribe; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Label; import javafx.scene.control.SkinBase; -import javafx.scene.layout.BorderPane; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.shape.Rectangle; +import javafx.util.Duration; import java.util.Optional; import java.util.function.Function; public class MainPageSkin extends SkinBase { - private final BorderPane borderPane = new BorderPane(); - private final BottomMenuBar bottomMenuBar = new BottomMenuBar(); - private final StackPane centerPane = new StackPane(); + private final BottomMenuBar bottomMenuBar; + private final StackPane contentPane = new StackPane(); + private final GlassPane glassPane = new GlassPane(); + private final Node drawer; + private StackPane drawerHandle; + + private final DoubleProperty drawerHeightPercentage = new SimpleDoubleProperty(); + private CategoriesPane content; + private Timeline timeline; public MainPageSkin(MainPage control) { super(control); + EventBusUtil.register(this); - bottomMenuBar.managedProperty().bind(bottomMenuBar.visibleProperty()); + bottomMenuBar = new BottomMenuBar(() -> hideDrawer()); bottomMenuBar.setVisible(false); - centerPane.getStyleClass().add("content-pane"); + drawer = createDrawerContent(); + + contentPane.getStyleClass().add("content-pane"); + + contentPane.setManaged(false); + bottomMenuBar.setManaged(false); + glassPane.setManaged(false); + drawer.setManaged(false); + + glassPane.setOnMouseClicked(evt -> hideDrawer()); + glassPane.hideProperty().bind(drawerHeightPercentage.isEqualTo(0)); + + getChildren().setAll(contentPane, bottomMenuBar, glassPane, drawer); + + drawerHeightPercentage.addListener(it -> getSkinnable().requestLayout()); + } + + @Override + protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { + double barHeight = bottomMenuBar.prefHeight(contentWidth); + bottomMenuBar.resizeRelocate(contentX, contentY + contentHeight - barHeight, contentWidth, barHeight); + double drawerHeight = drawerHeightPercentage.get() * content.prefHeight(-1) + drawerHandle.prefHeight(-1); + drawer.resizeRelocate(contentX, contentY + contentHeight - barHeight - drawerHeight, contentWidth, drawerHeight); + contentPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight - barHeight - drawerHeight); + glassPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight - barHeight - drawerHeight); + } + + private Node createDrawerContent() { + Region region = new Region(); + region.getStyleClass().add("region"); + region.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + StackPane.setAlignment(region, Pos.CENTER); + + drawerHandle = new StackPane(region); + drawerHandle.setMaxWidth(Double.MAX_VALUE); + drawerHandle.setMinHeight(Region.USE_PREF_SIZE); + drawerHandle.getStyleClass().add("handle"); + drawerHandle.setOnMouseClicked(evt -> { + if (drawerHeightPercentage.get() > 0) { + hideDrawer(); + } else { + showDrawer(); + } + }); + + content = new CategoriesPane(); + content.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + content.setAlignment(Pos.CENTER); + content.setMouseTransparent(false); + VBox.setVgrow(content, Priority.ALWAYS); + + installCloseDrawerHandler(content); + + VBox drawer = new VBox(drawerHandle, content); + drawer.getStyleClass().add("drawer"); + drawer.setMaxHeight(Region.USE_PREF_SIZE); - borderPane.setCenter(centerPane); - borderPane.setBottom(bottomMenuBar); - getChildren().add(borderPane); + Rectangle clip = new Rectangle(); + clip.widthProperty().bind(drawer.widthProperty()); + clip.heightProperty().bind(drawer.heightProperty().add(50)); + clip.setLayoutY(-50); + drawer.setClip(clip); + + return drawer; + } + + private void installCloseDrawerHandler(Node node) { + node.addEventFilter(MouseEvent.MOUSE_CLICKED, evt -> { + if (evt.getButton().equals(MouseButton.PRIMARY) && evt.isStillSincePress()) { + hideDrawer(); + } + }); + } + + private void hideDrawer() { + animateDrawer(0); + } + + private void showDrawer() { + animateDrawer(1); + } + + private void animateDrawer(double value) { + if (timeline != null && timeline.getStatus().equals(Animation.Status.RUNNING)) { + timeline.stop(); + } + + KeyValue keyValue = new KeyValue(drawerHeightPercentage, value); + KeyFrame keyFrame = new KeyFrame(Duration.millis(100), keyValue); + timeline = new Timeline(keyFrame); + timeline.play(); } @Subscribe public void onMobileResponseEvent(MobileResponseEvent event) { + Node oldView = null; + // Old view will disappear - Node oldView = borderPane.getCenter(); - invokeLifecycleMethod(oldView, MobilePageBase::getViewWillDisappear); + if (!contentPane.getChildren().isEmpty()) { + oldView = contentPane.getChildren().get(0); + invokeLifecycleMethod(oldView, MobilePageBase::getViewWillDisappear); + } // New view will appear Node newView = event.mobileResponse().getView(); invokeLifecycleMethod(newView, MobilePageBase::getViewWillAppear); - centerPane.getChildren().setAll(newView); + contentPane.getChildren().setAll(newView); // Old view did disappear invokeLifecycleMethod(oldView, MobilePageBase::getViewDidDisappear); @@ -62,7 +176,7 @@ public void onMobileResponseEvent(MobileResponseEvent event) { } } } - + private void invokeLifecycleMethod(Node view, Function eventFunction) { if (view instanceof MobilePageBase mobilePage) { Optional.ofNullable(eventFunction.apply(mobilePage)).ifPresent(Runnable::run); diff --git a/mobile/src/main/resources/com/dlsc/jfxcentral2/mobile/mobile.css b/mobile/src/main/resources/com/dlsc/jfxcentral2/mobile/mobile.css index f7028897..20a0eb31 100644 --- a/mobile/src/main/resources/com/dlsc/jfxcentral2/mobile/mobile.css +++ b/mobile/src/main/resources/com/dlsc/jfxcentral2/mobile/mobile.css @@ -553,34 +553,39 @@ -fx-alignment: center-left; } -.mobile-home-page .drawer { +/** ---------------------------------- + * MainPage + */ +.main-page .drawer { -fx-background-color: -background; -fx-background-radius: 32px 32px 0px 0px; - -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.25), 30, 0.2, 0, 0); /* 20% alpha */; } -.mobile-home-page .drawer > .title { - -fx-background-radius: 32px 32px 0px 0px; +.main-page .drawer > .handle { -fx-background-color: -background; - -fx-padding: 10px; - -fx-font-size: 1.2em; - -fx-font-family: "Roboto Condensed"; - -fx-font-weight: bold; + -fx-padding: 6px 0px; } -.mobile-home-page .drawer > .categories-pane { +.main-page .drawer > .handle > .region { + -fx-pref-width: 50px; + -fx-pref-height: 4px; + -fx-background-radius: 8px; + -fx-background-color: -grey-30; +} + +.main-page .drawer > .categories-pane { -fx-font-size: 12px; } -.mobile-home-page .drawer > .categories-pane > .button { +.main-page .drawer > .categories-pane > .button { -fx-content-display: top; -fx-background-color: transparent; -fx-text-fill: -grey-60; - -fx-padding: 0 0 5px 0; + -fx-padding: 10px 0px; -fx-graphic-text-gap: 0px; } -.mobile-home-page .drawer > .categories-pane > .button .ikonli-font-icon { +.main-page .drawer > .categories-pane > .button .ikonli-font-icon { -fx-icon-size: 24px; -fx-icon-color: -grey-60; }