Swing实现模仿HTML5模拟时钟特效

25 篇文章 0 订阅
5 篇文章 0 订阅

  自从研究了Java Swing的Graphics,觉得能按照自己的想法画出一些东西,还是挺有意思的,之前学习Java Swing的时候练习过一个模拟时钟,自己觉得不错,后来看到这个html5 canvas实现的模拟时钟(HTML5 canvas超逼真的模拟时钟特效),瞬间觉得自己之前的实现弱爆了,这个真是漂亮,忍不住想用自己有限的Swing Graphics知识山寨一个。看下山寨后的效果图吧

基本是最终效果了
  这是mac的运行效果,虽然有点锯齿感,总体还行,比html5还是差不少,h5的有指针的阴影效果,看起来比较有立体感,h5的表盘数字位置也比较准确,这两方面还差点,还有刻度的圆角矩形比例什么的还差点。ubuntu linux运行后,锯齿感非常明显,可能显卡驱动不行吧,windows目前没运行。

在这里插入图片描述
可以看出ubuntu linux的锯齿感非常强,代码开启下抗锯齿试试看

在这里插入图片描述
omg, 看来是没有开启抗锯齿效果的原因,开启后效果立马提升了几个档次……坐标轴只是个参考可以去掉,最新代码解决了时间数字与刻度贴合不够近的问题,算是比较完善了。

  模仿实现思路如下:继承Jpanel重写paintComponent方法。

1.绘制表盘时间刻度

  这一圈刻度看起来像圆角矩形,只是大小、长短有所不同。

  • 方案1:使用的是圆角矩形RoundRectangle进行的实现,然而矩形默认只能水平或垂直,这时候就需要使用Graphics的rotate方法,将画布进行旋转,其实我觉得根本上来说,旋转的是画布中的坐标系,画布其实没转,一圈60个刻度,循环60次,每5个画一个略大的矩形即可。
  • 方案2:不需要调用rotate旋转,通过改变画笔线条的粗细(stroke)和线条末端的形状(end_round)即可,通过BasicStroke构造方法传递相应的参数实现。显然这种方法更为简单方便。代码示例如下(kotlin实现,java也是一样道理)
    var line: Line2D.Float
    val big = BasicStroke(10.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
    val small = BasicStroke(5.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
    for (i in 0 until 60) {
        var x1 = 0f
        var y1 = 0f
        var x2 = 0f
        var y2 = 0f
        if (i % 5 == 0) {
            x1 = (Math.cos(theta * i) * radiusLong).toFloat()
            y1 = (Math.sin(theta * i) * radiusLong).toFloat()
            x2 = (Math.cos(theta * i) * radiusEnd).toFloat()
            y2 = (Math.sin(theta * i) * radiusEnd).toFloat()
            line = Line2D.Float(x1,y1,x2,y2)
            g2.stroke = big
        } else {
            x1 = (Math.cos(theta * i) * (radiusLong+5)).toFloat()
            y1 = (Math.sin(theta * i) * (radiusLong+5)).toFloat()
            x2 = (Math.cos(theta * i) * radiusEnd).toFloat()
            y2 = (Math.sin(theta * i) * radiusEnd).toFloat()
            line = Line2D.Float(x1,y1,x2,y2)
            g2.stroke = small
        }
        g2.draw(line)
    }

  之前文章draw heart的时候,以为graphics的形状不支持浮点数,是自己当时所知有限了,graphics2d提供了基本所有形状的2D版本,既支持float还支持double。如Line2D,Rectangle2D,RoundRect2D,Arc2D,Ellipse2D等等,它们都实现了Shape接口。

  绘制刻度的时候,起始点其实是在x轴的正方向,也就是相当于3点钟方向,循环开始以后,是从这点顺时针开始绘制的,在swing graphics坐标系里面y轴的正方向是向下的,android里面也是这样,即使在windows里面也是,h5的canvas也是,看来这个在好多平台是统一的吧,研究了一下h5的代码实现,发现它在计算完y的值以后乘以了-1,也就是取反了,开始觉得很奇怪,在代码里面试了一下,发现这样相当于把y轴的正方向给改变为指向上方了,相当于变成了数学几何里面的那种标准平面直角坐标系,这样绘制的时候就是从3点钟,逆时针绘制了,殊途同归。下面是逆时针示例,顺时针看代码吧
在这里插入图片描述
  通过查看H5的js实现代码,发现并没有处理从三点钟后退90度的操作,内心一直对此存着疑问,突然想起自己的实现使用的是斜线与x轴夹角进行计算实现的,那么他可能用的是斜线与y轴夹角进行的实现,仔细看了下,确实如此。默认情况,这种实现是从6点钟方向逆时针绘制,但是他将y坐标的值进行了取反后,一下就变成了从12点钟进行顺时针绘制了,厉害。这个时候的斜线与圆的交点坐标(x, y)的计算方式就变了:
x = r ∗ s i n ( t ) x = r * sin(t) x=rsin(t)
y = r ∗ c o s ( t ) y = r * cos(t) y=rcos(t)
依然可以参考下图的坐标示意图,只是夹角变成了斜线与y轴的夹角。

2.绘制表盘时间数字

  主要是12个数字,分别对应于表盘上面的时间刻度,这个实现的时候需要考虑角度(弧度)的一个移动的问题,因为我们是从3点钟开始绘制的,如果不考虑这个问题,那就需要从3画到12,再画1,2。或者(顺时针实现方法)减去相应的度数,这样可以从1画到12。目前数字与刻度贴合的不够近…通过Graphics测量字符串宽度已经解决了

    var numRadius = radius * 0.9f
    g2.font = Font("", Font.PLAIN, 22)
    var fontMetrics = getFontMetrics(g2.font)
    //draw numbers
    var numTheta = 2 * Math.PI / 12
    for (i in 0..11) {
        var x1 = Math.cos(numTheta * i - Math.PI / 3) * numRadius
        var y1 = Math.sin(numTheta * i - Math.PI / 3) * numRadius
        //println("x1:$x1, y1:$y1")
        // 测量字符串宽度
        var num = (i + 1).toString()
        var strW = fontMetrics.stringWidth(num)
        g2.drawString(num, (x1 - strW / 2).toFloat(), y1.toFloat())
    }

3.绘制时间指针

  这个不同指针的先后顺序要注意,按照这个h5的效果,先画时针、分针、秒针,然后在它们上面还有覆盖的一个圆圈,秒针有一个尾巴超出圆圈的覆盖在另一侧露出来了,这个细节也要实现。这些实现后,定义时分秒三个变量,取系统当前的时间进行分别赋值,运行后即可实现时间指向。
  接着让时钟的指针动起来,需要创建一个Timer每一秒更新一次时间(启动一个线程也行),这样实现指针位置的变化。主要思路就是秒数或分钟数、小时数计算为对应的角度,这样计算出一个点的坐标,然后坐标原点(0,0)为另外一个点,在这两点进行drawLine即可,公式如下:
x = r ∗ c o s ( t ) x = r * cos(t) x=rcos(t)
y = r ∗ s i n ( t ) y = r * sin(t) y=rsin(t)
  下面这个图片中斜线与圆的交点的坐标(x,y)就是这样计算出来的,三条线围起来的三角形,斜线(相当于斜边,值为圆的半径)与x轴的夹角t的cos三角函数值的乘积就得到了交点的x值,同样斜线与t的sin的乘积可得到交点的y值。这样弧度从0到2PI,即可实现获取一圈的坐标点,一定要用弧度,角度有问题。这个画图软件不支持文字(其实是对ubuntu画图软件还不太熟),大致就是这么个效果。
在这里插入图片描述
  这个时间的分针和时针还有一个问题,就是H5这个实现是,秒针每走一次,分针都会相应变化的,而不是秒针走完一圈,分针一下从一个点指向下一个点,同理时针也是这个样子的。分针的实现是使用两个刻度之间的度数乘以当前(秒数/60),这个计算出来的度数加到分针的计算度数中即可实现,通过查看H5的js源代码发现,它不是这样实现的,它的分钟数是浮点数,分钟数 = 当前分钟数 + 当前秒数/60,这样也同样的实现了这个效果,殊途同归。时针也是一样的道理……

4.绘制时钟品牌名称

  在钟表的6点钟以上,圆心以下绘制了2行文字,大小不同,一个是品牌名称,一个是所属country,需要测量一下文字字符串的宽度,再绘制比较准确。

5.根据系统时间进行时间的动态更新

  可以使用Timer,每1000毫秒更新一次表盘的指针指向;也可以启动一个线程,sleep1000毫秒后通知ui线程更新表盘指针。

6.最终动态效果

  这个gif有闪烁,不是太优雅…又给它去掉了

在这里插入图片描述

  到这里山寨H5时钟的效果就全部结束了,虽然还有没完全模仿到的地方(比如指针的阴影效果,或许Android平台可以支持?下回分解吧),后续再研究吧(Swing Graphics路还很长),更多代码(有java实现)可以参考github:https://github.com/ximen502/SwingLearn

  这里给出kotlin的完整实现代码

/**
 * 第三版高仿H5模拟时钟部分代码优化
 */
class ClockFrameKt3 : JFrame() {
    init {
        val clockPanel = ClockPanel()
        add(clockPanel)
        clockPanel.start()
    }

    inner class ClockPanel : JPanel() {
        val secondHandColor = Color(0xf3, 0xa8, 0x29)
        val minuteHandColor = Color(0x22, 0x22, 0x22)
        val hourHandColor = Color(0x22, 0x22, 0x22)
        val markColor = Color(0x22, 0x22, 0x22)
        var hour = 0
        var minute = 0
        var second = 0

        override fun paintComponent(g: Graphics?) {
            super.paintComponent(g)
            var g2: Graphics2D = g as Graphics2D
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
            g2.translate(width / 2, height / 2)
            //drawAxis(g2)

            val theta = 2 * Math.PI / 60
            val radius = 150f
            val radiusLong = 164f
            val radiusEnd = 178
            g2.color = markColor
            var line: Line2D.Float
            val big = BasicStroke(10.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
            val small = BasicStroke(5.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
            for (i in 0 until 60) {
                var x1 = 0f
                var y1 = 0f
                var x2 = 0f
                var y2 = 0f
                val cost = Math.cos(theta * i)
                val sint = Math.sin(theta * i)
                // hour marker
                if (i % 5 == 0) {
                    x1 = (cost * radiusEnd * 0.90).toFloat()
                    y1 = (sint * radiusEnd * 0.90).toFloat()
                    x2 = (cost * radiusEnd).toFloat()
                    y2 = (sint * radiusEnd).toFloat()
                    line = Line2D.Float(x1,y1,x2,y2)
                    g2.stroke = big
                } else {
                    // minute marker
                    x1 = (cost * radiusEnd * 0.95).toFloat()
                    y1 = (sint * radiusEnd * 0.95).toFloat()
                    x2 = (cost * radiusEnd).toFloat()
                    y2 = (sint * radiusEnd).toFloat()
                    line = Line2D.Float(x1,y1,x2,y2)
                    g2.stroke = small
                }
                g2.draw(line)
            }

            var numRadius = radius * 0.9f
            g2.font = Font("", Font.PLAIN, 22)
            var fontMetrics = getFontMetrics(g2.font)
            //draw numbers
            var numTheta = 2 * Math.PI / 12
            for (i in 0..11) {
                var x1 = Math.cos(numTheta * i - Math.PI / 3) * numRadius
                var y1 = Math.sin(numTheta * i - Math.PI / 3) * numRadius
                //println("x1:$x1, y1:$y1")
                // 测量字符串宽度
                var num = (i + 1).toString()
                var strW = fontMetrics.stringWidth(num)
                g2.drawString(num, (x1 - strW / 2).toFloat(), y1.toFloat())
            }

            var font = Font("",Font.PLAIN, 14)
            var fontSmall = Font("",Font.PLAIN, 10)
            val brand = "北极星"
            val brandPlace = "亚洲"
            fontMetrics = getFontMetrics(font)
            var bW = fontMetrics.stringWidth(brand)
            g2.font = font
            g2.drawString(brand, 0 - bW / 2, radius.toInt() / 2)
            fontMetrics = getFontMetrics(fontSmall)
            var bpW = fontMetrics.stringWidth(brandPlace)
            g2.font = fontSmall
            g2.drawString(brandPlace, 0 - bpW / 2, radius.toInt() / 2 + 15)


            var x1 = 0f
            var y1 = 0f
            var x2 = 0f
            var y2 = 0f

            var hourTheta = 2 * Math.PI / 12
            //draw hour hand
            //修线条粗细
            var basicStroke = BasicStroke(20.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
            g2.stroke = basicStroke
            g2.color = hourHandColor
            /* hourTheta * (minute / 60f)分针变化对时针的影响 2pi/12*(minute/60) */
            x2 = radius * 0.75f * Math.cos(hourTheta * hour + hourTheta * (minute / 60f) - Math.PI / 2).toFloat()
            y2 = radius * 0.75f * Math.sin(hourTheta * hour + hourTheta * (minute / 60f) - Math.PI / 2).toFloat()
            var hourLine = Line2D.Float(x1, y1, x2, y2)
            g2.draw(hourLine)

            //println("second:$second, ${theta * (second / 60f)}")

            //draw minute hand
            basicStroke = BasicStroke(9.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
            g2.stroke = basicStroke
            g2.color = minuteHandColor
            /* theta * (second / 60f)秒针变化对分针的影响 //2pi/60 * (second/60)*/
            x2 = radiusLong * 1.0f * Math.cos(theta * (minute) + theta * (second / 60f) - Math.PI / 2).toFloat()
            y2 = radiusLong * 1.0f * Math.sin(theta * (minute) + theta * (second / 60f) - Math.PI / 2).toFloat()
            var minuteLine = Line2D.Float(x1, y1, x2, y2)
            g2.draw(minuteLine)

            //draw second hand
            basicStroke = BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
            g2.stroke = basicStroke
            g2.color = secondHandColor
            x2 = radiusLong * 1.08f * Math.cos(theta * (second) - Math.PI / 2).toFloat()
            y2 = radiusLong * 1.08f * Math.sin(theta * (second) - Math.PI / 2).toFloat()
            var secondLine = Line2D.Float(x1, y1, x2, y2)
            g2.draw(secondLine);

            //draw second tail handle
            basicStroke = BasicStroke(13.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)
            g2.stroke = basicStroke
            g2.color = secondHandColor
            x2 = radiusLong * 0.1f * Math.cos(theta * (second) - Math.PI / 2).toFloat()
            y2 = radiusLong * 0.1f * Math.sin(theta * (second) - Math.PI / 2).toFloat()
            secondLine = Line2D.Float(x1, y1, -x2, -y2)
            g2.draw(secondLine);

            // draw circle cover three hands
            var worh = 16f
            var circle = Ellipse2D.Float(0f - worh / 2f, 0f - worh / 2f, worh, worh)
            g2.draw(circle)

        }

        fun drawAxis(g: Graphics?) {
            var g2: Graphics2D = g as Graphics2D
            g2.stroke = BasicStroke(1.0f)
            g2.color = Color.BLACK
            g2.drawLine(-width / 2, 0, width / 2, 0)
            g2.drawLine(0, -height / 2, 0, height / 2)
            //unit=10,vertical line,x1,y1,x2,y2
            // short line, long line
            val sl = 5
            val ll = 10
            //x axis
            for (i in 0..width / 2 step 10) {
                if (i % 50 == 0) {
                    g2.drawLine(i, 0, i, -ll)
                    g2.drawLine(-i, 0, -i, -ll)
                } else {
                    g2.drawLine(i, 0, i, -sl)
                    g2.drawLine(-i, 0, -i, -sl)
                }
            }
            //println(height / 2)
            //y axis
            for (i in 0..height / 2 step 10) {
                if (i % 50 == 0) {
                    g2.drawLine(0, i, ll, i)
                    g2.drawLine(0, -i, ll, -i)
                } else {
                    g2.drawLine(0, i, sl, i)
                    g2.drawLine(0, -i, sl, -i)
                }
            }
        }

        override fun getPreferredSize(): Dimension {
            return Dimension(250, 250)
        }

        private fun setCurrentTime(hour: Int, minute: Int, second: Int) {
            this.hour = hour
            this.minute = minute
            this.second = second
            repaint()
        }

        fun start() {
            var c = 0
            var timer = Timer(1000) {
                val calendar = Calendar.getInstance()
                var hour = calendar.get(Calendar.HOUR_OF_DAY)
                var minute = calendar.get(Calendar.MINUTE)
                var second = calendar.get(Calendar.SECOND)
                setCurrentTime(hour % 12, minute, second)
            }
            timer.start()
        }
    }

    //转弧度
    fun toRadians(deg: Float): Float {
        return ((Math.PI / 180) * deg).toFloat();
    }

}

fun main(args: Array<String>) {
    var frame = ClockFrameKt3()
    frame.apply {
        setSize(500, 450)
        title = "Kotlin clock"
        setLocationRelativeTo(null) // Center the frame

        defaultCloseOperation = JFrame.EXIT_ON_CLOSE
        isVisible = true
    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个Java Swing模拟实现QQ登录界面的示例代码: ```java import javax.swing.*; import java.awt.*; import java.awt.event.*; public class QQLogin extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public QQLogin() { // 设置窗口标题 setTitle("QQ登录"); // 设置窗口大小 setSize(300, 200); // 设置窗口居中 setLocationRelativeTo(null); // 设置窗口关闭时退出程序 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 创建用户名、密码输入框和登录、取消按钮 JLabel usernameLabel = new JLabel("用户名:"); usernameField = new JTextField(20); JLabel passwordLabel = new JLabel("密 码:"); passwordField = new JPasswordField(20); JButton loginButton = new JButton("登录"); JButton cancelButton = new JButton("取消"); // 创建一个面板来放置输入框和标签 JPanel inputPanel = new JPanel(new GridLayout(2, 2)); inputPanel.add(usernameLabel); inputPanel.add(usernameField); inputPanel.add(passwordLabel); inputPanel.add(passwordField); // 创建一个面板来放置按钮 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); buttonPanel.add(loginButton); buttonPanel.add(cancelButton); // 将面板添加到窗口中 setLayout(new BorderLayout()); add(inputPanel, BorderLayout.CENTER); add(buttonPanel, BorderLayout.SOUTH); // 为登录按钮添加事件监听器 loginButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String username = usernameField.getText(); char[] passwordChars = passwordField.getPassword(); String password = new String(passwordChars); // TODO: 在这里编写登录逻辑 JOptionPane.showMessageDialog(QQLogin.this, "登录成功!"); } }); // 为取消按钮添加事件监听器 cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.exit(0); } }); } public static void main(String[] args) { QQLogin qqLogin = new QQLogin(); qqLogin.setVisible(true); } } ``` 这个示例代码创建了一个窗口,包含一个用户名输入框、一个密码输入框和两个按钮:登录和取消。当用户点击登录按钮时,程序将获取用户名和密码,并在控制台输出。您可以在这里添加登录逻辑,比如将用户名和密码发送到后端进行验证。当用户点击取消按钮时,程序将退出。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值