JavaSE JFC技术 (AWT + Swing + Graphics2D):完全不改变原生Swing代码,换肤。
-->
源码打包:Swing_lnfImpl.zip (23.5Kb)
结构:
源码:
package com.han.lnf;
import javax.swing.*;
import javax.swing.plaf.LayerUI;
import javax.swing.plaf.synth.SynthLookAndFeel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.text.ParseException;
/**
* Class note: Created by Gaowen on 14-1-9.
*/
public class TestCase extends JPanel {
private static JLayer<JPanel> jLayer;
private static WaitLayerUI waitLayerUI;
private static Timer stopper;
private static JButton orderButton;
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
initLookAndFeel();// specify the L&F
final LayerUI<JPanel> spotlightLayerUI = new SpotlightLayerUI();
waitLayerUI = new WaitLayerUI();
stopper = new Timer(4000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
waitLayerUI.stop();
jLayer.setUI(spotlightLayerUI);
}
});
stopper.setRepeats(false);
JPanel contentPane = new TestCase();
jLayer = new JLayer<>(contentPane, spotlightLayerUI);
final JFrame f = new JFrame("Custom L&F");
f.add(jLayer);
f.getRootPane().setDefaultButton(orderButton);
f.setSize(290, 180);
f.setResizable(false);
f.setLocationRelativeTo(null);
f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
f.setVisible(true);
}
private static void initLookAndFeel() {
SynthLookAndFeel lookAndFeel = new SynthLookAndFeel();
try {
lookAndFeel.load( TestCase.class.getResourceAsStream("lnfimpl/synth.xml"), TestCase.class);
} catch (ParseException e) {
System.err.println("There is an error in parsing xml of Synth");
e.printStackTrace();
}
try {
UIManager.setLookAndFeel(lookAndFeel);
} catch (UnsupportedLookAndFeelException e) {
System.err.println("Synth is not a supported look and feel");
e.printStackTrace();
}
}
private static class SpotlightLayerUI extends LayerUI<JPanel> {
private boolean mActive;
private int mX, mY;
@Override
public void installUI(JComponent c) {
super.installUI(c);
@SuppressWarnings("unchecked")
JLayer<JPanel> jLayer = (JLayer<JPanel>) c;
jLayer.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
@Override
public void uninstallUI(JComponent c) {
super.uninstallUI(c);
@SuppressWarnings("unchecked")
JLayer<JPanel> jLayer = (JLayer<JPanel>) c;
jLayer.setLayerEventMask(0);
}
@Override
public void paint(Graphics g, JComponent c) {
super.paint(g, c);// paint the view
Graphics2D g2 = (Graphics2D) g.create();
if (mActive) {
/* Create a radial gradient, transparent in the middle */
Point2D center = new Point2D.Float(mX, mY);
float radius = 72;
float[] dist = {0.0f, 1.0f};
Color[] colors = {new Color(0.0f, 0.0f, 0.0f, 0.0f), Color.BLACK};
RadialGradientPaint p = new RadialGradientPaint(center, radius, dist, colors);
g2.setPaint(p);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .6f));
g2.fillRect(0, 0, c.getWidth(), c.getHeight());
}
g2.dispose();
}
@Override
protected void processMouseEvent(MouseEvent e, JLayer<? extends JPanel> l) {
if (e.getID() == MouseEvent.MOUSE_ENTERED) {
mActive = true;
} else if (e.getID() == MouseEvent.MOUSE_EXITED) {
mActive = false;
}
l.repaint();
}
@Override
protected void processMouseMotionEvent(MouseEvent e, JLayer<? extends JPanel> l) {
Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), l);
mX = p.x;
mY = p.y;
l.repaint();
}
}
private static class WaitLayerUI extends LayerUI<JPanel> implements ActionListener {
private boolean mIsRunning;
private boolean mIsFadingOut;
private Timer mTimer;
private int mAngle;
private int mFadeCount;
private final int mFadeLimit = 15;// immutable, so declared as final
@Override
public void paint(Graphics g, JComponent c) {
super.paint(g, c);// paint the view
if (!mIsRunning) return;
float fade = mFadeCount / mFadeLimit;
Graphics2D g2 = (Graphics2D) g.create();
/* Gray it out, in fact it uses the FOREGROUND of L&F */
Composite urComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f * fade));
int w = c.getWidth();
int h = c.getHeight();
g2.fillRect(0, 0, w, h);
g2.setComposite(urComposite);
/* Paint the wait indicator */
int s = Math.min(w, h) / 5;
int cx = w / 2;
int cy = h / 2;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setStroke(new BasicStroke(s / 4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setPaint(Color.white);
g2.rotate(Math.PI * mAngle / 180, cx, cy);
for (int i = 0; i < 12; i++) {
float scale = (11.0f - i) / 11.0f;
g2.drawLine(cx + s, cy, cx + s * 2, cy);
g2.rotate(-Math.PI / 6, cx, cy);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, scale * fade));
}
g2.dispose();
}
@Override
public void actionPerformed(ActionEvent e) {
if (mIsRunning) {
firePropertyChange("tick", 0, 1);
mAngle += 3;
if (mAngle >= 360) {
mAngle = 0;
}
if (mIsFadingOut) {
if (--mFadeCount == 0) {
mIsRunning = false;
mTimer.stop();
}
} else if (mFadeCount < mFadeLimit) {
mFadeCount++;
}
}
}
private void start() {
if (mIsRunning) return;
/* Run a thread for animation */
mIsRunning = true;
mIsFadingOut = false;
mFadeCount = 0;
int fps = 24;
int tick = 1000 / fps;
mTimer = new Timer(tick, this);
mTimer.start();
}
private void stop() {
mIsFadingOut = true;
}
@Override
public void applyPropertyChange(PropertyChangeEvent pce, JLayer<? extends JPanel> l) {
if ("tick".equals(pce.getPropertyName())) {
l.repaint();
}
}
}
TestCase() {
ButtonGroup entreeGroup = new ButtonGroup();
JRadioButton radioButton = new JRadioButton("Beef", true);
JRadioButton radioButton2 = new JRadioButton("Chicken");
JRadioButton radioButton3 = new JRadioButton("Vegetable");
entreeGroup.add(radioButton);
entreeGroup.add(radioButton2);
entreeGroup.add(radioButton3);
JCheckBox jCheckBox = new JCheckBox("Ketchup");
JCheckBox jCheckBox2 = new JCheckBox("Mustard");
JCheckBox jCheckBox3 = new JCheckBox("Pickles");
JLabel label = new JLabel("Special requests:");
JTextField tf = new JTextField(15);
final JLabel info = new JLabel();
info.setName("customLabel");
orderButton = new JButton("Place Order");
orderButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
info.setText("default button clicked");
if (stopper.isRunning()) return;
stopper.start();
waitLayerUI.start();
jLayer.setUI(waitLayerUI);
}
});
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
info.setText("cancel button clicked");
}
});
add(radioButton);
add(radioButton2);
add(radioButton3);
add(jCheckBox);
add(jCheckBox2);
add(jCheckBox3);
add(label);
add(tf);
add(orderButton);
add(cancelButton);
add(info);
}
}
皮肤:
<?xml version="1.0" encoding="UTF-8"?>
<synth>
<!-- A backing style, it is good practice to do this -->
<style id="backingStyle">
<opaque value="true"/>
<font name="Dialog" size="12"/>
<state>
<color type="BACKGROUND" value="#D8D987"/>
<color type="FOREGROUND" value="#9400d3"/>
<color type="TEXT_FOREGROUND" value="#3C3C3C"/>
</state>
</style>
<bind style="backingStyle" type="region" key=".*"/>
<style id="button">
<font name="Monotype Corsiva" size="18"/>
<property key="Button.defaultButtonFollowsFocus" type="boolean" value="false"/>
<!-- Shift the text one pixel when pressed -->
<property key="Button.textShiftOffset" type="integer" value="1"/>
<!-- set size of buttons -->
<insets top="0" left="10" bottom="0" right="10"/>
<!-- The state behavior is like an cascading filter -->
<state>
<imagePainter id="defaultStatePainter" method="buttonBackground"
path="lnfimpl/button/graphics/btn_general_normal.png" sourceInsets="3 3 3 3"/>
</state>
<state value="DISABLED">
<imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_disabled.png"
sourceInsets="3 2 4 3"/>
</state>
<state value="DEFAULT">
<imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_special.png"
sourceInsets="4 4 4 4"/>
</state>
<state value="PRESSED">
<imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_general_pressed.png"
sourceInsets="4 4 4 4"/>
</state>
<state value="MOUSE_OVER">
<imagePainter method="buttonBackground" path="lnfimpl/button/graphics/btn_general_hover.png"
sourceInsets="4 4 4 4"/>
</state>
<state value="FOCUSED">
<painter idref="defaultStatePainter" method="buttonBackground"/>
<object id="buttonFocusedPainter" class="com.han.lnf.lnfimpl.button.ButtonFocusedPainter"/>
<object id="bgColor" class="javax.swing.plaf.ColorUIResource">
<int>218</int>
<int>116</int>
<int>62</int>
</object>
<defaultsProperty key="Button.background" type="idref" value="bgColor"/>
<painter idref="buttonFocusedPainter" method="buttonBorder"/>
</state>
</style>
<bind style="button" type="region" key="Button"/>
<style id="textfield">
<!-- font here must support Chinese characters -->
<font name="Microsoft YaHei UI" size="11"/>
<color id="caretColor" value="MAGENTA"/>
<property key="TextField.caretForeground" type="idref" value="caretColor"/>
<insets top="3" left="5" bottom="4" right="5"/>
<state>
<color type="TEXT_FOREGROUND" value="#008b8b"/>
<color type="TEXT_BACKGROUND" value="#ffa349"/>
<imagePainter method="textFieldBorder" path="lnfimpl/textfield/graphics/text_field_normal.png"
sourceInsets="5 5 5 5" paintCenter="false"/>
</state>
<state value="DISABLED">
<imagePainter method="textFieldBorder" path="lnfimpl/textfield/graphics/text_field_disabled.png"
sourceInsets="5 5 5 5" paintCenter="true"/>
</state>
<state value="FOCUSED">
<imagePainter method="textFieldBorder" path="lnfimpl/textfield/graphics/text_field_pressed.png"
sourceInsets="5 5 5 5" paintCenter="false"/>
</state>
</style>
<bind style="textfield" type="region" key="TextField"/>
<style id="label">
<font name="Impact" size="12"/>
</style>
<bind style="label" type="region" key="Label"/>
<style id="customLabel" clone="label">
<font name="Comic Sans MS" size="12"/>
</style>
<bind style="customLabel" type="name" key="custom.*"/>
<style id="checkbox">
<font name="Segoe Script" size="12"/>
<insets top="4" left="2" bottom="4" right="4"/>
<imageIcon id="check_off_normal" path="lnfimpl/checkbox/graphics/cb_un_normal.png"/>
<imageIcon id="check_off_disable" path="lnfimpl/checkbox/graphics/cb_un_disable.png"/>
<imageIcon id="check_off_pressed" path="lnfimpl/checkbox/graphics/cb_un_pressed.png"/>
<imageIcon id="check_on_normal" path="lnfimpl/checkbox/graphics/cb_normal.png"/>
<imageIcon id="check_on_disable" path="lnfimpl/checkbox/graphics/cb_disable.png"/>
<imageIcon id="check_on_pressed" path="lnfimpl/checkbox/graphics/cb_pressed.png"/>
<state>
<property key="CheckBox.icon" value="check_off_normal"/>
</state>
<state value="SELECTED and DISABLED">
<property key="CheckBox.icon" value="check_on_disable"/>
</state>
<state value="SELECTED and PRESSED">
<property key="CheckBox.icon" value="check_on_pressed"/>
</state>
<state value="SELECTED and FOCUSED">
<property key="CheckBox.icon" value="check_on_normal"/>
<object id="checkboxFocusedPainter" class="com.han.lnf.lnfimpl.checkbox.CheckBoxFocusedPainter"/>
<object id="bgColor" class="javax.swing.plaf.ColorUIResource">
<int>29</int>
<int>88</int>
<int>32</int>
</object>
<defaultsProperty key="CheckBox.background" type="idref" value="bgColor"/>
<painter idref="checkboxFocusedPainter" method="checkBoxBackground"/>
</state>
<state value="SELECTED and ENABLED">
<property key="CheckBox.icon" value="check_on_normal"/>
</state>
<state value="DISABLED">
<property key="CheckBox.icon" value="check_off_disable"/>
</state>
<state value="PRESSED">
<property key="CheckBox.icon" value="check_off_pressed"/>
</state>
<state value="FOCUSED">
<painter idref="checkboxFocusedPainter" method="checkBoxBackground"/>
</state>
</style>
<bind style="checkbox" type="region" key="CheckBox"/>
<style id="radiobutton">
<font name="Segoe Print" size="12"/>
<insets top="4" left="2" bottom="4" right="4"/>
<imageIcon id="radio_off_normal" path="lnfimpl/radiobutton/graphics/rb_un_normal.png"/>
<imageIcon id="radio_off_disable" path="lnfimpl/radiobutton/graphics/rb_un_disable.png"/>
<imageIcon id="radio_off_pressed" path="lnfimpl/radiobutton/graphics/rb_un_pressed.png"/>
<imageIcon id="radio_on_normal" path="lnfimpl/radiobutton/graphics/rb_normal.png"/>
<imageIcon id="radio_on_disable" path="lnfimpl/radiobutton/graphics/rb_disable.png"/>
<imageIcon id="radio_on_pressed" path="lnfimpl/radiobutton/graphics/rb_pressed.png"/>
<state>
<property key="RadioButton.icon" value="radio_off_normal"/>
</state>
<state value="SELECTED and DISABLED">
<property key="RadioButton.icon" value="radio_on_disable"/>
</state>
<state value="SELECTED and PRESSED">
<property key="RadioButton.icon" value="radio_on_pressed"/>
</state>
<state value="SELECTED and FOCUSED">
<property key="RadioButton.icon" value="radio_on_normal"/>
<object id="radiobuttonFocusedPainter" class="com.han.lnf.lnfimpl.radiobutton.RadioButtonFocusedPainter"/>
<painter idref="radiobuttonFocusedPainter" method="radioButtonBackground"/>
</state>
<state value="SELECTED and ENABLED">
<property key="RadioButton.icon" value="radio_on_normal"/>
</state>
<state value="DISABLED">
<property key="RadioButton.icon" value="radio_off_disable"/>
</state>
<state value="PRESSED">
<property key="RadioButton.icon" value="radio_off_pressed"/>
</state>
<state value="FOCUSED">
<painter idref="radiobuttonFocusedPainter" method="radioButtonBackground"/>
</state>
</style>
<bind style="radiobutton" type="region" key="RadioButton"/>
<style id="panel">
<object id="panelPainter" class="com.han.lnf.lnfimpl.panel.PanelPainter"/>
<object id="backgroundColor" class="javax.swing.plaf.ColorUIResource">
<int>255</int>
<int>255</int>
<int>255</int>
</object>
<defaultsProperty key="Panel.background" type="idref" value="backgroundColor"/>
<object id="foregroundColor" class="javax.swing.plaf.ColorUIResource">
<int>200</int>
<int>200</int>
<int>200</int>
</object>
<defaultsProperty key="Panel.foreground" type="idref" value="foregroundColor"/>
<painter idref="panelPainter" method="panelBackground"/>
</style>
<bind style="panel" type="region" key="Panel"/>
</synth>
package com.han.lnf.lnfimpl.button;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.UIManager;
import javax.swing.plaf.synth.SynthContext;
import javax.swing.plaf.synth.SynthPainter;
public class ButtonFocusedPainter extends SynthPainter {
@Override
public void paintButtonBorder(SynthContext context, Graphics g, int x, int y, int w, int h) {
Graphics2D g2 = (Graphics2D) g.create();
float[] arr = {8, 2, 2, 6};
Color background = UIManager.getColor("Button.background");
g2.setColor(background);
g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 4, arr, 0));
g2.drawRoundRect(x + 4, y + 4, w - 8, h - 8, 10, 10);
g2.dispose();
}
}
package com.han.lnf.lnfimpl.checkbox;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.UIManager;
import javax.swing.plaf.synth.SynthContext;
import javax.swing.plaf.synth.SynthPainter;
public class CheckBoxFocusedPainter extends SynthPainter {
@Override
public void paintCheckBoxBackground(SynthContext context, Graphics g, int x, int y, int w, int h) {
Graphics2D g2 = (Graphics2D) g.create();
float[] arr = {8, 2, 2, 6};
Color background = UIManager.getColor("CheckBox.background");
g2.setColor(background);
g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 4, arr, 0));
g2.drawRoundRect(x + 28, y + 7, w - 30, h - 13, 10, 10);
g2.dispose();
}
}
package com.han.lnf.lnfimpl.panel;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.CubicCurve2D;
import javax.swing.UIManager;
import javax.swing.plaf.synth.SynthContext;
import javax.swing.plaf.synth.SynthPainter;
public class PanelPainter extends SynthPainter {
@Override
public void paintPanelBackground(SynthContext context, Graphics g, int x, int y, int w, int h) {
Graphics2D g2 = (Graphics2D) g.create();
Color background = UIManager.getColor("Panel.background");
g2.setColor(background);
g2.fillRect(x, y, w, h);
Color foreground = UIManager.getColor("Panel.foreground");
g2.setColor(foreground);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
CubicCurve2D.Double arc2d = new CubicCurve2D.Double(0, h / 4d, w / 3d, h / 10d, .67 * w, 1.5 * h, w, h / 8d);
g2.draw(arc2d);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g2.dispose();
}
}
package com.han.lnf.lnfimpl.radiobutton;
import java.awt.BasicStroke;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.plaf.synth.SynthContext;
import javax.swing.plaf.synth.SynthPainter;
public class RadioButtonFocusedPainter extends SynthPainter {
@Override
public void paintRadioButtonBackground(SynthContext context, Graphics g, int x, int y, int w, int h) {
Graphics2D g2 = (Graphics2D) g.create();
float[] arr = {8, 2, 2, 6};
g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 4, arr, 0));
g2.drawRoundRect(x + 28, y + 8, w - 30, h - 14, 10, 10);
g2.dispose();
}
}
由于本人不是专门做美工的,所以界面有定制的功能,但是定制的不算漂亮~望喜欢
这个和Android的xml皮肤比较像,不过我觉得Android的更强大,因为它xml里面可以使用NinePatch Tech。不过幸运的是,安卓的NinePatch技术可以由源码过度到JavaSE Swing里,但是必须是Java paint代码。。所以Synth可以使用NinePatch技术,但不如Android直接在XML中描述方便。
利用 Synth 可以创建出完全专业的外观,Java 1.4 中发布的 GTK+ 和 Windows XP 外观就完全是用 Synth 创建的。(那时它不是一个已公布的 API。)
由于我上面的例子实践中用了20张png图片,加载速度以及内存占用比Metal的L&F可能差一点,不过如果是复杂界面或者创建数量多的Components时,Synth和Metal还是差不多的。比如,我这个例子中(XP + JDK7u45 + Eclipse Kepler)如果注销掉
initLookAndFeel();// specify the L&F
这行代码,得到的Metal经典外观时占用的内存为25Kb,如果不注销,使用的定制的外观时内存为29Kb,这些内存和启动速度我觉得都比JavaFX应用要好。
如果使用100 个组件的界面来测试:
当时设计时遇到一个问题:
当使用自定义感官时,比如我上面的L&F或者使用jgoodies的UIManager.setLookAndFeel(new com.jgoodies.looks.windows.WindowsLookAndFeel());
则如果定义文本框等,像JTextField,JTextPane时,在用智能拼音输入时,输入窗口会出现方框形式的乱码。取消这种观感的设置既可解决问题(比如注销掉initLookAndFeel();// specify the L&F)。原因是这种观感不支持中文。怎么让自定义的感官支持呢?
解决了,是因为字体的原因,Arial不支持中文的显示。改为雅黑就行了,可以使用Component的getFont()以及Font的canDisplay(int codePoint)或者canDisplayUpTo(String)来判断是否支持特定的字符显示。