记一次有趣的时钟效果复刻实现过程

淘晶驰串口屏:复刻苹果时钟,指针控件保姆级实战教程 本文基于淘晶驰串口屏,从RTC模块获取时间数据,通过指针控件(z0/z1/z2)实现时钟的时针、分针、秒针动态效果。重点解析12小时制转换逻辑及分针/时针的0.6°/秒和0.5°/分钟细腻移动算法,还原机械表真实走动体验。涵盖控件配置、代码逻辑、变量设计及硬件测试 阅读详情

前言

上班摸鱼的某个上午,一铁哥们突然发了个链接给我,说这个效果顶不顶、啥水平,效果如下:

git效果图看起来是有点点卡的,实际效果还是很好看的!!!

目前已实现并且上传到MavenCentral,详细用法可以去项目README里面看,项目地址:

https://github.com/fastcv-cc/LineNumberClock

觉得可以的给个Star!!!

本着学习的目的(主要是他问我能不能做出来这样的效果,都问了肯定是能做的!!),我开始思考这玩意怎么去实现?

思路

如何将整个效果拆分出来?

首先,我们根据效果来看,我想到的第一个思路就是:

将整个内容分成了很多个小圆,然后每个小圆根据自己的职责显示不同的内容,小圆的类型分为以下几种:

  • 基础圆,带个白色圆心点
  • 数字部分的圆,负责数字的线性拼接效果
  • 分隔圆,带个黑色圆心点

然后根据View的宽高,将整个View区域划分出14 * 3个小圆,然后去控制不同的圆,在不同的时间点显示对应的效果,但是,延伸思考之后,发现了以下这些问题:

  • 我得去仔细的找每个数字变化时,线条的变化过程的规律,然后根据时间、数字的变化,去控制每个小圆的显示效果,这样想想都头疼。
  • 得仔细的编码,精确的控制小圆坐标,这也太麻烦了。

基于这个想法的基础上,我衍生了第二个想法,画区域!!!

既然我们是做数字时钟,那整块区域就被分为了以下几个部分:

我们只需要将整个区域的小圆,划分成这几个区域既可,按照这个图,我们的区域大体分为两种类型:

  • 时间类型
  • 分割线类型

然后不同区域的小圆所拥有的小圆坐标集合如下:

分割线类型有两个坐标集合:

  • [ [0,4] , [1,4] , [2,4] ]
  • [ [0,9] , [1,9] , [2,9] ]

每个分割线拥有3个小圆,所以,将大坐标系传入转化为分割线内部坐标系时,分割线就是一个长度为3的一维坐标数组了。

时间类型有三个坐标集合:

  • 小时 [ [[0-2] , [0-3]] ]
  • 分钟 [ [[0-2] , [5-8]] ]
  • 秒 [ [[0-2] , [10-13]] ]

每个时间类型拥有12个小圆,所以,将大坐标系传入转化为时间类型内部坐标系时,时间类型就是一个 4 x 3 二维坐标数组了。

效果如下:

在这个基础上面,我们可以无需关心小圆了,我们只需要关系分割线和时间类型的实现思路了。

分割线的效果如何实现?

根据UI效果:

可以看到,分割线内,除了基础小圆的UI效果外,它只是针对中间小圆,修改了下圆心点的颜色而已。

所以针对分割线的做法就是,将它持有的坐标系中[0 , 1] 这个小圆做特殊处理即可。

时间类型的效果如何实现?

在这里很容易陷入一个牛角尖,就是这个变化效果我该如何实现?

我刚开始也在这个问题上思考了一会,比如:

  • 数字之间的变化(动画)效果,我要怎么实现?写死0-1,1-2,…,9-0的过度效果?
  • 按照毫秒去显示变化? 那一毫秒刷新一次? 一秒钟1000帧?

很明显,这两个问题或者说思路,现实起来都有很大的问题,那我们不如先简单化,一步一步的走,我们先考虑时间固定显示要如何实现?这个问题又可以简化为单个数字要如何显示?

单个数字要如何显示?

我们先观察下0-9的显示效果,如下图:

我们拿数字6举例子,我们将数字占有的坐标区域分为6个部分:

根据安卓角度规则,各下标部分显示线条的角度为:

而数字1的各下标部分显示线条的角度为:

定义小圆基础类

从这些样式和数据可以看出,我们每个小圆最多有2条线,最少0条,而且每条线还要能控制显示隐藏。所以这里我们定义一个小圆绘制参数类:

data class CircleDrawParam(
    var line1Angle: Float,
    var line1Alpha: Int,
    var line2Angle: Float,
    var line2Alpha: Int
)

用 line1Angle 和 line1Alpha 控制线段1的角度和显示/隐藏。用 line2Angle 和 line2Alpha 控制线段2的角度和显示/隐藏。

在这个基础上面,定义出小圆绘制对象类:

//一个圆单元参数
class CircleDrawer {

    var x: Float = 0.0f
    var y: Float = 0.0f
    var radius: Float = 0.0f
    var paint: Paint? = null


    //绘制基础圆
    fun drawBaseCircle(canvas: Canvas) {
        paint?.let {
            //画圆
            //TODO 定义颜色
            it.color = Color.parseColor("#F4EBEB")
            canvas.drawCircle(x, y, radius, it)
            //画小白点
            it.color = Color.WHITE
            canvas.drawCircle(x, y, it.strokeWidth / 2, it)
        }
    }

    //绘制圆中心点
    fun drawBlackPoint(canvas: Canvas) {
        paint?.let {
            //画小黑点
            it.color = Color.BLACK
            canvas.drawCircle(x, y, it.strokeWidth / 2, it)
        }
    }

    //通过绘制参数绘制线段
    fun drawByCircleDrawParam(
        canvas: Canvas,
        param: CircleDrawParam
    ) {
        paint?.let {
            //画小黑点
            it.color = Color.BLACK
            it.alpha = param.line1Alpha
            canvas.drawCircle(x, y, it.strokeWidth / 2, it)
            //画线1
            canvas.save()
            canvas.translate(x, y)
            canvas.rotate(param.line1Angle)
            it.alpha = param.line1Alpha
            canvas.drawLine(0f, 0f, radius, 0f, it)
            canvas.restore()
            canvas.save()
            canvas.translate(x, y)
            canvas.rotate(param.line2Angle)
            it.alpha = param.line2Alpha
            canvas.drawLine(0f, 0f, radius, 0f, it)
            canvas.restore()
        }
    }

}

定义组件类

在CircleDrawer的基础上,我将分割线和时间类型定义为时钟View中的两类组件:

/**
 * 数字时钟组件类型
 * 这里格局ui效果分为两个类型:
 * - 时间组件
 * - 分割线组件
 */
enum class ComponentsType {
    COMPONENTS_TYPE_TIME, COMPONENTS_SPLIT_LINE
}
abstract class AbsComponents {

    abstract val type: ComponentsType

    abstract fun draw(canvas: Canvas)

}

分割线组件

而分割线组件类的实现

/**
 * 数字分割线 由3个圆点组成
 */
class SplitLineComponents(
    private val array: Array<CircleDrawer>,
) : AbsComponents() {

    override fun draw(canvas: Canvas) {
        for ((index, circle) in array.withIndex()) {
            circle.drawBaseCircle(canvas)
            if (index == 1) {
                circle.drawBlackPoint(canvas)
            }
        }
    }

    override val type: ComponentsType
        get() = ComponentsType.COMPONENTS_SPLIT_LINE

}

时间类型组件

而时间类型的实现就相对比较复杂了。

通过UI效果可以看出,一个时间由两个数字组成,每个数字由6个小圆组成。

定义数字类

所以这个定义数字参数类

/**
 * 数字绘制参数
 * 指代每个数据默认的绘制参数
 * 每个数字由6个圆点组成
 */
abstract class AbsNumberDrawParam {
    abstract val params: Array<CircleDrawParam>
}

数字0-9的数字参数类的实现如下:

class Number0 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(), 90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), 180f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(), -90f, (1f * 255).toInt())
    )
}

class Number1 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),135f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (1f * 255).toInt())
    )
}

class Number2 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(),180f, (1f * 255).toInt())
    )
}

class Number3 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(),-90f, (1f * 255).toInt())
    )
}

class Number4 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(90f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
    )
}

class Number5 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 180f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

class Number6 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 180f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

class Number7 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
    )
}

class Number8 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( 180f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

class Number9 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam( 0f, (1f * 255).toInt(),90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( 90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam( 0f, (1f * 255).toInt(),0f, (1f * 255).toInt()),
        CircleDrawParam( -90f, (1f * 255).toInt(),180f, (1f * 255).toInt()),
    )
}

而数字绘制对象的定义如下:

/**
 * 指的是数字时钟中的一个数字单位
 * 从设计图中可以看出 一个数字单位 由6个圆点组成
 * 然后需要具体数字的绘制参数
 * @param array 传入的6个圆点信息数组
 * @param number 当前数字图形(绘制)参数
 */
class NumberProxy(
    private val array: Array<Array<CircleDrawer>>,
    private val number: AbsNumberDrawParam
) {

    //绘制数字
    fun draw(canvas: Canvas) {
        for ((index, minimalUhrCircles) in array.withIndex()) {
            for ((index1, minimalUhrCircle) in minimalUhrCircles.withIndex()) {
                minimalUhrCircle.drawBaseCircle(canvas)
                minimalUhrCircle.drawByCircleDrawParam(canvas, number.params[index * 2 + index1])
            }
        }
    }

}
定义时间组件类

而时间类的组件由两个数字组成,我将它们分为左、右两个数字对象:

class TimeComponents(private val array: Array<Array<CircleDrawer>>) :
    AbsComponents() {
    override val type: ComponentsType
        get() = ComponentsType.COMPONENTS_TYPE_TIME

    private var newLeftNumber: AbsNumberDrawParam? = null
    private var newRightNumber: AbsNumberDrawParam? = null

    fun setNumber(number: Int) {
        newLeftNumber = transformNumber(number / 10)
        newRightNumber = transformNumber(number % 10)
    }

    override fun draw(canvas: Canvas) {
        val leftNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][0], array[0][1]),
                arrayOf(array[1][0], array[1][1]),
                arrayOf(array[2][0], array[2][1]),
            ), newLeftNumber!!
        )
        leftNumber.draw(canvas)
        val rightNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][2], array[0][3]),
                arrayOf(array[1][2], array[1][3]),
                arrayOf(array[2][2], array[2][3]),
            ), newRightNumber!!
        )
        rightNumber.draw(canvas)

    }

    private fun transformNumber(i: Int): AbsNumberDrawParam? {
        return when (i) {
            0 -> Number0()
            1 -> Number1()
            2 -> Number2()
            3 -> Number3()
            4 -> Number4()
            5 -> Number5()
            6 -> Number6()
            7 -> Number7()
            8 -> Number8()
            9 -> Number9()
            else -> null
        }
    }
}

定义组件管理类

由于需要将整个区域的小圆进行初始化,并且要将初始化后的小圆数字拆分到各个组件里面,所以我们需要一个管理类来处理这些逻辑。

这里我们总共分成了5个组件:

  • 小时时间类型组件
  • 分割线1组件
  • 分钟时间类型组件
  • 分割线2组件
  • 秒时间类型组件
class ComponentsManager {

    //小圆半径
    private var radius = 0f

    //圆之间的间距
    private var circleMargin = 0f

    //原点半径
    private var pointRadius = 0f

    //宽高比
    private var aspectRatio = 0f

    //使用的宽高
    private var useWidth = 0f
    private var useHeight = 0f

    //小圆的二维数组
    private val circleArray = Array(3) {
        return@Array Array(14) {
            CircleDrawer()
        }
    }

    //画线的画笔
    private val paint = Paint().apply {
        setColor(Color.BLACK)
        style = Paint.Style.FILL
    }

    private lateinit var hourComponents: TimeComponents
    private lateinit var split1Components: SplitLineComponents
    private lateinit var minuteComponents: TimeComponents
    private lateinit var split2Components: SplitLineComponents
    private lateinit var secondComponents: TimeComponents

    fun initConfigureInfo(context: Context, attrs: AttributeSet?) {
        radius = dp2px(context, 20)
        circleMargin = dp2px(context, 4)
        pointRadius = dp2px(context, 2)
        paint.strokeWidth = circleMargin
        aspectRatio =
            (14 * radius * 2 + circleMargin * 15) * 1.0f / (3 * radius * 2 + circleMargin * 4)
    }

    fun initComponents(w: Int, h: Int) {
        val realAspectRatio = w * 1.0f / h
        if (realAspectRatio > aspectRatio) {
            //以高为基准
            useHeight = h * 1.0f
            useWidth = useHeight * aspectRatio
        } else {
            //以宽为基准
            useWidth = w * 1.0f
            useHeight = useWidth / aspectRatio
        }
        //重新计算圆半径
        radius = (useWidth - circleMargin * 15) / 14 / 2
        //以画布左上角为原点计算小圆的分布和分组
        countCirclePosition()
    }

    fun draw(canvas: Canvas) {
        val calendar: Calendar = Calendar.getInstance()
        val hour = calendar[Calendar.HOUR_OF_DAY]
        val minute = calendar[Calendar.MINUTE]
        val second = calendar[Calendar.SECOND]
        hourComponents.setNumber(hour)
        minuteComponents.setNumber(minute)
        secondComponents.setNumber(second)
        drawComponents(canvas)
    }

    private fun drawComponents(canvas: Canvas) {
        hourComponents.draw(canvas)
        split1Components.draw(canvas)
        minuteComponents.draw(canvas)
        split2Components.draw(canvas)
        secondComponents.draw(canvas)
    }

    private fun countCirclePosition() {
        //根据属性计算所有圆的位置
        initAllCircleParamArray()
        initHourComponents()
        initSplit1Components()
        initMinuteComponents()
        initSplit2Components()
        initSecondComponents()
    }

    private fun initSecondComponents() {
        secondComponents = TimeComponents(
            arrayOf(
                arrayOf(
                    circleArray[0][10], circleArray[0][11], circleArray[0][12], circleArray[0][13]
                ),
                arrayOf(
                    circleArray[1][10], circleArray[1][11], circleArray[1][12], circleArray[1][13]
                ),
                arrayOf(
                    circleArray[2][10], circleArray[2][11], circleArray[2][12], circleArray[2][13]
                ),
            )
        )
    }

    private fun initSplit2Components() {
        split2Components = SplitLineComponents(
            arrayOf(
                circleArray[0][9],
                circleArray[1][9],
                circleArray[2][9],
            )
        )
    }

    private fun initMinuteComponents() {
        minuteComponents = TimeComponents(
            arrayOf(
                arrayOf(circleArray[0][5], circleArray[0][6], circleArray[0][7], circleArray[0][8]),
                arrayOf(circleArray[1][5], circleArray[1][6], circleArray[1][7], circleArray[1][8]),
                arrayOf(circleArray[2][5], circleArray[2][6], circleArray[2][7], circleArray[2][8]),
            )
        )
    }

    private fun initSplit1Components() {
        split1Components = SplitLineComponents(
            arrayOf(
                circleArray[0][4],
                circleArray[1][4],
                circleArray[2][4],
            )
        )
    }

    private fun initHourComponents() {
        hourComponents = TimeComponents(
            arrayOf(
                arrayOf(circleArray[0][0], circleArray[0][1], circleArray[0][2], circleArray[0][3]),
                arrayOf(circleArray[1][0], circleArray[1][1], circleArray[1][2], circleArray[1][3]),
                arrayOf(circleArray[2][0], circleArray[2][1], circleArray[2][2], circleArray[2][3]),
            )
        )
    }

    private fun initAllCircleParamArray() {
        var positionY: Float
        var positionX: Float
        for (i in 0 until 3) {
            positionY = (i + 1) * circleMargin + (1 + 2 * i) * radius * 1.0f
            for (i1 in 0 until 14) {
                positionX = (i1 + 1) * circleMargin + (1 + 2 * i1) * radius * 1.0f
                circleArray[i][i1].x = positionX
                circleArray[i][i1].y = positionY
                circleArray[i][i1].radius = radius
                circleArray[i][i1].paint = paint
            }
        }
    }

    private fun dp2px(context: Context, dpValue: Int): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, dpValue.toFloat(), context.resources.displayMetrics
        )
    }
}

通过管理类,我们控制小圆半径、间距、绘制颜色等等。

定义线段数字时钟View类

而View类的定义就很简单了,因为逻辑全部交由管理类进行了。

class LineNumberClockView : View {

    constructor(context: Context?) : super(context) {
        initParams(context, null)
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        initParams(context, attrs)
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        initParams(context, attrs)
    }

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        initParams(context, attrs)
    }

    private val manager = ComponentsManager()

    private fun initParams(context: Context?, attrs: AttributeSet?) {
        if (context == null) {
            return
        }
        manager.initConfigureInfo(context, attrs)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //计算最小宽高值
        manager.initComponents(w, h)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        manager.draw(canvas)
    }
}

在Activity中显示如下:

至此,我们使用线段显示时间的功能实现了,但是,我们想要的是动起来的时钟,这个不是我们想要的,这样最多定个1秒刷一次更新时间,没有那炫酷的数字过度动画效果怎么能行。那这个效果如何实现了?

动画效果如何实现?

结合上面的实现方式,我们应该知道了,每个数字依赖于6个小圆的绘制参数进行绘制的,0-9每个数字的参数是固定的,那有没有一种不固定的数字参数,用它来绘制动态的效果?

于是我们定义出这个类TransitionNumberDrawParam,它用于保存动画过程中临时的数字绘制参数。

/**
 * 过渡数字绘制参数
 */
class TransitionNumberDrawParam(override val params: Array<CircleDrawParam>) : AbsNumberDrawParam()

那这个类怎么计算得出呢?举个例子。

我们看下数字1到2的参数对比

class Number0 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(0f, (1f * 255).toInt(), 90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), 180f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(0f, (1f * 255).toInt(), -90f, (1f * 255).toInt()),
        CircleDrawParam(180f, (1f * 255).toInt(), -90f, (1f * 255).toInt())
    )
}

class Number1 : AbsNumberDrawParam() {
    override val params = arrayOf(
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),135f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(90f, (1f * 255).toInt(),-90f, (1f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (0f * 255).toInt()),
        CircleDrawParam(-90f, (0f * 255).toInt(),-90f, (1f * 255).toInt())
    )
}

是不是好像就是4个参数的变化。

没错,我们可以直接通过进度控制两个数字对象相减,计算出过渡数字参数,我们定义个AbsNumberDrawParam的扩展方法用以计算TransitionNumberDrawParam,而这个方法内部就是相应的两个小圆的参数做减法处理。

//计算过渡数字绘制参数
fun AbsNumberDrawParam.transition(newNumber: AbsNumberDrawParam, progress: Float): AbsNumberDrawParam {
    val circleDrawParam0 = this.params[0].transition(newNumber.params[0],progress)
    val circleDrawParam1 = this.params[1].transition(newNumber.params[1],progress)
    val circleDrawParam2 = this.params[2].transition(newNumber.params[2],progress)
    val circleDrawParam3 = this.params[3].transition(newNumber.params[3],progress)
    val circleDrawParam4 = this.params[4].transition(newNumber.params[4],progress)
    val circleDrawParam5 = this.params[5].transition(newNumber.params[5],progress)
    return TransitionNumberDrawParam(
        arrayOf(
            circleDrawParam0,
            circleDrawParam1,
            circleDrawParam2,
            circleDrawParam3,
            circleDrawParam4,
            circleDrawParam5,
        )
    )
}

小圆参数类添加transition方法

package cc.fastcv.line_number_clock

/**
 * 圆绘制参数
 * angle: -180 ~ 180
 * alpha: 0 ~ 255
 */
data class CircleDrawParam(
    var line1Angle: Float,
    var line1Alpha: Int,
    var line2Angle: Float,
    var line2Alpha: Int
) {
    //参数变更差计算
    fun transition(newParam: CircleDrawParam, progress: Float): CircleDrawParam {
        val tempLine1Angle: Float = line1Angle + (newParam.line1Angle - line1Angle) * progress
        val tempLine1Alpha: Int =
            (line1Alpha + (newParam.line1Alpha - line1Alpha) * progress).toInt()
        val tempLine2Angle: Float = line2Angle + (newParam.line2Angle - line2Angle) * progress
        val tempLine2Alpha: Int =
            (line2Alpha + (newParam.line2Alpha - line2Alpha) * progress).toInt()
        return CircleDrawParam(tempLine1Angle, tempLine1Alpha, tempLine2Angle, tempLine2Alpha)
    }
}

新增了个参数计算方法得到临时的CircleDrawParam,然后组合成TransitionNumberDrawParam,最后在两个数字变换的过程中,使用TransitionNumberDrawParam来显示效果。

那得到中间过程的产物了,我们就把动画相关的逻辑在LineNumberClockView中加上。

    private var progress = 999

    private val handler = Handler(Looper.getMainLooper())

    private val showRunnable = Runnable {
        showInAnim()
        showNext()
    }

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000)
    }

    private fun showInAnim() {
        progress = 0
        val anim = ValueAnimator.ofInt(0, 999).apply {
            addUpdateListener {
                progress = it.animatedValue as Int
                invalidate()
            }
            duration = 650L
        }
        anim.start()
    }

这里我们考虑到使用了Handler重复的发送message,所以要和生命周期绑定在一起,不然可能会产生内存泄漏,于是又加上了DefaultLifecycleObserver

class LineNumberClockView : View, DefaultLifecycleObserver {

    ...
    
    fun bindLifecycle(lifecycle: Lifecycle) {
        lifecycle.addObserver(this)
    }

    ...

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        postDelayed(showRunnable, 1000L - Calendar.getInstance().get(Calendar.MILLISECOND))
    }

    override fun onStop(owner: LifecycleOwner) {
        super.onStop(owner)
        handler.removeCallbacks(showRunnable)
    }

    ...
}

最后,我们将progress传入下层去控制绘制进度。完整的LineNumberClockView如下:

class LineNumberClockView : View, DefaultLifecycleObserver {

    constructor(context: Context?) : super(context) {
        initParams(context, null)
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        initParams(context, attrs)
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        initParams(context, attrs)
    }

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        initParams(context, attrs)
    }

    private val manager = ComponentsManager()

    private fun initParams(context: Context?, attrs: AttributeSet?) {
        if (context == null) {
            return
        }
        manager.initConfigureInfo(context, attrs)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //计算最小宽高值
        manager.initComponents(w, h)
    }

    private var progress = 999

    private val handler = Handler(Looper.getMainLooper())

    private val showRunnable = Runnable {
        showInAnim()
        showNext()
    }

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000)
    }

    fun bindLifecycle(lifecycle: Lifecycle) {
        lifecycle.addObserver(this)
    }

    private fun showInAnim() {
        progress = 0
        val anim = ValueAnimator.ofInt(0, 999).apply {
            addUpdateListener {
                progress = it.animatedValue as Int
                invalidate()
            }
            duration = 650L
        }
        anim.start()
    }

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        postDelayed(showRunnable, 1000L - Calendar.getInstance().get(Calendar.MILLISECOND))
    }

    override fun onStop(owner: LifecycleOwner) {
        super.onStop(owner)
        handler.removeCallbacks(showRunnable)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        manager.draw(canvas, progress)
    }
}

与之对应的manager的draw方法也要添加参数

    fun draw(canvas: Canvas, progress: Int) {
        val calendar: Calendar = Calendar.getInstance()
        val hour = calendar[Calendar.HOUR_OF_DAY]
        val minute = calendar[Calendar.MINUTE]
        val second = calendar[Calendar.SECOND]
        val drawProgress = (progress + 1) * 1f / 1000
        hourComponents.setNumberAndProgress(hour,drawProgress)
        minuteComponents.setNumberAndProgress(minute,drawProgress)
        secondComponents.setNumberAndProgress(second,drawProgress)
        drawComponents(canvas)
    }

然后就是时间类型的组件的setNumber方法改成setNumberAndProgress

    fun setNumberAndProgress(number: Int, process: Float) {
        newLeftNumber = transformNumber(number / 10)
        newRightNumber = transformNumber(number % 10)
        drawProgress = process
    }

与之对应的绘制方法也要改动

    override fun draw(canvas: Canvas) {
        if (drawProgress == 1f) {
            lastLeftNumber = newLeftNumber
            lastRightNumber = newRightNumber
        }
        val leftTempNumber: AbsNumberDrawParam =
            lastLeftNumber!!.transition(newLeftNumber!!, drawProgress)
        val rightTempNumber: AbsNumberDrawParam =
            lastRightNumber!!.transition(newRightNumber!!, drawProgress)
        val leftNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][0], array[0][1]),
                arrayOf(array[1][0], array[1][1]),
                arrayOf(array[2][0], array[2][1]),
            ), leftTempNumber
        )
        leftNumber.draw(canvas)
        val rightNumber = NumberProxy(
            arrayOf(
                arrayOf(array[0][2], array[0][3]),
                arrayOf(array[1][2], array[1][3]),
                arrayOf(array[2][2], array[2][3]),
            ), rightTempNumber
        )
        rightNumber.draw(canvas)
    }

到这里我们的动画效果就编码完成了,运行看下效果。

完整的代码可以去仓库里面查看,地址如下,可以的话,给个Star!!!

https://github.com/fastcv-cc/LineNumberClock

遇到的问题

动画时间和我们设置的不一样?

在使用过程中,会看到某些时候,动画速度突然变快,我觉得很奇怪,我们不是设置的好好的吗?为啥会这样?

从效果中可以看出,几乎没有了动画效果,这是为啥呢?

经过我一顿分析,原来是在启动动画的时机存在问题

    private val handler = Handler(Looper.getMainLooper())

    private val showRunnable = Runnable {
        showInAnim()
        showNext()
    }

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000)
    }

    private fun showInAnim() {
        progress = 0
        val anim = ValueAnimator.ofInt(0, 999).apply {
            addUpdateListener {
                progress = it.animatedValue as Int
                invalidate()
            }
            duration = 650L
        }
        anim.start()
    }

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        post(showRunnable)
    }

通过代码我们可以知道,在界面恢复可交互的时候,我们会去启动这个Handler的1s定时任务,然后在动画过程中通过对比上次的时间来计算动画效果。

理想情况是我在初始化的时候会设置当前秒,比如7s,然后在8s的0ms的时候启动动画,会在650ms内完成上一秒到下一秒动画过渡,然后在9s 0ms的时候启动下一次动画,依次反复。

问题就在这里,如果现在初始化的时候就是7s,然后启动动画的时机是7s 500ms的时候,此时记录的上次的秒是7,然后获取的当前的秒也是7,那么动画时长650ms中的前500ms都是7 -7 的变化,等于是没有变化。

然后最后150ms的时候,等于是进度值比较大的时候开始的 7 - 8的变化,所以动画效果很快。

然后记录下来上次的秒是8,下次启动的时间是8s 500ms,就会一直出现相同的问题。

于是我们控制下启动时间点,控制到下一秒0ms的时候启动,修改onResume方法的内容

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        postDelayed(showRunnable, 1000L - System.currentTimeMillis() % 1000L)
    }

这样修改完,就可以解决上面的问题了。

但是吧,跑一段时间后,还是出现了相同的问题。啊这?

不是解决了吗?为啥还会出现?

这里我们陷入了一个误区,就是就算在7秒0ms的时候定时下一秒执行的任务,Handler很难在8秒0ms的时候启动这个任务,多多少少会有几毫米的延迟,那么一定时间后,就会出现上面说的问题。

那么解决方案也很简单,就是尽量控制任务在8秒0ms附近启动。修改showNext方法

    private fun showNext() {
        handler.postDelayed(showRunnable, 1000L - System.currentTimeMillis() % 1000L)
    }

至此,目前就没发现类似的问题了。

PPT复刻Windows7界面:从Aero效果到交互实现的完整指南 用户界面设计是软件开发中的重要环节,它通过视觉元素和交互逻辑构建用户与系统的沟通桥梁。在UI设计领域,PowerPoint因其强大的动画和形状处理能力,常被用于快速原型制作。通过合理运用PPT的触发器动画、超链接和VBA脚本,可以实现复杂的交互效果,这在产品演示和设计验证中具有重要价值。本教程以复刻Windows7经典界面为例,详细解析如何利用PPT模拟Aero玻璃效果、任务栏预览等核心功能,为UI设计初学者和PPT爱好者提供实用的工程实践方案。 阅读详情

相关推荐

从零DIY拟辉光管时钟:用WS2812B与ESP8266复刻复古美学

在嵌入式开发与创客项目中,LED灯带(如WS2812B)因其可编程、色彩丰富的特性,成为模拟传统显示器件(如辉光管、VFD屏)视觉效果的热门选择。其工作原理是通过独立的RGB LED像素点,配合PWM信号精确控制每个点的颜色与亮度,从而实现复杂的动态光效。这项技术的核心价值在于,它让爱好者能够以较低的成本和安全的低压方案,复刻经典器件的独特美学,并融入现代智能控制功能。典型的应用场景包括桌面艺术摆件、环境氛围灯、以及复古风格的信息显示器。本文以制作一个拟辉光管时钟为例,详细解析了如何利用WS2812B灯带和

weixin_29165465的博客 345

玩原神学编程-原神时钟

原神里面有一个可以玩家自己调节时间的时钟,看着挺不错的,所以由“钟(感)”而发,利用vue3 + TS复刻一下原神时钟|自定义css变量|扇区设计|页面设计|原神启动

weixin_45785873的博客 1235

从零打造拟辉光管时钟:用WS2812B与ESP32复刻复古电子美学

在嵌入式系统与数字显示领域,可编程LED(如WS2812B)和微控制器(如ESP32)是构建智能照明与交互项目的核心技术。其原理在于通过单总线通信协议,实现对每个LED灯珠颜色与亮度的独立精准控制,这为动态视觉效果创造了硬件基础。这项技术的工程价值在于将复杂的灯光控制简化为易于编程和集成的模块,极大降低了创意实现的硬件门槛。在应用场景上,它广泛用于氛围照明、艺术装置和个性化电子产品的开发。本文聚焦于一个具体而迷人的应用:利用WS2812B和ESP32,结合导光板与3D打印技术,模拟复古辉光管(Nixie T

weixin_28273593的博客 308

JS实现仿IOS时钟

JS实现仿IOS时钟 文章目录JS实现仿IOS时钟成果图HTML以及CSS部分JS部分最终效果 最近都在练习一些小Demo,来提高对JS的掌握度,当然这其中也顺便温习了HTML和CSS,今天又花了一些时间仿写了一个IOS时钟,虽然还是在网上找的教程,但还是实现了练手的效果,每天进步一点点吧。 成果图 我这里因为不是放的动图,所以时钟并没有转动,实际效果中是可以获取本地时间并且转动的。 HTM...

weixin_43212739的博客 971

从零开始:淘晶驰串口屏复刻苹果时钟的5个关键步骤(含代码解析)

本文详细介绍了使用淘晶驰串口屏复刻苹果时钟的5个关键步骤,包括环境配置、RTC时间处理、指针角度计算、界面优化和高级功能扩展。通过代码解析和实操建议,帮助开发者掌握指针控件和实时时钟(RTC)的应用技巧,提升嵌入式界面开发能力。

weixin_29296241的博客 109

ESP32圆形屏示波器时钟:李萨如图形与余晖效果的嵌入式实现

在嵌入式图形显示领域,模拟经典CRT示波器的视觉效果是一项融合了数学美学与硬件编程的挑战。其核心原理在于将时间变量映射为X-Y坐标系下的动态波形,通过控制像素点的亮度衰减来模拟电子束击打荧光屏后产生的余晖轨迹。这种技术不仅复刻了复古仪器的独特美感,更在低功耗微控制器上实现了实时图形渲染的工程实践。常见的实现方案包括利用李萨如图形算法进行坐标映射,并借助帧缓冲区管理技术来高效模拟轨迹的渐隐效果。该技术可广泛应用于创意时钟、艺术装置及嵌入式UI等场景,为基于ESP32等微控制器的圆形显示屏项目提供了兼具科技感与

weixin_33910137的博客 469

ESP32制作一个拟辉光管时钟

拟辉光管时钟60 多年过去了,尽管辉光管已经停产,有很多工程师仍然对它情有独钟。我用 8 块液晶显示屏制作了一个拟辉光管时钟,感受复古元素的美感,表达对过去经典的敬意。项目起源看过《命运石之门》的朋友应该知道里面有一个“时间线变动仪”,就是用辉光管呈现数字,网上很多电子爱好者也成功用辉光管进行了复刻,效果拔群。对于刚接触电子行业的我来说有点复杂,而且辉光管早已停产,后来我在各个平台寻觅,终于看到了...

karaxiaoyu的专栏 2622

ESP32-S3复刻开源小电视:硬件优化与CRT效果实现

嵌入式开发中,SPI通信和硬件加速解码是提升显示性能的关键技术。通过ESP32-S3的USB OTG功能实现超低延迟数据传输,结合LVGL图形库的GPU加速渲染,可在低成本硬件上实现流畅的60FPS视频播放。在复古设备复刻场景中,采用像素位移和亮度调制算法模拟CRT扫描线效果,配合3D打印外壳设计,既保留了经典显像管的视觉美感,又融入了现代嵌入式系统的工程优势。该项目验证了开源硬件在音视频处理领域的创新潜力,为DIY数字标牌和怀旧设备改造提供了可复用的技术方案。

weixin_29194255的博客 269

Clock.saver深度解析:4大Braun经典手表型号的数字复刻实现

Clock.saver是一款用Swift编写的简约时钟屏保程序,它精准复刻了Braun四大经典手表型号的设计美学与功能特点。通过数字技术重现了BN0021、BN0032、BN0095和BN0111等标志性腕表的视觉风格,为现代数字设备带来经典工业设计的永恒魅力。 ## 经典型号复刻实现 ### BN0021:极简主义的黑白美学 BN0021型号以其纯粹的极简设计著称,Clock.saver通

gitblog_00980的博客 864

PPT复刻操作系统界面:交互逻辑实现与性能优化指南

交互设计是构建用户界面的核心概念,通过合理的触发逻辑和状态管理实现用户与系统的有效沟通。其原理基于事件驱动模型,利用动画和页面跳转模拟真实操作流程。在技术价值层面,良好的交互设计能显著提升用户体验和操作效率。应用场景广泛涵盖原型演示、教学案例和UI设计展示等领域。本文聚焦PPT高级功能,通过热词"动画触发"和"VBA模拟"等关键技术,详细解析如何突破工具限制,实现桌面环境、开始菜单等系统界面的动态复刻,并针对性能卡顿提供实测有效的优化方案。

weixin_34277853的博客 409

转录因子详细介绍(motif)

转录因子详细介绍(motif) TF: transcription factor转录因子TFBS: transcription factor binding site转录因子结合位点TFBS是序列内的location,TF特异结合在这里,这个site有这种特点 1 和一些参考相关的一个位置(开始,结束,strand),这些reference可以是染色体开始,geneTSS。也可以是一段sequence 2 A SITE可以是实验证实的(已知的),也可以是一些算法(预测的) 3 例子,下面这个图是酵母

wangprince2017 3万+

FlipIt翻页时钟:为Windows桌面注入复古时间艺术

在数字化时代,FlipIt翻页时钟屏幕保护程序为Windows用户带来了一场视觉盛宴。这款基于.NET Framework 4.8技术构建的屏保程序,完美复刻了经典机械翻页时钟的优雅韵味,让你的电脑闲置时刻化身为流动的时间艺术品。无需Flash支持,FlipIt以其纯净的技术架构和精致的视觉效果,重新定义了屏幕保护程序的审美标准。 ## 🎯 轻松安装与即刻体验 **Windows翻页时钟安装

gitblog_00259的博客 994

【Jlink & C#】通过C#实现Jlink RTT上位机的功能

某天看到安富莱家的自己开发的调试器可以实现输出中文调试信息到自己编写的Jlink RTT上位机上,觉得无比神奇,略好奇到底是如何做的,找了很久,发现其上位机的对应的实现好像也没有完全开源出来。后来经过很多搜索确认,知道了几个事实:那么有没有可能用C#来控制Jlink呢?答案是,有!!! 解决思路在于,C#调用JLinkARM.dll里面的函数 这里转载下我认为很有意义的发现结合以上的几个发现,加上我自己的测试,我把我对接JLinkARM.dll的C#接口分享出来,不保证都能用,我只是测试了几个常用的功能,R

一只爱做笔记的码农 9145

Mac 风格时钟(美化版)

美化改进说明 界面设计优化:视觉效果提升:布局调整:技术改进:这个版本完美复刻了Mac风格时钟的所有细节,包括流畅的秒针动画、精致的数字显示和专业的视觉效果,同时保持了代码的简洁和高效。

辅导大学生代码设计15年 970

腾讯文档API逆向分析:不用Selenium也能批量导出的黑科技

本文揭秘了如何通过Python逆向分析腾讯文档的隐藏API,实现无需Selenium即可批量导出在线表格的高效方法。详细解析了API调用流程、Cookie维护及异常处理技巧,相比传统方案性能提升显著,适用于大量文档导出的自动化场景。

h9j8k7l6m5n的博客 1201

Cadence 17.2配置CIS数据库报错?手把手解决ODBC驱动位数不匹配问题

本文详细解析了Cadence 17.2配置CIS数据库时常见的ODBC驱动位数不匹配问题,提供了从系统环境检查到分步解决方案的完整指南。重点解决32位应用程序与64位ODBC驱动之间的架构冲突,包含驱动安装、DSN配置及高级排查技巧,帮助电子工程师快速恢复设计环境。

play7的博客 970

嵌入式AI语音复刻实战:从录音采集到端侧部署

语音复刻是将目标说话人音色特征建模并合成新语音的关键技术,其核心依赖高质量原始音频输入与轻量化模型部署能力。原理上需兼顾声学特征提取(如基频、共振峰、气声比)与低资源推理适配,技术价值在于实现角色化、个性化的边缘语音交互。典型应用场景包括智能硬件语音助手、游戏IP衍生设备、教育机器人等对隐私性、实时性和定制化要求高的领域。本文聚焦嵌入式端语音复刻落地瓶颈,围绕音频采集规范(8–59秒/44.1kHz/单声道)、无损格式转换、云端API集成及ESP32-S3等MCU平台的I2S驱动优化、唤醒词工程选型等关键环

fern8的博客 924

【科普知识】什么是驱控一体化伺服电机?

驱控一体化伺服电机以其高度的集成性、兼容性、便捷性以及卓越的性能表现,成为了现代工业自动化领域的一颗璀璨明珠。对于那些追求高效、精准、智能控制的行业用户而言,它无疑是一种理想的选择。

m0_46512607的博客 3151

【ESP8266物联网天气时钟】解决太极创客物联网天气时钟二三页出现NA的问题——新版和风天气API以及gzip数据解压

复刻太极创客物联网天气时钟项目,解决由于和风天气升级导致的二三页出现NA的问题

zhanwudi的博客 3056

Ren’py教程《从入门到精通》

Ren’py是什么?Ren'py是一款用于制作视觉小说、冒险游戏、galgame的游戏制作工具,被来自世界各地的数千名创作者使用。Ren'py的名字由Ren’ai和Python混合而成,而Python也是Ren'py所使用的环境语言。Ren'py可以帮助你使用文字、图像和声音来讲述在计算机和移动设备上运行的交互式故事。这些既可以是视觉小说,也可以是生活模拟游戏。易于学习的脚本语言允许 任何人都可以有效地编写大型视觉小说,而它的Python 脚本足以应对复杂的 模拟游戏。

2302_77140651的博客 1万+

labelImg使用教程

Anaconda环境 安装 Anaconda 之后,在开始菜单下找到 Anaconda 文件夹 找到 Anaconda Prompt 点击运行 安装 labelImg pip install labelImg #直接命令行输入 启动 labelImg labelImg #直接命令行输入名称即可 labelImg界面 开始标注 快捷键:W(创建方框),A(上一张),D(下一张) XML文件示例 <annotation> <folder>夜读</folder>

G果的博客 3万+

多功能像素时钟(开源)

本项目由本人闲暇时间所做,拥有USB拓展,蓝牙音响,天气时钟等多种功能

mengquaner的博客 1416

第18章:AI辅助Debug——如何将错误日志交给AI定位根因

> - 如何将错误日志、堆栈、代码片段组合发给AI,快速定位问题根因 > - AI分析错误的3个核心维度:环境差异、逻辑缺陷、并发竞态 > - 常见错误的AI分析Prompt模板(前端报错、后端异常、数据库错误、网络请求失败) > - 一个完整的调试案例:从用户报错到找出问题根源的全过程 > - 我如何用AI把平均调试时间从2小时缩短到20分钟

sfishfly的博客 44

FlipIt翻页时钟终极指南:让Windows屏保变身复古时光艺术

厌倦了单调乏味的屏幕保护程序?FlipIt翻页时钟为Windows用户带来了全新的视觉盛宴!这款完全免费的开源屏幕保护程序完美复刻了经典机械翻页时钟的优雅质感,将电脑闲置时刻转化为令人惊艳的时间艺术品。 ## 🎯 五分钟快速上手配置 **一键获取源码**:通过命令`git clone https://gitcode.com/gh_mirrors/fl/FlipIt`即可获得完整项目文件,无需

gitblog_01145的博客 621

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

TUI 终端用户界面快速入门指南:5 分钟给纯命令行加一层“界面感“

昨晚跑编译任务突然卡住,我分不清是哪个进程在抢 CPU。平时开着 `top` 也只能看到一屏数字。后来我跟着 TUI(终端用户界面)工具精选仓库 awesome-tuis 装了 btop,打开就能看到内存、CPU、磁盘的实时曲线,一眼定位了元凶。 awesome-tuis 是一个由社区维护的 TUI 应用清单:不用离开终端,就能监控系统状态、浏览文件、操作 Git。换句话说,纯命令行也能有"界面

gitblog_00961的博客 240

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

quartus2多功能数字时钟

华南师范大学的数电综设首先看看最终效果各按键功能表1.切换12/24进制2.小时较时选通(按key8可以自增)3.分较时选通(按key8可以自增)4.秒清零5.切换秒表()6.秒表开始/暂停7.秒表清零8.自增键最终效果。

2401_87850819的博客 2994

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

上一篇: DataStore-SharedPreference知识点
下一篇: 手把手教你上传安卓库到Central Portal
fastcv
博客等级 码龄10年 90粉丝 5原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值