Gluon Mobile Cardpane UI 增强功能:Cardcell Generation/Deletion 和 Cardpane 样式

Gluon Mobile Cardpane UI Enhancements: Cardcell Generation/Deletion & Cardpane Styling

我正在尝试使用自定义 HBox CardCells 创建卡片窗格。

第 1 期

如何设置这个CardPane的背景?我希望它是透明的,但它不会从这种灰色改变。我尝试直接向节点添加样式以及添加自定义样式表。我也试过 setBackground 方法:

第 2 期

取自 this SO post,我能够为细胞生成添加一个向上淡入淡出的动画。然而,在随机卡片插入中,不同的单元格丢失了我嵌入到该单元格中的节点。我不知道这是因为这些卡片的回收概念(基于 Gluon 文档)还是什么:

第 3 期

我创建了这样的功能,即用户可以通过向左滑动来删除卡片。然而,问题 #2 出现了同样的问题,但更大程度上是整个单元格丢失但仍然占用 space。如果我只有一个单元格并向左滑动,它会一直有效。但是,当我有多个单元格时(例如,我有 3 个单元格,我删除了第二个单元格),事情就坏了,单元格的事件处理程序被删除,在一个单元格上向左滑动会在它下面的单元格上启动动画,等等。有没有一种方法可以执行此功能,或者我最好的选择是摆脱 CardPane 并结合使用 VBox 和 HBox 元素?

private void addToCardPane(CustomCard newCard) {

    ObservableList<Node> items = cardpane.getItems();
    boolean override = false;

    for (int i = 0; i < cardpane.getItems().size(); i++) {
        CustomCard box = (CustomCard) items.get(i);
        if (box.checkEquality(newCard)) {
            box.increaseNumber(newCard);
            override = true;
            break;
        }
    }

    if (override == false) {
        cardpane.getItems().add(newCard);
        cardpane.layout();
        VirtualFlow vf = (VirtualFlow) cardpane.lookup(".virtual-flow");
        Node cell = vf.getCell(cardpane.getItems().size() - 1);
        cell.setTranslateX(0);
        cell.setOpacity(1.0);
        if (!cardpane.lookup(".scroll-bar").isVisible()) {
            FadeInUpTransition f = new FadeInUpTransition(cell);
            f.setRate(2);
            f.play();
        } else {
            PauseTransition p = new PauseTransition(Duration.millis(20));
            p.setOnFinished(e -> {
                vf.getCell(cardpane.getItems().size() - 1).setOpacity(0);
                vf.show(cardpane.getItems().size() - 1);
                FadeTransition f = new FadeTransition();
                f.setDuration(Duration.seconds(1));
                f.setFromValue(0);
                f.setToValue(1);
                f.setNode(vf.getCell(cardpane.getItems().size() - 1));
                f.setOnFinished(t -> {
                });
                f.play();
            });
            p.play();
        }
    }  
    initializeDeletionLogic();
}

private void initializeDeletionLogic() {
    VirtualFlow vf = (VirtualFlow) cardpane.lookup(".virtual-flow");
    for (int i = 0; i < cardpane.getItems().size(); i++) {
        CustomCard card = (CustomCard ) cardpane.getItems().get(i);
        Node cell2 = vf.getCell(i);
        addRemovalLogicForCell(card, cell2);
    }
}

private static double initX = 0;
private void addRemovalLogicForCell(OpioidCard card, Node cell) {
    card.setOnMousePressed(e -> {
        initX = e.getX();
    });

    card.setOnMouseDragged(e -> {
        double current = e.getX();
        if (current < initX) {
            if ((current - initX) < 0 && (current - initX) > -50) {
                cell.setTranslateX(current - initX);
            }
        }
    });

    card.setOnMouseReleased(e -> {
        double current = e.getX();
        double delta = current - initX;
        System.out.println(delta);
        if (delta > -50) {
            int originalMillis = 500;
            double ratio = (50 - delta) / 50;
            int newMillis = (int) (500 * ratio);
            TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
            translate.setToX(0);
            translate.setNode(cell);
            translate.play();
        } else {
            FadeTransition ft = new FadeTransition(Duration.millis(300), cell);
            ft.setFromValue(1.0);
            ft.setToValue(0);

            TranslateTransition translateTransition
                    = new TranslateTransition(Duration.millis(300), cell);
            translateTransition.setFromX(cell.getTranslateX());
            translateTransition.setToX(-400);

            ParallelTransition parallel = new ParallelTransition();
            parallel.getChildren().addAll(ft, translateTransition);
            parallel.setOnFinished(evt -> {

                removeCard(card);

                ObservableList<CustomCard > cells = FXCollections.observableArrayList();

                for(int i = 0; i < this.cardpane.getItems().size(); i++){
                    cells.add((CustomCard )this.cardpane.getItems().get(i));
                }

                this.cardpane.getItems().clear();
                for(int i = 0; i < cells.size(); i++){
                    this.cardpane.getItems().add(cells.get(i));
                }

                initializeDeletionLogic();
                initX = 0;
            });

            parallel.play();
        }
    });
}

private void removeCard(OpioidCard card) {
    for (int i = 0; i < cardpane.getItems().size(); i++) {
        if (cardpane.getItems().get(i) == card) {
            cardpane.getItems().remove(i);
            updateNumber(this.totalNumber);
            break;
        }
    }

    for (int i = 0; i < dataList.size(); i++) {
        if (dataList.get(i).getName().equalsIgnoreCase(card.getName())) {
            dataList.remove(i);
        }
    }

    this.cardpane.layout();
    initializeDeletionLogic();
}

问题的工作演示:

package com.mobiletestapp;

import com.gluonhq.charm.glisten.animation.FadeInUpTransition;
import com.gluonhq.charm.glisten.control.AppBar;
import com.gluonhq.charm.glisten.control.CardCell;
import com.gluonhq.charm.glisten.control.CardPane;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.animation.FadeTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;

public class BasicView extends View {

    class CustomCard extends StackPane{
        public CustomCard(String text){
            this.getChildren().add(new Label(text));
        }  
    }
    private static double initX = 0;
    private static void addRemovalLogicForCell(CustomCard card, Node cell) {
        card.setOnMousePressed(e -> {
            initX = e.getX();
        });

        card.setOnMouseDragged(e -> {
            double current = e.getX();
            if (current < initX) {
                if ((current - initX) < 0 && (current - initX) > -50) {
                    cell.setTranslateX(current - initX);
                }
            }
        });

        card.setOnMouseReleased(e -> {
            double current = e.getX();
            double delta = current - initX;
            System.out.println(delta);
            if (delta > -50) {
                int originalMillis = 500;
                double ratio = (50 - delta) / 50;
                int newMillis = (int) (500 * ratio);
                TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
                translate.setToX(0);
                translate.setNode(cell);
                translate.play();
            } else {
                FadeTransition ft = new FadeTransition(Duration.millis(300), cell);
                ft.setFromValue(1.0);
                ft.setToValue(0);

                TranslateTransition translateTransition
                        = new TranslateTransition(Duration.millis(300), cell);
                translateTransition.setFromX(cell.getTranslateX());
                translateTransition.setToX(-400);

                ParallelTransition parallel = new ParallelTransition();
                parallel.getChildren().addAll(ft, translateTransition);
                parallel.setOnFinished(evt -> {

                    for(int i = 0; i < cardPane.getItems().size(); i++){
                        if(cardPane.getItems().get(i) == card){
                            cardPane.getItems().remove(i);
                        }
                    }
                    initX = 0;
                });

                parallel.play();
            }
        });
    }

    private static CardPane cardPane = null;
    public BasicView(String name) {
        super(name);

        cardPane = new CardPane();

        cardPane.setCellFactory(p -> new CardCell<CustomCard>() {

            @Override
            public void updateItem(CustomCard item, boolean empty) {
                super.updateItem(item, empty);
                if (!empty) {
                    setText(null);
                    setGraphic(item);
                } else {
                    setText(null);
                    setGraphic(null);
                }
            }
        });



        setCenter(cardPane);
    }

    private static void addCard(CustomCard newCard){
        cardPane.getItems().add(newCard);
            cardPane.layout();
            VirtualFlow vf = (VirtualFlow) cardPane.lookup(".virtual-flow");
            Node cell = vf.getCell(cardPane.getItems().size() - 1);
            cell.setTranslateX(0);
            cell.setOpacity(1.0);
            if (!cardPane.lookup(".scroll-bar").isVisible()) {
                FadeInUpTransition f = new FadeInUpTransition(cell);
                f.setRate(2);
                f.play();
            } else {
                PauseTransition p = new PauseTransition(Duration.millis(20));
                p.setOnFinished(e -> {
                    vf.getCell(cardPane.getItems().size() - 1).setOpacity(0);
                    vf.show(cardPane.getItems().size() - 1);
                    FadeTransition f = new FadeTransition();
                    f.setDuration(Duration.seconds(1));
                    f.setFromValue(0);
                    f.setToValue(1);
                    f.setNode(vf.getCell(cardPane.getItems().size() - 1));
                    f.setOnFinished(t -> {
                    });
                    f.play();
                });
                p.play();
            }
            addRemovalLogicForCell(newCard, cell);
    }

    @Override
    protected void updateAppBar(AppBar appBar) {
        appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
        appBar.setTitleText("Basic View");
        appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> addCard(new CustomCard("Hello"))));
    }

}

这会在添加和向左滑动删除时导致以下输出:

如果您查看 ScenicView,您会注意到 CardPane 包含一个 CharmListView 控件,它使用了一个内部 ListView,其大小为它的 parent.

所以这应该有效:

.card-pane > .charm-list-view > .list-view {
    -fx-background-color: transparent;
}

正如我提到的,控件基于ListView,因此提供单元格的方式是使用单元格工厂。正如您在控件的 JavaDoc:

中所读到的

The CardPane is prepared for a big number of items by reusing its cards.

A developer may personalize cell creation by specifying a cell factory through cellFactoryProperty(). The default cell factory is prepared to accept objects from classes that extend Node or other classes that don't extend from Node, in the latter case the card text will be given by the Object.toString() implementation of the object.

如果您还没有使用它,请考虑使用这样的东西:

cardPane.setCellFactory(p -> new CardCell<T>() {

    @Override
    public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        if (!empty) {
            setText(null);
            setGraphic(createContent(item));
        } else {
            setText(null);
            setGraphic(null);
        }
    }
});
 

这应该可以为您管理卡片布局,避免出现空白单元格或错误地重复使用它们。

至于动画,使用起来应该没有问题。

对于滑动动画,Comments2.0 sample 提供了一个类似的用例:A ListView,其中每个单元格使用 SlidingListTile。看看它的实现。

您应该可以在 CardPane 中重复使用它。

尝试一下,如果仍有问题,post 此处提供工作示例(或提供 link),以便我们重现它们。

编辑

基于 posted 代码,关于如何设置工厂单元的注释:

所有使用单元格的 JavaFX 控件(如 ListViewTableView),以及 Gluon CardPane,遵循 MVC 模式:

  • 模型。控件绑定到模型,使用该模型的可观察项目列表。对于示例,String 或任何常规 POJO,或者作为首选,JavaFX bean(具有可观察的属性)。

所以在这种情况下,您应该:

CardPane<String> cardPane = new CardPane<>();
  • 查看。该控件有一个方法来设置单元格如何呈现模型,即 cellFactory。该工厂可以只定义文本或任何图形节点,例如您的 CustomCard.

在这种情况下,您应该:

cardPane.setCellFactory(p -> new CardCell<String>() {
        
        private final CustomCard card;
        {
            card = new CustomCard();
        }

        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (item != null && !empty) {
                card.setText(item);
                setGraphic(card);
                setText(null);
            } else {
                setGraphic(null);
                setText(null);
            }
        }
    });

其中:

class CustomCard extends StackPane {
    
    private final Label label;
    
    public CustomCard(){
        label = new Label();
        getChildren().add(label);
    }

    public void setText(String text) {
        label.setText(text);
    }
}

在内部,该控件使用 VirtualFlow 管理重用单元格,并且仅在滚动时修改内容(模型)。

正如您在单元工厂中看到的那样,现在您将迭代模型 (String),而 CustomCard 保持不变,只是更新了内容。

使用这种方法不会出现您描述的任何问题,至少在添加单元格时是这样。

编辑 2

我想出了一个适合我的解决方案,应该可以解决所有提到的问题。除了前面提到的,还需要在 updateItem 回调中恢复应用于 CustomCard 的转换。

public class BasicView extends View {

    private final CardPane<String> cardPane;

    public BasicView(String name) {
        super(name);

        cardPane = new CardPane<>();

        cardPane.setCellFactory(p -> new CardCell<String>() {

            private final CustomCard card;
            private final HBox box;
            {
                card = new CustomCard();
                card.setMaxWidth(Double.MAX_VALUE);
                card.prefWidthProperty().bind(widthProperty());
                box = new HBox(card);
                box.setAlignment(Pos.CENTER);
                box.setStyle("-fx-background-color: grey");
                addRemovalLogicForCell(card);
            }

            @Override
            public void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null && !empty) {
                    card.setText(item);
                    card.setTranslateX(0);
                    card.setOpacity(1.0);
                    setGraphic(box);
                    setText(null);
                } else {
                    setGraphic(null);
                    setText(null);
                }
            }
        });

        setCenter(cardPane);
    }

    class CustomCard extends StackPane {

        private final Label label;

        public CustomCard(){
            label = new Label();
            label.setStyle("-fx-font-size: 20;");
            getChildren().add(label);
            setStyle("-fx-padding: 20; -fx-background-color: white");
            setPrefHeight(100);
        }

        public void setText(String text) {
            label.setText(text);
        }

        public String getText() {
            return label.getText();
        }
    }

    private double initX = 0;
    private void addRemovalLogicForCell(CustomCard card) {
        card.setOnMousePressed(e -> {
            initX = e.getX();
        });

        card.setOnMouseDragged(e -> {
            double current = e.getX();
            if ((current - initX) < 0 && (current - initX) > -50) {
                card.setTranslateX(current - initX);
            }
        });

        card.setOnMouseReleased(e -> {
            double current = e.getX();
            double delta = current - initX;
            if (delta < 50) {
                if (delta > -50) {
                    int originalMillis = 500;
                    double ratio = (50 - delta) / 50;
                    int newMillis = (int) (500 * ratio);
                    TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
                    translate.setToX(0);
                    translate.setNode(card);
                    translate.play();
                } else {
                    FadeTransition ft = new FadeTransition(Duration.millis(300), card);
                    ft.setFromValue(1.0);
                    ft.setToValue(0);

                    TranslateTransition translateTransition
                            = new TranslateTransition(Duration.millis(300), card);
                    translateTransition.setFromX(card.getTranslateX());
                    translateTransition.setToX(-400);

                    ParallelTransition parallel = new ParallelTransition();
                    parallel.getChildren().addAll(ft, translateTransition);
                    parallel.setOnFinished(evt -> {
                        cardPane.getItems().remove(card.getText());
                        initX = 0;
                    });

                    parallel.play();
                }
            }

        });
    }

    private void addCard(String newCard){
        cardPane.getItems().add(newCard);
        cardPane.layout();

        VirtualFlow vf = (VirtualFlow) cardPane.lookup(".virtual-flow");
        IndexedCell cell = vf.getCell(cardPane.getItems().size() - 1);
        cell.setTranslateX(0);
        cell.setOpacity(0);
        if (! cardPane.lookup(".scroll-bar").isVisible()) {
            FadeInUpTransition f = new FadeInUpTransition(cell, true);
            f.setRate(2);
            f.play();
        } else {
            PauseTransition p = new PauseTransition(Duration.millis(20));
            p.setOnFinished(e -> {
                vf.show(cardPane.getItems().size() - 1);
                FadeInTransition f = new FadeInTransition(cell);
                f.setRate(2);
                f.play();
            });
            p.play();
        }
    }

    @Override
    protected void updateAppBar(AppBar appBar) {
        appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
        appBar.setTitleText("Basic View");
        appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> addCard("Hello #" + new Random().nextInt(100))));
    }

}