/* * 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.android.skins; import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.controls.JFXTextArea; import com.jfoenix.transitions.CachedTransition; import com.jfoenix.validation.base.ValidatorBase; import com.sun.javafx.scene.control.skin.TextAreaSkin; import com.sun.javafx.scene.control.skin.TextAreaSkinAndroid; import javafx.animation.Animation.Status; import javafx.animation.*; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; 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; import java.lang.reflect.Field; /** * <h1>Material Design TextArea Skin for android</h1> * The JFXTextAreaSkinAndroid implements material design text area for android * when porting JFoenix to android using JavaFXPorts * <p> * <b>Note:</b> the implementation is a copy of the original {@link com.jfoenix.skins.JFXTextAreaSkin JFXTextAreaSkin} * however it extends the JavaFXPorts text area android skin. * * @author Shadi Shaheen * @version 2.0 * @since 2017-01-25 */ public class JFXTextAreaSkinAndroid extends TextAreaSkinAndroid { private static Background transparentBackground = new Background( new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY), new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY), new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY), new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)); private boolean invalid = true; private StackPane line = new StackPane(); private StackPane focusedLine = new StackPane(); private Label errorLabel = new Label(); private StackPane errorIcon = new StackPane(); private HBox errorContainer; private ScrollPane scrollPane; private double initScale = 0.05; private double oldErrorLabelHeight = -1; // private Region textPane; private double initYLayout = -1; private double initHeight = -1; private boolean errorShown = false; private double currentFieldHeight = -1; private double errorLabelInitHeight = 0; private boolean heightChanged = false; private Pane promptContainer; private Text promptText; private CachedTransition promptTextUpTransition; private CachedTransition promptTextDownTransition; private CachedTransition promptTextColorTransition; private Timeline hideErrorAnimation; private ParallelTransition transition; private Scale promptTextScale = new Scale(1, 1, 0, 0); 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 Paint oldPromptTextFill; private BooleanBinding usePromptText = Bindings.createBooleanBinding(() -> usePromptText(), getSkinnable().textProperty(), getSkinnable().promptTextProperty()); public JFXTextAreaSkinAndroid(JFXTextArea textArea) { super(textArea); // init text area properties scrollPane = (ScrollPane) getChildren().get(0); ((Region) scrollPane.getContent()).setPadding(new Insets(0)); // hide text area borders scrollPane.setBackground(transparentBackground); ((Region) scrollPane.getContent()).setBackground(transparentBackground); getSkinnable().setBackground(transparentBackground); textArea.setWrapText(true); errorLabel.getStyleClass().add("error-label"); errorLabel.setPadding(new Insets(4, 0, 0, 0)); errorLabel.setWrapText(true); errorIcon.setTranslateY(3); StackPane errorLabelContainer = new StackPane(); errorLabelContainer.getChildren().add(errorLabel); StackPane.setAlignment(errorLabel, Pos.CENTER_LEFT); promptContainer = new StackPane(); line.getStyleClass().add("input-line"); focusedLine.getStyleClass().add("input-focused-line"); // draw lines line.setPrefHeight(1); line.setTranslateY(1 + 4 + 2); // translate = prefHeight + init_translation line.setBackground(new Background(new BackgroundFill(((JFXTextArea) getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); if (getSkinnable().isDisabled()) { line.setBorder(new Border(new BorderStroke(((JFXTextArea) 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(4 + 2); // translate = prefHeight + init_translation(-1) focusedLine.setBackground(new Background(new BackgroundFill(((JFXTextArea) getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); focusedLine.setOpacity(0); focusedLine.getTransforms().add(scale); errorContainer = new HBox(); errorContainer.getChildren().setAll(errorLabelContainer, errorIcon); HBox.setHgrow(errorLabelContainer, Priority.ALWAYS); errorContainer.setSpacing(10); errorContainer.setVisible(false); errorContainer.setOpacity(0); getChildren().addAll(line, focusedLine, promptContainer, errorContainer); getSkinnable().setBackground(transparentBackground); // add listeners to show error label errorLabel.heightProperty().addListener((o, oldVal, newVal) -> { if (errorShown) { if (oldErrorLabelHeight == -1) { oldErrorLabelHeight = errorLabelInitHeight = oldVal.doubleValue(); } heightChanged = true; currentFieldHeight = this.getSkinnable().getHeight() - oldErrorLabelHeight + newVal.doubleValue(); oldErrorLabelHeight = newVal.doubleValue(); } }); errorContainer.visibleProperty().addListener((o, oldVal, newVal) -> { // show the error label if it's not shown if (newVal) { new Timeline(new KeyFrame(Duration.millis(160), new KeyValue(errorContainer.opacityProperty(), 1, Interpolator.EASE_BOTH))).play(); } }); textArea.labelFloatProperty().addListener((o, oldVal, newVal) -> { if (newVal) { JFXUtilities.runInFX(() -> createFloatingLabel()); } else { promptText.visibleProperty().bind(usePromptText); } createFocusTransition(); }); textArea.activeValidatorProperty().addListener((o, oldVal, newVal) -> { if (scrollPane != null) { if (!((JFXTextArea) getSkinnable()).isDisableAnimation()) { if (hideErrorAnimation != null && hideErrorAnimation.getStatus() == Status.RUNNING) { hideErrorAnimation.stop(); } if (newVal != null) { hideErrorAnimation = new Timeline(new KeyFrame(Duration.millis(160), new KeyValue(errorContainer.opacityProperty(), 0, Interpolator.EASE_BOTH))); hideErrorAnimation.setOnFinished(finish -> { errorContainer.setVisible(false); JFXUtilities.runInFX(() -> showError(newVal)); }); hideErrorAnimation.play(); } else { JFXUtilities.runInFX(() -> hideError()); } } else { if (newVal != null) { JFXUtilities.runInFXAndWait(() -> showError(newVal)); } else { JFXUtilities.runInFXAndWait(() -> hideError()); } } } }); textArea.focusColorProperty().addListener((o, oldVal, newVal) -> { if (newVal != null) { focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY))); if (((JFXTextArea) getSkinnable()).isLabelFloat()) { promptTextColorTransition = new CachedTransition(promptContainer, 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; } } }); textArea.unFocusColorProperty().addListener((o, oldVal, newVal) -> { if (newVal != null) { line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY))); } }); // handle animation on focus gained/lost event textArea.focusedProperty().addListener((o, oldVal, newVal) -> { if (newVal) { focus(); } else { unFocus(); } }); // handle text changing at runtime textArea.textProperty().addListener((o, oldVal, newVal) -> { if (!getSkinnable().isFocused() && ((JFXTextArea) getSkinnable()).isLabelFloat()) { if (newVal == null || newVal.isEmpty()) { animateFLoatingLabel(false); } else { animateFLoatingLabel(true); } } }); textArea.backgroundProperty().addListener((o, oldVal, newVal) -> { // Force transparent background if (oldVal == transparentBackground && newVal != transparentBackground) { textArea.setBackground(transparentBackground); } }); textArea.disabledProperty().addListener((o, oldVal, newVal) -> { line.setBorder(newVal ? new Border(new BorderStroke(((JFXTextArea) getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(line.getHeight()))) : Border.EMPTY); line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXTextArea) getSkinnable()) .getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY))); }); // prevent setting prompt text fill to transparent when text field is focused (override java transparent color if the control was focused) promptTextFill.addListener((o, oldVal, newVal) -> { if (Color.TRANSPARENT.equals(newVal) && ((JFXTextArea) getSkinnable()).isLabelFloat()) { promptTextFill.set(oldVal); } }); } @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { super.layoutChildren(x, y, w, h); // change control properties if and only if animations are stopped if (transition == null || transition.getStatus() == Status.STOPPED) { if (getSkinnable().isFocused() && ((JFXTextArea) getSkinnable()).isLabelFloat()) { promptTextFill.set(((JFXTextArea) getSkinnable()).getFocusColor()); } } if (invalid) { invalid = false; // set the default background of text area viewport to white Region viewPort = (Region) scrollPane.getChildrenUnmodifiable().get(0); viewPort.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY))); // reapply css of scroll pane in case set by the user viewPort.applyCss(); // create floating label createFloatingLabel(); // to position the prompt node properly super.layoutChildren(x, y, w, h); // update validation container if (((JFXTextArea) getSkinnable()).getActiveValidator() != null) { updateValidationError(); } // focus createFocusTransition(); if (getSkinnable().isFocused()) { focus(); } } focusedLine.resizeRelocate(x, h - focusedLine.prefHeight(-1), w, focusedLine.prefHeight(-1)); line.resizeRelocate(x, h - focusedLine.prefHeight(-1), w, line.prefHeight(-1)); errorContainer.resizeRelocate(x, y, w, -1); errorContainer.setTranslateY(h + focusedLine.getHeight() + 4); scale.setPivotX(w / 2); } private void updateValidationError() { if (hideErrorAnimation != null && hideErrorAnimation.getStatus() == Status.RUNNING) { hideErrorAnimation.stop(); } hideErrorAnimation = new Timeline( new KeyFrame(Duration.millis(160), new KeyValue(errorContainer.opacityProperty(), 0, Interpolator.EASE_BOTH))); hideErrorAnimation.setOnFinished(finish -> { errorContainer.setVisible(false); showError(((JFXTextArea) getSkinnable()).getActiveValidator()); }); hideErrorAnimation.play(); } private void createFloatingLabel() { if (((JFXTextArea) getSkinnable()).isLabelFloat()) { if (promptText == null) { // get the prompt text node or create it boolean triggerFloatLabel = false; if (((Region) scrollPane.getContent()).getChildrenUnmodifiable().get(0) instanceof Text) { promptText = (Text) ((Region) scrollPane.getContent()).getChildrenUnmodifiable().get(0); } else { Field field; try { field = TextAreaSkin.class.getDeclaredField("promptNode"); field.setAccessible(true); createPromptNode(); field.set(this, promptText); // position the prompt node in its position triggerFloatLabel = true; oldPromptTextFill = promptTextFill.get(); } catch (NoSuchFieldException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // fixed issue text area is being resized when the content is excedeing its width promptText.wrappingWidthProperty().addListener((o, oldval, newVal) -> { if (newVal.doubleValue() > getSkinnable().getWidth()) { promptText.setWrappingWidth(getSkinnable().getWidth()); } }); promptText.getTransforms().add(promptTextScale); promptContainer.getChildren().add(promptText); if (triggerFloatLabel) { promptText.setTranslateY(-promptText.getBoundsInLocal().getHeight() - 2); promptTextScale.setX(0.85); promptTextScale.setY(0.85); } } // create prompt animations promptTextUpTransition = new CachedTransition(promptContainer, new Timeline( new KeyFrame(Duration.millis(1300), new KeyValue(promptText.translateYProperty(), -promptText.getLayoutBounds().getHeight() - 2, 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(promptContainer, new Timeline( new KeyFrame(Duration.millis(1300), new KeyValue(promptTextFill, ((JFXTextArea) getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) { { setDelay(Duration.millis(0)); setCycleDuration(Duration.millis(160)); } protected void starting() { super.starting(); oldPromptTextFill = promptTextFill.get(); } }; promptTextDownTransition = new CachedTransition(promptContainer, 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); }); promptText.visibleProperty().unbind(); promptText.visibleProperty().set(true); } } private void createPromptNode() { promptText = new Text(); promptText.setManaged(false); promptText.getStyleClass().add("text"); promptText.visibleProperty().bind(usePromptText); promptText.fontProperty().bind(getSkinnable().fontProperty()); promptText.textProperty().bind(getSkinnable().promptTextProperty()); promptText.fillProperty().bind(promptTextFill); promptText.setLayoutX(1); } private void focus() { // in case the method request layout is not called before focused // this is bug is reported while editing treetableview cells if (scrollPane == null) { Platform.runLater(() -> focus()); } else { // create the focus animations if (transition == null) { createFocusTransition(); } transition.play(); } } private void createFocusTransition() { transition = new ParallelTransition(); if (((JFXTextArea) 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 (oldPromptTextFill != null && ((JFXTextArea) getSkinnable()).isLabelFloat()) { promptTextFill.set(oldPromptTextFill); if (usePromptText()) { promptTextDownTransition.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 = null; } if (up && promptContainer.getTranslateY() == 0) { promptTextDownTransition.stop(); promptTextUpTransition.play(); } else if (!up) { promptTextUpTransition.stop(); promptTextDownTransition.play(); } } } private boolean usePromptText() { String txt = getSkinnable().getText(); String promptTxt = getSkinnable().getPromptText(); return (txt == null || txt.isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !promptTextFill .get() .equals(Color.TRANSPARENT); } private void showError(ValidatorBase validator) { // set text in error label errorLabel.setText(validator.getMessage()); // show error icon Node awsomeIcon = validator.getIcon(); errorIcon.getChildren().clear(); if (awsomeIcon != null) { errorIcon.getChildren().add(awsomeIcon); StackPane.setAlignment(awsomeIcon, Pos.TOP_RIGHT); } // init only once, to fix the text pane from resizing if (initYLayout == -1) { scrollPane.setMaxHeight(scrollPane.getHeight()); initYLayout = scrollPane.getBoundsInParent().getMinY(); initHeight = getSkinnable().getHeight(); currentFieldHeight = initHeight; } errorContainer.setVisible(true); errorShown = true; } private void hideError() { if (heightChanged) { new Timeline(new KeyFrame(Duration.millis(160), new KeyValue(scrollPane.translateYProperty(), 0, Interpolator.EASE_BOTH))).play(); // reset the height of text field new Timeline(new KeyFrame(Duration.millis(160), new KeyValue(getSkinnable().minHeightProperty(), initHeight, Interpolator.EASE_BOTH))).play(); heightChanged = false; } // clear error label text errorLabel.setText(null); oldErrorLabelHeight = errorLabelInitHeight; // clear error icon errorIcon.getChildren().clear(); // reset the height of the text field currentFieldHeight = initHeight; // hide error container errorContainer.setVisible(false); errorShown = false; } }