问题描述
几天前,我设法使用JavaFX创建一个自定义按钮,创建一个简单的按钮并使用 setStyle()
方法修改它的样式 String
对象(其值取决于是否单击按钮)作为参数。
A few days ago I managed to create a custom button with JavaFX by creating a simple button and modifying it's style using the setStyle()
method with String
objects (whose values vary depending on if the button was clicked or not) as parameters.
我不知道如何将自定义按钮转换为类
,每次我都可以导入想要使用它,所以我一直在研究,我找到了项目,其中包括使用Material Design定制的多个JavaFX控制器。我现在感兴趣的控制器是 MaterialButton
,其源代码如下:
I didn't know how to convert that customized button into a class
that I can import each time that I want to use it, so I've been researching and I found this project, which includes several JavaFX controllers customized with Material Design. The controller in which I'm interested right now is MaterialButton
, whose source code is the following:
import com.sun.javafx.scene.control.skin.ButtonSkin;
import javafx.animation.Animation;
import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ParallelTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
@SuppressWarnings("restriction")
public class CustomButton extends Button {
private static final Duration RIPPLE_DURATION = Duration.millis(250); // Duration of the ripple effect
private static final Duration SHADOW_DURATION = Duration.millis(350); // Duration of the shadow effect
private static final Color RIPPLE_COLOR = Color.web("#FFF", 0.3); // Ripple color
public CustomButton() { // Except from the setPrefHeifht() method, everything between this braces seems useless.
// Probably I'm wrong, but why would you want to do this?
textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> {
if (!oldValue.endsWith(newValue.toUpperCase())) {
textProperty().set(newValue.toUpperCase());
}
});
setPrefHeight(36); // Height of the button
}
@Override
public Skin<?> createDefaultSkin() { // Overrides the default skin of the button.
ButtonSkin buttonSkin = (ButtonSkin) getSkin();
if (buttonSkin == null) {
buttonSkin = new ButtonSkin(this);
Circle circleRipple = new Circle(0.1, RIPPLE_COLOR); // Creates the circle that must appear when the
// ripple effect animation is displayed.
buttonSkin.getChildren().add(0, circleRipple); // Adds the nodes to the screen.
setSkin(buttonSkin);
createRippleEffect(circleRipple); // Creates the ripple effect animation.
getStyleClass().add("ripple-button"); // I don't know what this line does, but if it is
// removed, the button is narrowed.
}
return buttonSkin;
}
public ButtonSkin getButtonSkin() { // Returns the skin casted to a ButtonSkin.
return (ButtonSkin) getSkin();
}
public void setFlated(boolean flated) { // The button is "flated" when it's pressed, so I guess that this is the same that saying "clicked".
if (flated) {
getStyleClass().add("flat"); // I don't know what this does.
} else {
getStyleClass().remove("flat"); // I don't know what does this do, either.
}
}
public boolean getFlated() {
return getStyleClass().indexOf("flat") != -1; // If the style class doesn't contain "flat", it returns false.
}
public void toggled(boolean toggled) { // For as far as I know, a toggle is the switch from one effect to another.
if (toggled) {
getStyleClass().add("toggle"); // I know as much about this line as I know about the other "getStyleClass()" lines.
} else {
getStyleClass().remove("toggle"); // I know as much about this line as I know about the other "getStyleClass()" lines.
}
}
public boolean getToggled() {
return getStyleClass().indexOf("toggle") != -1; // If the style class doesn't contain "toggle". it returns false.
}
private void createRippleEffect(Circle circleRipple) { // Defines the ripple effect animation.
Rectangle rippleClip = new Rectangle(); // Creates a new Rectangle
rippleClip.widthProperty().bind(widthProperty()); // For as far as I understand, it binds the width property of the
// rippleClip to itself. Why would you do that?
rippleClip.heightProperty().bind(heightProperty()); // For as far as I understand, it binds the width property of the
// rippleClip to itself. Why would you do that?
circleRipple.setClip(rippleClip); // For as far as I know, clipping is the process that consists
// in hiding everything that is outside of a specified area.
// What this does is specifying that area so that the parts of the circle
// that are outside of the rectangle, can be hided.
circleRipple.setOpacity(0.0); // Sets the circle's opacity to 0.
/*Fade Transition*/
FadeTransition fadeTransition = new FadeTransition(RIPPLE_DURATION, circleRipple); // Creates the fadeTransition.
fadeTransition.setInterpolator(Interpolator.EASE_OUT);
fadeTransition.setFromValue(1.0);
fadeTransition.setToValue(0.0);
/*ScaleTransition*/
final Timeline scaleRippleTimeline = new Timeline(); // Creates the scaleRippleTimeLine Timeline.
DoubleBinding circleRippleRadius = new DoubleBinding() { // Binds the radius of the circle to a double value.
{
bind(heightProperty(), widthProperty());
}
@Override
protected double computeValue() {
return Math.max(heightProperty().get(), widthProperty().get() * 0.45); // Returns the greater of both.
}
};
// The below line adds a listener to circleRippleRadius.
circleRippleRadius.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
KeyValue scaleValue = new KeyValue(circleRipple.radiusProperty(), newValue, Interpolator.EASE_OUT);
KeyFrame scaleFrame = new KeyFrame(RIPPLE_DURATION, scaleValue);
scaleRippleTimeline.getKeyFrames().add(scaleFrame);
});
/*ShadowTransition*/
Animation animation = new Transition() { // Creates and defines the animation Transition.
{
setCycleDuration(SHADOW_DURATION); // Sets the duration of "animation".
setInterpolator(Interpolator.EASE_OUT); // It sets the EASE_OUT interpolator,
// so that the shadow isn't displayed forever and its an animation.
}
@Override
protected void interpolate(double frac) {
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.30), 5 + (10 * frac), 0.10 + ((3 * frac) / 10), 0, 2 + (4 * frac)));
// Creates a a DropShadow effect and then sets it to "animation".
}
};
animation.setCycleCount(2);
animation.setAutoReverse(true);
final SequentialTransition rippleTransition = new SequentialTransition(); // Creates a SequentialTransition. The circle's scaling is the
// first transition to occur, and then the color of the button
// changes to the original one with fadeTransition
rippleTransition.getChildren().addAll(
scaleRippleTimeline,
fadeTransition
);
final ParallelTransition parallelTransition = new ParallelTransition();
getStyleClass().addListener((ListChangeListener.Change<? extends String> c) -> { // For as far as I understand, each time that the
// Style class changes, the lines of code between the
// braces are executed, but I still don't understand how
// does the Style class work.
if (c.getList().indexOf("flat") == -1 && c.getList().indexOf("toggle") == -1) {
setMinWidth(88);
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.30), 5, 0.10, 0, 2));
parallelTransition.getChildren().addAll(rippleTransition, animation);
} else {
parallelTransition.getChildren().addAll(rippleTransition);
setMinWidth(USE_COMPUTED_SIZE);
setEffect(null);
}
});
this.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { // When the button is clicked, each object's value is assigned to the first
// that it must have at the beginning of the animation. Then, the animation
// starts.
parallelTransition.stop();
circleRipple.setOpacity(0.0);
circleRipple.setRadius(0.1);
circleRipple.setCenterX(event.getX());
circleRipple.setCenterY(event.getY());
parallelTransition.playFromStart();
});
}
public void setRippleColor(Color color) {
((Shape) ((SkinBase) getSkin()).getChildren().get(0)).setFill(color); // I don't understand this line of code.
}
}
因为我很擅长JavaFX,我将整个GitHub项目视为金矿,因为我不仅可以访问示例,了解如何创建自定义控制器作为可以从另一个导入的类,还可以显示如何自定义其他几个控制器。
As I'm pretty new to JavaFX, I see the whole GitHub project as a gold mine, since not only I have access to examples that show how to create a custom controller as a class that can be imported from another, but also it shows how to customize several other controllers too.
问题是有一些我不理解的代码行(如果您阅读我对源代码所做的评论,您将会这样做) 。
The problem is that there are some lines of code that I don't understand (as you'll se if you read the comments I made on the source code).
例如,有几次 getStyleClass()。add(something)
是用过的。
我知道 getStylesheets()。add()
是如何工作的,但这是不同的;我会说Style类与CSS文件不同。
As an example, there are several times in which getStyleClass().add("something")
is used.I know how getStylesheets().add()
works, but this is different; I would say that the "Style" class is different from a CSS file.
如果是这样的话,它是如何工作的?据我所知, String
用作 getStyleClass()。add()
方法的参数用于能够确定它是否在Style类中,后面带有 if()
语句;但是这个课究竟是什么?我没有在互联网上看到任何关于它的文档。
If that's the case, how does it work? For as far as I understand, the String
s used as a parameter for the getStyleClass().add()
method are used for being able to determine if it is inside of the "Style" class with an if()
statement later; but what exactly is this class? I haven't seend any documentation about it on the internet.
我在理解 setRippleColor()
方法时遇到问题在源代码的最后也是。如果有人知道它是如何工作的,或者我应该注意什么来理解它,我会很感激。
I have problems understanding the setRippleColor()
method at the end of the source code, too. If somebody has an idea of how it works or what should I look up for for understanding it, I'd appreciate it.
提前致谢。
UPDATE :有人指出 ripple-button
是CSS文件的一部分,可以找到GitHub项目。
我复制了 MaterialButton
类并将其粘贴到一个新项目中,因此它无法访问 ripple-button
只提一下。然而,事实证明,如果我删除这行代码,按钮会缩小。我可以用任何东西改变波纹按钮,结果将是相同的,但线必须在那里。为什么会发生这种情况?
UPDATE: Somebody pointed out that ripple-button
is part of a CSS file that can be found on the GitHub project.I copied the MaterialButton
class and pasted it in a new project, so it can't access to ripple-button
by just mentioning it. Nevertheless, it turns out that if I delete this line of code, the button is narrowed. I can change "ripple-button" by anything and the result is going to be the same, but the line has to be there. Why does this happen?
UPDATE 2 :我已经明白 setRippleColor(颜色)
方法确实:基本上它获取了圆形的皮肤并获得了它的子项,因此它可以在将其转换为 Shape
后更改矩形的颜色。它被铸造成一个形状因为 Rectangle
extends Shape
。实际上这很简单。
UPDATE 2: I already understood what the setRippleColor(Color color)
method does: basically it gets the skin of the circle and gets its children so then it can change the rectangle's color once it's casted into a Shape
. It's casted into a shape because Rectangle
extends Shape
. It's pretty simple actually.
推荐答案
有些问题可能会让你感到困惑。
There are some issues that might shed some light on your confusion.
首先,这些东西不是控制器而是控制,这只是为了清晰起见,因为它可能很容易混淆。
First the things are not called 'controllers' but 'controls', this just for clarity as it might be easily confused.
方法 getStyleSheets()
返回 String
类型的 ObservableList
。此列表包含定义应用程序样式的 .css
文件的各种路径。样式添加在或类型。有关更多详细信息,请查看链接的JavaDoc或这些文章:
The method getStyleSheets()
returns an ObservableList
of type String
. This list contains the various path to the .css
files defining the style of the application. Styles are added either on a Scene
or a Control
of type Parent
. For more details check the linked JavaDoc or these articles:
- http://docs.oracle.com/javafx/2/css_tutorial/jfxpub-css_tutorial.htm
- https://blog.idrsolutions.com/2014/04/use-external-css-files-javafx/
样式表定义控件的样式。它们还提供了一些可以在任何到 getStyleClass()
,它还会返回 ObservableList
类型 String
,这次定义样式类名称。渲染名称时,在为该节点
定义的样式表集中查找,然后应用。如果没有找到这样的样式类,则会被忽略。 节点
是任何控件的基类。
The style sheets define the style of the controls. They also provide some additional style classes that can be set on any Node
through getStyleClass()
, which also returns an ObservableList
of type String
, this time defining the style class names. When rendering the name is looked up in the set of style sheets defined for that Node
and then applied. If no such style class is found it is just ignored. A Node
is the base class for any control.
方法 createDefaultSkin()
不会覆盖默认皮肤,正如您在评论中提到的那样,但它定义了默认皮肤(嗯,您部分正确,因为 CustomButton
extends 按钮
其中皮肤
被覆盖)。一般来说,一个控件由一个'control'类和一个'skin'类组成,至少在JavaFX到版本8时就是这种情况,当它发生变化时。有关详细信息,请参阅有关的文章。
The method createDefaultSkin()
does not override the default skin, as you mentioned in your comment, but it defines the default skin (Well you are partially correct as CustomButton
extends Button
which's Skin
is overriden). Generally a control is made up of a 'control' class and a 'skin' class, at least this was the case with JavaFX up till version 8, when it changed. See the article on the control architecture for full detail.
这篇关于理解getStyleClass()。add()和几行代码的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!