JavaFX自定义组件——添加自定义的css属性

目标

  1. 可以通过css类名选中控件。自定义一个组件,让这个给这个组件一个默认的css样式类,让别人可以通过.xxx来选中这个组件,进而给这个自定义的组件样式,如.button{这里写-fx开头的样式}
.hua-avatar {

}

如上,实现目标1后,别人可以选中这个控件,进而通过css的方式,在里面写-fx开头的样式,但只能写-fx开头的(部分visibility除外),也就是说,只能写官方支持的属性。

  1. 自定义css属性。选中这个自定义控件后,可以通过自定义属性,不用默认的-fx开头的属性,如
.hua-avatar {
    -hua-avatar-type: "SQUARE";
}

1实现可以通过css类名选中控件


public class HuaAvatar extends Control {

	private static final String DEFAULT_STYLE_CLASS = "hua-avatar";
    public HuaAvatar() {
        getStyleClass().add(DEFAULT_STYLE_CLASS);
    }
}

分析:自定义组件需要继承于 Control,具体为什么要继承。因为Control是全部UI控件的基类,具体可以看JavaFX的继承体系。
上面就实现了一个构造函数,里面就一句话,给当前控件添加一个默认的样式。
这样,我们就完成了第一步,给这个自定义的控件一个默认的样式名。

2 实现自定义的CSS属性

为了实现自定义控件的css属性,我们需要先来看一下官方的Control控件的javafx.scene.control.Control#getCssMetaData 方法


    /**
     * This method returns a {@link List} containing all {@link CssMetaData} for
     * both this Control (returned from {@link #getControlCssMetaData()} and its
     * {@link Skin}, assuming the {@link #skinProperty() skin property} is a
     * {@link SkinBase}.
     *
     * <p>Developers who wish to provide custom CssMetaData are therefore
     * encouraged to override {@link Control#getControlCssMetaData()} or
     * {@link SkinBase#getCssMetaData()}, depending on where the CssMetaData
     * resides.
     * @since JavaFX 8.0
     */
    @Override
    public final List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
        if (styleableProperties == null) {

            // RT-29162: make sure properties only show up once in the list
            java.util.Map<String, CssMetaData<? extends Styleable, ?>> map =
                new java.util.HashMap<String, CssMetaData<? extends Styleable, ?>>();

            List<CssMetaData<? extends Styleable, ?>> list =  getControlCssMetaData();

            for (int n=0, nMax = list != null ? list.size() : 0; n<nMax; n++) {

                CssMetaData<? extends Styleable, ?> metaData = list.get(n);
                if (metaData == null) continue;

                map.put(metaData.getProperty(), metaData);
            }

            //
            // if both control and skin base have the same property, use the
            // one from skin base since it may be a specialization of the
            // property in the control. For instance, Label has -fx-font and
            // so does LabeledText which is Label's skin.
            //
            list =  skinBase != null ? skinBase.getCssMetaData() : null;

            for (int n=0, nMax = list != null ? list.size() : 0; n<nMax; n++) {

                CssMetaData<? extends Styleable, ?> metaData = list.get(n);
                if (metaData == null) continue;

                map.put(metaData.getProperty(), metaData);
            }

            styleableProperties = new ArrayList<CssMetaData<? extends Styleable, ?>>();
            styleableProperties.addAll(map.values());
        }
        return styleableProperties;
    }

先大概了解一下CssMetaData,这个就是css中的一个样式,再通俗点,就是,css中设置一行,每一个分号;结尾的,都需要转化为这个cssMetaData的一个对象。所以,css文件,都被转换为了CssMetaData列表集合。
上面代码最重要的2句如下:

	 List<CssMetaData<? extends Styleable, ?>> list =  getControlCssMetaData();
	list =  skinBase != null ? skinBase.getCssMetaData() : null;

第一句是通过调用 getControlCssMetaData() 获取控件的cssMetaData
第二句是通过 skinBase.getCssMetaData() 获取 皮肤的cssMetaData
所以,我们可以正如官方建议说,如果要实现自定义的css属性的话,需要重写getControlCssMetaData() 方法或SkinBase.getCssMetaData()
本例中,只有重写了 getControlCssMetaData()

那么,因为 Control这个控件,他默认就有一些CssMetaData,我们自定义的时候,千万不能给他覆盖没了,必须给他加上,所以,我们首先需要的是获取原来 Control控件自己的CssMetaData 列表。
那么,思路就是,重写这个getControlCssMetaData() 方法,先获取原来Control有的CssMetaData,我再添加自己单独的CssMetaData,再组合起来返回。

那么,你可能很简单的写出下面代码:

    @Override
    protected List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
        final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());

		// styleables.add("自己是自定义的cssMetaData");
         List<CssMetaData<? extends Styleable, ?>> STYLEABLES  = Collections.unmodifiableList(styleables);
        return STYLEABLES;
    }

但是呢,这样写会有很大的问题。
问题1):想一下,一个控件在渲染的时候,肯定不止一次的加载,比如,我一个页面上放了10个Button组件,那么,每一个组件都需要走一遍这个 getControlCssMetaData方法重新调用一遍相同的逻辑。
问题2):这样写会导致使用Scene Builder进行页面布局时,在右侧的style下拉中选择不到自定义的css属性,就不可以达到下图的操作了。
在这里插入图片描述

解决方法

问题1的解决方法:
我们可以将上面的 STYLEABLES 变量,保存成一个类静态变量,这样,就只会进行一次初始化。

问题2的解决方法:
为了解决这个问题,我看了一下官方控件的写法,他们,都有一个 getClassCssMetaData 的静态方法,在这个方法,返回刚才问题1分析中的保存的控件全部的CssMetaData静态变量。如下:


public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
        return STYLEABLES;
    }

但是,如果你去看源码的话,会发现源码中其中,内部还有一个静态内部类,他将所有自定义的css属性,及有关刚才css等操作,都封装到一个叫 private static class StyleableProperties 的静态内部类中。这样,我们就可以参考官方的写法,来改造我们的代码。

到目前为止,你的代码应该长这样

	/**
	* 直接去调用 getClassCssMetaData 静态方法,返回所有的本类的CssMetaData
	*/
    @Override
    protected List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
        return getClassCssMetaData();
    }

    /**
     * 此为静态方法,为了能让自定义属性在Scene Builder中可以下拉选择
     *
     * @return 返回与该类关联的CssMetaData,其中包括其父类的CssMetaData。
     */
    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
        return StyleableProperties.STYLEABLES;
    }

// 一个静态内部类,关于自定义CSS属性的添加原来CSS不支持的,如 -hua-avatar-type
    private static class StyleableProperties {
        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;        // -hua-avatar-type 这个属性名称,可以在scene builder中的style中下拉选择到
        static {
            final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());

			// styleables.add("这里就是自定义的CssMetaData了");
            STYLEABLES = Collections.unmodifiableList(styleables);
        }
    }

好的,终于到最后一步了,我们需要定义一个 new CssMetaData 对象

CssMetaData

下面就来详细说一下这个CssMetaData
我们在css文件中写的每一行,他是怎么对应到我们的javafx的颜色,字体,大小等属性的呢?
我们先想一下,他底层一定是进行了将css中的文本,转换为了对应的java对象的。
因为,我们不用css文件写样式,只用java代码也是可以进行设置样式的。
那么,先看下面的css

.hua-fill-button {
    -fx-background-color: white;
    -fx-border-color: #d1d1d1;
    -fx-border-width: 1px;
    -rx-fill: #616dff;
}

这个css,在进行转换的时候,css文件中的每一行(每个;结尾就是一行),都会被转为一个CssMetaData这个类的对象,那么,我们要想支持自定义的css,首先要定义一个 CssMetaData 对象。
看下图可知,一个 CssMetaData 对象的构造函数有4个,最少的就是2个,第一个参数就是你自定义的css属性的名称了,比如,我这里叫 -hua-avatar-type
那么,第二个StyleConverter,就是转换器了。那么,什么是转换器呢??
在这里插入图片描述
转换器,就是告诉java,这个字符串我读取的时候,需要怎么转换他,我们 来看一下 这个 StyleConverter
在这里插入图片描述
根据上面的说明,一个字符串转换为颜色的转换器,其中的F为string类型,T为Color类型.

官方默认提供了很多的转换器。常用的,颜色转换器,字体转换器等,如下图:
在这里插入图片描述
这里,我用到的是一个 EnumConverter 转换器的,直接传入一个枚举类,其内部就可以将我们css设置的值进行转换成对应枚举类型。
下面是我设置的枚举类型。

public enum Type {
        /**
         * 圆形头像
         */
        CIRCLE,
        /**
         * 正方形头像
         */
        SQUARE,
        /**
         * 六边形 水平
         */
        HEXAGON_H,
        /**
         * 六边形 垂直
         */
        HEXAGON_V
    }

那么,我在写css时,就只能从这4个值里面选择了。
下面是我的CssMetaData对象

	private static final CssMetaData<HuaAvatar, Type> AVATAR_TYPE_CSS_META_DATA = new CssMetaData<>("-hua-avatar-type", new EnumConverter<>(Type.class), Type.SQUARE) {
            @Override
            public boolean isSettable(HuaAvatar avatar) {
                return (avatar.shapeType == null || !avatar.shapeType.isBound());
            }

            @Override
            public StyleableProperty<Type> getStyleableProperty(HuaAvatar avatar) {
                return avatar.shapeTypeProperty();
            }
        };

第三个参数,我传递了一个默认值,圆形。
当我们new一个CssMetaData对象时,要求重写2个方法,第1个方法返回当前属性是否可以设置,第2个方法返回绑定的property属性。这里为什么又要和一个属性进行绑定呢?
这个很容易理解,当我们动态改变这个css属性的值时,若想要其组件动态的进行改变,一定要有一个Property属性,进行监听,一旦他发生修改了,会调用set方法,在set方法内部,又会调用观察者模式那一套,去通知所有这个属性绑定的事件。进而使修改及时生效。

那么,总体下来流程已经清晰了,当读取到这个自定义组件设置的css样式时,先获取这个组件全部的CssMetaData,再根据其对应的每个转换器,将css转换为对应的java类型,进而,设置到 getStyleableProperty 方法返回的属性中。

当然,这里不是全部的代码,接下来,需要监听这个属性的变化,进而进行一些操作。这里就不再开展了,想要看代码的朋友,可以查看下面源代码。

最后声明一下,这个组件库是一个叫 梦想哥的写的,我只是花了点时间研究一下其源码,学习一下,进而后面再考虑自己写组件库。

这里放上大佬的github,喜欢的可以去看看,本文写的代码,大部分都是那个自定义头像组件库。

leewyatt的github

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值