Полоса выбора
Недавно мне пришлось реализовать пользовательский элемент управления, который позволяет пользователю выбирать один элемент из списка элементов. Этот элемент управления «SelectionStrip» должен был размещать элементы по горизонтали, а в случае слишком большого количества элементов пользователь мог прокручивать по горизонтали влево и вправо. Элемент управления должен был использоваться в ограниченном пространстве, поэтому кнопки для прокрутки должны появляться только при необходимости. Они также не должны тратить дополнительное пространство при показе. Поэтому я решил разместить их на верхней панели управления слева и справа. Все это было легко достигнуто, за исключением того, что теперь было трудно отличить кнопки прокрутки от элементов. Это можно увидеть на трех изображениях ниже.
Альфа-канал?
Поэтому я подумал, что было бы неплохо как-то потушить предметы, когда они находятся близко к левому или правому краю. Такое поведение обычно может быть достигнуто с помощью альфа-канала. Это может уменьшить непрозрачность пикселей при уменьшении их расстояния до краев. ОК … но как это сделать в JavaFX? Некоторое время я смотрел на различные «режимы наложения», которые можно использовать для определения того, как два перекрывающихся узла рисуются друг на друге. Однако это было неправильное направление. Как оказалось, я уже мог знать, как это сделать, потому что однажды написал в блоге статью, в которой говорилось об отсечении и разнице между заполненным и незаполненным клипом . Но я думаю, это было слишком давно, и я не установил связь между «заполненным» и «заполненным непрозрачностью меньше 1».
Сложный клип!
До сих пор большинство клипов, которые я использовал для пользовательских элементов управления, были простыми прямоугольниками. Они обычно следили за тем, чтобы дочерние узлы, которые выходили за пределы макета родительского элемента управления, не были или только частично видны. Но этот клип был другим, он был более сложным. Он должен был определить три разные зоны. Область «постепенного появления» в левой части, область «полной непрозрачности» в центре и область «постепенного исчезновения» в правой части. Чтобы это работало, я определил «Группу», которая состоит из трех заполненных «Прямоугольных» узлов. Хотя цвет заливки центрального прямоугольника является сплошным черным, цвета заливки двух других прямоугольников являются линейными градиентами, переходящими от прозрачного к черному и наоборот. Изображение ниже иллюстрирует это.
С помощью этой настройки мы можем теперь добавить любой узел в качестве дочернего к панели стека, и он будет рисоваться с эффектами постепенного появления и исчезновения по бокам.
Результат
При применении к элементу управления «SelectionStrip» с самого начала стрелки / кнопки прокрутки теперь всегда хорошо видны, и общее впечатление от пользователя стало немного более приятным. Именно эти маленькие детали делают разницу между пользовательским интерфейсом, который считается «студенческим проектом» или «коммерческим приложением». Поэтому иногда в них действительно стоит потратить время.
Исходный код
Я поместил логику маскирования в пользовательский элемент управления под названием «MaskedView». Внизу этого поста вы увидите Gist (или ссылку на Gist), которая содержит исходный код этого элемента управления. Думайте об этом как обертка вокруг данного узла содержимого.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Node; import javafx.scene.control.Control; import javafx.scene.control.Skin; public class MaskedView extends Control { public MaskedView(Node content) { setContent(content); } @Override protected Skin<?> createDefaultSkin() { return new MaskedViewSkin( this ); } private final SimpleObjectProperty<Node> content = new SimpleObjectProperty<>( this , "content" ); public final Node getContent() { return content.get(); } public final SimpleObjectProperty<Node> contentProperty() { return content; } public final void setContent(Node content) { this .content.set(content); } private final DoubleProperty fadingSize = new SimpleDoubleProperty( this , "fadingSize" , 120 ); public final double getFadingSize() { return fadingSize.get(); } public final DoubleProperty fadingSizeProperty() { return fadingSize; } public final void setFadingSize( double fadingSize) { this .fadingSize.set(fadingSize); } } |
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
|
import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.SkinBase; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.paint.CycleMethod; import javafx.scene.paint.LinearGradient; import javafx.scene.paint.Stop; import javafx.scene.shape.Rectangle; public class MaskedViewSkin extends SkinBase { private final Rectangle leftClip; private final Rectangle rightClip; private final Rectangle centerClip; private final Group group; private final StackPane stackPane; public MaskedViewSkin(MaskedView view) { super (view); leftClip = new Rectangle(); rightClip = new Rectangle(); centerClip = new Rectangle(); centerClip.setFill(Color.BLACK); leftClip.setManaged( false ); centerClip.setManaged( false ); rightClip.setManaged( false ); group = new Group(leftClip, centerClip, rightClip); stackPane = new StackPane(); stackPane.setManaged( false ); stackPane.setClip(group); getChildren().add(stackPane); view.contentProperty().addListener((observable, oldContent, newContent) -> buildView(oldContent, newContent)); buildView( null , view.getContent()); view.widthProperty().addListener(it -> updateClip()); view.fadingSizeProperty().addListener(it -> updateClip()); } private final InvalidationListener translateXListener = it -> updateClip(); private final WeakInvalidationListener weakTranslateXListener = new WeakInvalidationListener(translateXListener); private void buildView(Node oldContent, Node newContent) { if (oldContent != null ) { stackPane.getChildren().clear(); oldContent.translateXProperty().removeListener(weakTranslateXListener); } if (newContent != null ) { stackPane.getChildren().setAll(newContent); newContent.translateXProperty().addListener(weakTranslateXListener); } updateClip(); } private void updateClip() { final MaskedView view = getSkinnable(); Node content = view.getContent(); if (content != null ) { final double fadingSize = view.getFadingSize(); if (content.getTranslateX() < 0 ) { leftClip.setFill( new LinearGradient( 0 , 0 , fadingSize, 0 , false , CycleMethod.NO_CYCLE, new Stop( 0 , Color.TRANSPARENT), new Stop( 1 , Color.BLACK))); } else { leftClip.setFill(Color.BLACK); } if (content.getTranslateX() + content.prefWidth(- 1 ) > view.getWidth()) { rightClip.setFill( new LinearGradient( 0 , 0 , fadingSize, 0 , false , CycleMethod.NO_CYCLE, new Stop( 0 , Color.BLACK), new Stop( 1 , Color.TRANSPARENT))); } else { rightClip.setFill(Color.BLACK); } } view.requestLayout(); } @Override protected void layoutChildren( double contentX, double contentY, double contentWidth, double contentHeight) { final double fadingSize = Math.min(contentWidth / 2 , getSkinnable().getFadingSize()); stackPane.resizeRelocate(snapPosition(contentX), snapPosition(contentY), snapSpace(contentWidth), snapSpace(contentHeight)); resizeRelocate(leftClip, snapPosition(contentX), snapPosition(contentY), snapSpace(fadingSize), snapSpace(contentHeight)); resizeRelocate(centerClip, snapPosition(contentX + fadingSize), snapPosition(contentY), snapSpace(contentWidth - 2 * fadingSize), snapSpace(contentHeight)); resizeRelocate(rightClip, snapPosition(contentX + contentWidth - fadingSize), snapPosition(contentY), snapSpace(fadingSize), snapSpace(contentHeight)); } private void resizeRelocate(Rectangle rect, double x, double y, double w, double h) { rect.setLayoutX(x); rect.setLayoutY(y); rect.setWidth(w); rect.setHeight(h); } } |
Я надеюсь, что вы найдете хороший вариант использования этого элемента управления.
Удачного кодирования всем!
Опубликовано на Java Code Geeks с разрешения Дирка Леммермана, партнера нашей программы JCG. Смотрите оригинальную статью здесь: JavaFX Совет 31: Маскировка / Обрезка / Альфа-канал
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |