/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 com.jfoenix.skins; import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.transitions.CachedTransition; import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin; import javafx.animation.*; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.text.Text; import javafx.scene.transform.Scale; import javafx.util.Duration; /** * <h1>Material Design ComboBox Skin</h1> * * @author Shadi Shaheen * @version 2.0 * @since 2017-01-25 */ public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> { /*************************************************************************** * * * Private fields * * * **************************************************************************/ private boolean invalid = true; private StackPane customPane; private StackPane line = new StackPane(); private StackPane focusedLine = new StackPane(); private Text promptText = new Text(); private double initScale = 0.05; private Scale scale = new Scale(initScale, 1); private Timeline linesAnimation = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(scale.xProperty(), initScale, Interpolator.EASE_BOTH), new KeyValue(focusedLine.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1), new KeyValue(focusedLine.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(160), new KeyValue(scale.xProperty(), 1, Interpolator.EASE_BOTH)) ); private ParallelTransition transition; private CachedTransition promptTextUpTransition; private CachedTransition promptTextDownTransition; private CachedTransition promptTextColorTransition; private Scale promptTextScale = new Scale(1, 1, 0, 0); private Paint oldPromptTextFill; protected final ObjectProperty<Paint> promptTextFill = new SimpleObjectProperty<>(Color.valueOf("#B2B2B2")); private BooleanBinding usePromptText = Bindings.createBooleanBinding(() -> usePromptText(), ((JFXComboBox<?>) getSkinnable()).valueProperty(), getSkinnable().promptTextProperty()); /*************************************************************************** * * * Constructors * * * **************************************************************************/ public JFXComboBoxListViewSkin(final JFXComboBox<T> comboBox) { super(comboBox); // customize combo box arrowButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null))); // create my custom pane for the prompt node promptText.textProperty().bind(comboBox.promptTextProperty()); promptText.fillProperty().bind(promptTextFill); promptText.getStyleClass().addAll("text", "prompt-text"); promptText.getTransforms().add(promptTextScale); if (!comboBox.isLabelFloat()) { promptText.visibleProperty().bind(usePromptText); } customPane = new StackPane(); customPane.setMouseTransparent(true); customPane.getStyleClass().add("combo-box-button-container"); customPane.backgroundProperty().bindBidirectional(getSkinnable().backgroundProperty()); customPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null))); customPane.getChildren().add(promptText); getChildren().add(0, customPane); StackPane.setAlignment(promptText, Pos.CENTER_LEFT); // add lines line.getStyleClass().add("input-line"); focusedLine.getStyleClass().add("input-focused-line"); getChildren().add(line); getChildren().add(focusedLine); line.setPrefHeight(1); line.setTranslateY(1); // translate = prefHeight + init_translation line.setManaged(false); line.setBackground(new Background(new BackgroundFill(((JFXComboBox<?>) getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); if (getSkinnable().isDisabled()) { line.setBorder(new Border(new BorderStroke(((JFXComboBox<?>) getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1)))); line.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY))); } // focused line focusedLine.setPrefHeight(2); focusedLine.setTranslateY(0); // translate = prefHeight + init_translation(-1) focusedLine.setBackground(new Background(new BackgroundFill(((JFXComboBox<?>) getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); focusedLine.setOpacity(0); focusedLine.getTransforms().add(scale); focusedLine.setManaged(false); if (comboBox.isEditable()) { comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 4 0 4 0"); comboBox.getEditor().promptTextProperty().unbind(); comboBox.getEditor().setPromptText(null); comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> { usePromptText.invalidate(); comboBox.setValue(getConverter().fromString(newVal)); }); } comboBox.labelFloatProperty().addListener((o, oldVal, newVal) -> { if (newVal) { promptText.visibleProperty().unbind(); JFXUtilities.runInFX(() -> createFloatingAnimation()); } else { promptText.visibleProperty().bind(usePromptText); } createFocusTransition(); }); comboBox.focusColorProperty().addListener((o, oldVal, newVal) -> { if (newVal != null) { focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY))); if (((JFXComboBox<?>) getSkinnable()).isLabelFloat()) { promptTextColorTransition = new CachedTransition(customPane, new Timeline( new KeyFrame(Duration.millis(1300), new KeyValue(promptTextFill, newVal, Interpolator.EASE_BOTH)))) { { setDelay(Duration.millis(0)); setCycleDuration(Duration.millis(160)); } protected void starting() { super.starting(); oldPromptTextFill = promptTextFill.get(); } }; // reset transition transition = null; } } }); comboBox.unFocusColorProperty().addListener((o, oldVal, newVal) -> { if (newVal != null) { line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY))); } }); comboBox.disabledProperty().addListener((o, oldVal, newVal) -> { line.setBorder(newVal ? new Border(new BorderStroke(((JFXComboBox<?>) getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(line.getHeight()))) : Border.EMPTY); line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXComboBox<?>) getSkinnable()) .getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); }); // handle animation on focus gained/lost event comboBox.focusedProperty().addListener((o, oldVal, newVal) -> { if (newVal) { focus(); } else { unFocus(); } }); // handle animation on value changed comboBox.valueProperty().addListener((o, oldVal, newVal) -> { if (((JFXComboBox<?>) getSkinnable()).isLabelFloat()) { if (newVal == null || newVal.toString().isEmpty()) { animateFloatingLabel(false); } else { animateFloatingLabel(true); } } }); } /*************************************************************************** * * * Public API * * * **************************************************************************/ @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { super.layoutChildren(x, y, w, h); customPane.resizeRelocate(x, y, w, h); if (invalid) { invalid = false; // create floating label createFloatingAnimation(); if(getSkinnable().getValue()!=null) animateFloatingLabel(true); } focusedLine.resizeRelocate(x, getSkinnable().getHeight(), w, focusedLine.prefHeight(-1)); line.resizeRelocate(x, getSkinnable().getHeight(), w, line.prefHeight(-1)); scale.setPivotX(w / 2); } private void createFloatingAnimation() { // TODO: the 6.05 should be computed, for now its hard coded to keep the alignment with other controls promptTextUpTransition = new CachedTransition(customPane, new Timeline( new KeyFrame(Duration.millis(1300), new KeyValue(promptText.translateYProperty(), -customPane.getHeight() + 6.05, Interpolator.EASE_BOTH), new KeyValue(promptTextScale.xProperty(), 0.85, Interpolator.EASE_BOTH), new KeyValue(promptTextScale.yProperty(), 0.85, Interpolator.EASE_BOTH)))) {{ setDelay(Duration.millis(0)); setCycleDuration(Duration.millis(240)); }}; promptTextColorTransition = new CachedTransition(customPane, new Timeline( new KeyFrame(Duration.millis(1300), new KeyValue(promptTextFill, ((JFXComboBox<?>) getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) { { setDelay(Duration.millis(0)); setCycleDuration(Duration.millis(160)); } protected void starting() { super.starting(); oldPromptTextFill = promptTextFill.get(); } }; promptTextDownTransition = new CachedTransition(customPane, new Timeline( new KeyFrame(Duration.millis(1300), new KeyValue(promptText.translateYProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(promptTextScale.xProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(promptTextScale.yProperty(), 1, Interpolator.EASE_BOTH)))) {{ setDelay(Duration.millis(0)); setCycleDuration(Duration.millis(240)); }}; promptTextDownTransition.setOnFinished((finish) -> { promptText.setTranslateY(0); promptTextScale.setX(1); promptTextScale.setY(1); }); } private void focus() { // create the focus animations if (transition == null) { createFocusTransition(); } transition.play(); } /** * this method is called when the text property is changed when the * field is not focused (changed in code) * * @param up */ private void animateFloatingLabel(boolean up) { if (promptText == null) { Platform.runLater(() -> animateFloatingLabel(up)); } else { if (transition != null) { transition.stop(); transition.getChildren().remove(promptTextUpTransition); transition.getChildren().remove(promptTextColorTransition); transition = null; } if (up && promptText.getTranslateY() == 0) { promptTextDownTransition.stop(); promptTextUpTransition.play(); if (getSkinnable().isFocused()) { promptTextColorTransition.play(); } } else if (!up) { promptTextUpTransition.stop(); if (getSkinnable().isFocused()) { promptTextFill.set(oldPromptTextFill); } promptTextDownTransition.play(); } } } private void createFocusTransition() { transition = new ParallelTransition(); if (((JFXComboBox<?>) getSkinnable()).isLabelFloat()) { transition.getChildren().add(promptTextUpTransition); transition.getChildren().add(promptTextColorTransition); } transition.getChildren().add(linesAnimation); } private void unFocus() { if (transition != null) { transition.stop(); } scale.setX(initScale); focusedLine.setOpacity(0); if (((JFXComboBox<?>) getSkinnable()).isLabelFloat() && oldPromptTextFill != null) { promptTextFill.set(oldPromptTextFill); if (usePromptText()) { promptTextDownTransition.play(); } } } private boolean usePromptText() { Object txt = ((JFXComboBox<?>) getSkinnable()).getValue(); String promptTxt = getSkinnable().getPromptText(); return (txt == null || txt.toString() .isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !promptTextFill .get() .equals(Color.TRANSPARENT); } }