/*
* 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.controls;
import com.jfoenix.controls.JFXTreeView.CellAnimation;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.animation.Animation.Status;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.scene.Node;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import java.lang.ref.WeakReference;
/**
* JFXTreeCell is the animated material design implementation of a tree cell.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2017-02-15
*/
public class JFXTreeCell<T> extends TreeCell<T> {
protected JFXRippler cellRippler = new JFXRippler(new StackPane()) {
@Override
protected void initListeners() {
ripplerPane.setOnMousePressed((event) -> createRipple(event.getX(), event.getY()));
}
};
private HBox hbox;
private StackPane selectedPane = new StackPane();
private WeakReference<TreeItem<T>> treeItemRef;
private ChangeListener<Boolean> weakExpandListener = (ChangeListener<Boolean>) (observable, oldValue, newValue) -> {
JFXTreeView<T> jfxTreeView = (JFXTreeView<T>) getTreeView();
jfxTreeView.clearAnimation();
int currentRow = getTreeView().getRow(getTreeItem());
jfxTreeView.animateRow = currentRow;
jfxTreeView.expand = newValue;
jfxTreeView.disableSiblings = false;
VirtualFlow<?> vf = (VirtualFlow<?>) getTreeView().lookup(".virtual-flow");
if (!newValue) {
int index = currentRow + getTreeItem().getChildren().size() + 1;
index = index > vf.getCellCount() ? vf.getCellCount() : index;
jfxTreeView.height = (index - currentRow - 1) * vf.getCell(currentRow).getHeight();
}
jfxTreeView.layoutY = vf.getCell(currentRow).getLayoutY();
};
private InvalidationListener treeItemGraphicInvalidationListener = observable -> updateDisplay(getItem(),
isEmpty());
private WeakInvalidationListener weakTreeItemGraphicListener = new WeakInvalidationListener(
treeItemGraphicInvalidationListener);
private ChangeListener<? super Status> weakAnimationListener = (o, oldVal, newVal) -> {
if (newVal == Status.STOPPED) {
clearCellAnimation();
}
};
private WeakReference<JFXTreeView<T>> treeViewRef;
public JFXTreeCell() {
selectedPane.setStyle("-fx-background-color:RED");
selectedPane.setPrefWidth(3);
selectedPane.setMouseTransparent(true);
selectedProperty().addListener((o, oldVal, newVal) -> {
selectedPane.setOpacity(newVal ? 1 : 0);
});
final InvalidationListener treeViewInvalidationListener = observable -> {
JFXTreeView<T> oldTreeView = treeViewRef == null ? null : treeViewRef.get();
if (oldTreeView != null) {
oldTreeView.trans.statusProperty().removeListener(weakAnimationListener);
}
JFXTreeView<T> newTreeView = (JFXTreeView<T>) getTreeView();
if (newTreeView != null) {
newTreeView.trans.statusProperty().addListener(weakAnimationListener);
treeViewRef = new WeakReference<>(newTreeView);
}
};
final WeakInvalidationListener weakTreeViewListener = new WeakInvalidationListener(treeViewInvalidationListener);
treeViewProperty().addListener(weakTreeViewListener);
final InvalidationListener treeItemInvalidationListener = observable -> {
TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
if (oldTreeItem != null) {
oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
oldTreeItem.expandedProperty().removeListener(weakExpandListener);
}
TreeItem<T> newTreeItem = getTreeItem();
if (newTreeItem != null) {
newTreeItem.graphicProperty().addListener(weakTreeItemGraphicListener);
newTreeItem.expandedProperty().addListener(weakExpandListener);
treeItemRef = new WeakReference<>(newTreeItem);
}
};
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
treeItemProperty().addListener(weakTreeItemListener);
if (getTreeItem() != null) {
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
}
}
@Override
public void updateIndex(int i) {
JFXTreeView<T> jfxTreeView = (JFXTreeView<T>) getTreeView();
if (jfxTreeView.animateRow != -1 && i != -1) {
int oldIndex = getIndex();
if (oldIndex == -1 || (oldIndex > 0 && i > 0 && oldIndex != i)) {
if (jfxTreeView.sibRow == -1) {
if (jfxTreeView.getTreeItem(i) != null && jfxTreeView.getTreeItem(jfxTreeView.animateRow) != null
&& i > jfxTreeView.animateRow
&& jfxTreeView.getTreeItem(i).getParent() == jfxTreeView.getTreeItem(jfxTreeView.animateRow)
.getParent()) {
jfxTreeView.sibRow = i;
if (jfxTreeView.expand) {
jfxTreeView.height = -(jfxTreeView.sibRow - jfxTreeView.animateRow - 1) * getHeight();
}
}
}
if (jfxTreeView.getTreeItem(i) != null && jfxTreeView.getTreeItem(i) == jfxTreeView.getTreeItem(
jfxTreeView.animateRow)) {
if (i * this.getHeight() != jfxTreeView.layoutY) {
jfxTreeView.disableSiblings = true;
for (int index : jfxTreeView.sibAnimationMap.keySet()) {
if (index > i) {
jfxTreeView.trans.getChildren()
.remove(jfxTreeView.sibAnimationMap.get(index).getAnimation());
jfxTreeView.sibAnimationMap.get(index).getCell().clearCellAnimation();
}
}
jfxTreeView.sibAnimationMap.clear();
}
}
if (i > jfxTreeView.animateRow) {
if (jfxTreeView.expand) {
// animate siblings
if (i >= jfxTreeView.sibRow && jfxTreeView.sibRow != -1) {
animateSibling(i, jfxTreeView);
}
// animate children
else {
animateChild(i, jfxTreeView);
}
} else {
// animate siblings
animateSibling(i, jfxTreeView);
}
}
}
}
super.updateIndex(i);
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (!getChildren().contains(cellRippler)) {
getChildren().add(0, cellRippler);
getChildren().add(0, selectedPane);
}
if (isEmpty()) {
cellRippler.resizeRelocate(0, 0, 0, 0);
} else {
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
}
selectedPane.resizeRelocate(0, 0, selectedPane.prefWidth(-1), getHeight());
selectedPane.setOpacity(isSelected() ? 1 : 0);
if (((JFXTreeView<T>) getTreeView()).trans.getChildren().isEmpty()) {
clearCellAnimation();
((JFXTreeView<T>) getTreeView()).animateRow = -1;
} else if (((JFXTreeView<T>) getTreeView()).trans.getStatus() == Status.STOPPED) {
((JFXTreeView<T>) getTreeView()).trans.setOnFinished((finish) -> {
((JFXTreeView<T>) getTreeView()).trans.getChildren().clear();
((JFXTreeView<T>) getTreeView()).animateRow = -1;
});
((JFXTreeView<T>) getTreeView()).trans.play();
}
}
private void updateDisplay(T item, boolean empty) {
if (item == null || empty) {
hbox = null;
setText(null);
setGraphic(null);
} else {
TreeItem<T> treeItem = getTreeItem();
if (treeItem != null && treeItem.getGraphic() != null) {
if (item instanceof Node) {
setText(null);
if (hbox == null) {
hbox = new HBox(3);
}
hbox.getChildren().setAll(treeItem.getGraphic(), (Node) item);
setGraphic(hbox);
} else {
hbox = null;
setText(item.toString());
setGraphic(treeItem.getGraphic());
}
} else {
hbox = null;
if (item instanceof Node) {
setText(null);
setGraphic((Node) item);
} else {
setText(item.toString());
setGraphic(null);
}
}
}
}
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateDisplay(item, empty);
}
private void animateChild(int i, JFXTreeView<T> jfxTreeView) {
Timeline createChildAnimation = createChildAnimation(this, i - jfxTreeView.animateRow - 1);
if (jfxTreeView.childrenAnimationMap.containsKey(i)) {
jfxTreeView.trans.getChildren().remove(jfxTreeView.childrenAnimationMap.get(i).getAnimation());
jfxTreeView.childrenAnimationMap.get(i).getCell().clearCellAnimation();
}
jfxTreeView.childrenAnimationMap.put(i, new CellAnimation(this, createChildAnimation));
jfxTreeView.trans.getChildren().add(createChildAnimation);
}
private void animateSibling(int i, JFXTreeView<T> jfxTreeView) {
if (!jfxTreeView.disableSiblings) {
Timeline createSibAnimation = createSibAnimation(this, i);
jfxTreeView.sibAnimationMap.put(i, new CellAnimation(this, createSibAnimation));
jfxTreeView.trans.getChildren().add(createSibAnimation);
}
}
private void clearCellAnimation() {
this.setOpacity(1);
this.setTranslateY(0);
}
private Timeline createSibAnimation(TreeCell<?> cell, int index) {
cell.setTranslateY(((JFXTreeView<T>) getTreeView()).height);
return new Timeline(new KeyFrame(Duration.millis(120),
new KeyValue(cell.translateYProperty(), 0, Interpolator.EASE_BOTH)));
}
private Timeline createChildAnimation(TreeCell<?> cell, int delay) {
cell.setOpacity(0);
cell.setTranslateY(-1);
Timeline f1 = new Timeline(new KeyFrame(Duration.millis(120),
new KeyValue(cell.opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(cell.translateYProperty(), 0, Interpolator.EASE_BOTH)));
f1.setDelay(Duration.millis(20 + delay * 10));
return f1;
}
}