如何仿一个抖音极速版领现金的进度条动画?

效果演示

20230617_064552_edit.gif

不仅仅是实现效果,要封装,就封装好

看完了演示的效果,你是否在思考,代码应该怎么实现?先不着急写代码,先想想哪些地方是要可以动态配置的。首先第一个,进度条的形状是不是要可以换?然后进度条的背景色和填充的颜色,以及动画的时长是不是也要可以配置?没错,起始位置是不是也要可以换?最好还要让速度可以一会快一会慢对吧,画笔的笔帽是不是还可以选择平的或圆的?带着这些问题,我们再开始写代码。

代码实现

我们写一个自定义View,把可以动态配置的地方想好后,就可以定义自定义属性了。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="DoraProgressView">
        <attr name="dview_progressType">
            <enum name="line" value="0"/>
            <enum name="semicircle" value="1"/>
            <enum name="semicircleReverse" value="2"/>
            <enum name="circle" value="3"/>
            <enum name="circleReverse" value="4"/>
        </attr>
        <attr name="dview_progressOrigin">
            <enum name="left" value="0"/>
            <enum name="top" value="1"/>
            <enum name="right" value="2"/>
            <enum name="bottom" value="3"/>
        </attr>
        <attr format="dimension|reference" name="dview_progressWidth"/>
        <attr format="color|reference" name="dview_progressBgColor"/>
        <attr format="color|reference" name="dview_progressHoverColor"/>
        <attr format="integer" name="dview_animationTime"/>
        <attr name="dview_paintCap">
            <enum name="flat" value="0"/>
            <enum name="round" value="1"/>
        </attr>
    </declare-styleable>
</resources>

然后我们不管三七二十一,先把自定义属性解析出来。

private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
    val a = context.obtainStyledAttributes(
        attrs,
        R.styleable.DoraProgressView,
        defStyleAttr,
        0
    )
    when (a.getInt(R.styleable.DoraProgressView_dview_progressType, PROGRESS_TYPE_LINE)) {
        0 -> progressType = PROGRESS_TYPE_LINE
        1 -> progressType = PROGRESS_TYPE_SEMICIRCLE
        2 -> progressType = PROGRESS_TYPE_SEMICIRCLE_REVERSE
        3 -> progressType = PROGRESS_TYPE_CIRCLE
        4 -> progressType = PROGRESS_TYPE_CIRCLE_REVERSE
    }
    when (a.getInt(R.styleable.DoraProgressView_dview_progressOrigin, PROGRESS_ORIGIN_LEFT)) {
        0 -> progressOrigin = PROGRESS_ORIGIN_LEFT
        1 -> progressOrigin = PROGRESS_ORIGIN_TOP
        2 -> progressOrigin = PROGRESS_ORIGIN_RIGHT
        3 -> progressOrigin = PROGRESS_ORIGIN_BOTTOM
    }
    when(a.getInt(R.styleable.DoraProgressView_dview_paintCap, 0)) {
        0 -> paintCap = Paint.Cap.SQUARE
        1 -> paintCap = Paint.Cap.ROUND
    }
    progressWidth = a.getDimension(R.styleable.DoraProgressView_dview_progressWidth, 30f)
    progressBgColor =
        a.getColor(R.styleable.DoraProgressView_dview_progressBgColor, Color.GRAY)
    progressHoverColor =
        a.getColor(R.styleable.DoraProgressView_dview_progressHoverColor, Color.BLUE)
    animationTime = a.getInt(R.styleable.DoraProgressView_dview_animationTime, 1000)
    a.recycle()
}

解析完自定义属性,切勿忘了释放TypedArray。接下来我们考虑下一步,测量。半圆是不是不要那么大的画板对吧,我们在测量的时候就要充分考虑进去。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    progressBgPaint.strokeWidth = progressWidth
    progressHoverPaint.strokeWidth = progressWidth
    if (progressType == PROGRESS_TYPE_LINE) {
        // 线
        var left = 0f
        var top = 0f
        var right = measuredWidth.toFloat()
        var bottom = measuredHeight.toFloat()
        val isHorizontal = when(progressOrigin) {
            PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
            else -> false
        }
        if (isHorizontal) {
            top = (measuredHeight - progressWidth) / 2
            bottom = (measuredHeight + progressWidth) / 2
            progressBgRect[left + progressWidth / 2, top, right - progressWidth / 2] = bottom
        } else {
            left = (measuredWidth - progressWidth) / 2
            right = (measuredWidth + progressWidth) / 2
            progressBgRect[left, top + progressWidth / 2, right] = bottom - progressWidth / 2
        }
    } else if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
        // 圆
        var left = 0f
        val top = 0f
        var right = measuredWidth
        var bottom = measuredHeight
        progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
            bottom - progressWidth / 2
    } else {
        // 半圆
        val isHorizontal = when(progressOrigin) {
            PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
            else -> false
        }
        val min = measuredWidth.coerceAtMost(measuredHeight)
        var left = 0f
        var top = 0f
        var right = 0f
        var bottom = 0f
        if (isHorizontal) {
            if (measuredWidth >= min) {
                left = ((measuredWidth - min) / 2).toFloat()
                right = left + min
            }
            if (measuredHeight >= min) {
                bottom = top + min
            }
            progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
                bottom - progressWidth / 2
            setMeasuredDimension(
                MeasureSpec.makeMeasureSpec(
                    (right - left).toInt(),
                    MeasureSpec.EXACTLY
                ),
                MeasureSpec.makeMeasureSpec(
                    (bottom - top + progressWidth).toInt() / 2,
                    MeasureSpec.EXACTLY
                )
            )
        } else {
            if (measuredWidth >= min) {
                right = left + min
            }
            if (measuredHeight >= min) {
                top = ((measuredHeight - min) / 2).toFloat()
                bottom = top + min
            }
            progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
                bottom - progressWidth / 2
            setMeasuredDimension(
                MeasureSpec.makeMeasureSpec(
                    (right - left + progressWidth).toInt() / 2,
                    MeasureSpec.EXACTLY
                ),
                MeasureSpec.makeMeasureSpec(
                    (bottom - top).toInt(),
                    MeasureSpec.EXACTLY
                )
            )
        }
    }
}

View的onMeasure()方法是不是默认调用了一个

super.onMeasure(widthMeasureSpec, heightMeasureSpec)

它最终会调用setMeasuredDimension()方法来确定最终测量的结果吧。如果我们对默认的测量不满意,我们可以自己改,最后也调用setMeasuredDimension()方法把测量结果确认。半圆,如果是水平的情况下,我们的宽度就只要一半,相反如果是垂直的半圆,我们高度就只要一半。最后我们画还是照常画,只不过在最后把画到外面的部分移动到画板上显示出来。接下来就是我们最重要的绘图环节了。

override fun onDraw(canvas: Canvas) {
    if (progressType == PROGRESS_TYPE_LINE) {
        val isHorizontal = when(progressOrigin) {
            PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
            else -> false
        }
        if (isHorizontal) {
            canvas.drawLine(
                progressBgRect.left,
                measuredHeight / 2f,
                progressBgRect.right,
                measuredHeight / 2f,
                progressBgPaint)
        } else {
            canvas.drawLine(measuredWidth / 2f,
                progressBgRect.top,
                                    measuredWidth / 2f,
                progressBgRect.bottom, progressBgPaint)
        }
        if (percentRate > 0) {
            when (progressOrigin) {
                PROGRESS_ORIGIN_LEFT -> {
                    canvas.drawLine(
                        progressBgRect.left,
                        measuredHeight / 2f,
                        (progressBgRect.right) * percentRate,
                        measuredHeight / 2f,
                        progressHoverPaint
                    )
                }
                PROGRESS_ORIGIN_TOP -> {
                    canvas.drawLine(measuredWidth / 2f,
                        progressBgRect.top,
                        measuredWidth / 2f,
                        (progressBgRect.bottom) * percentRate,
                        progressHoverPaint)
                }
                PROGRESS_ORIGIN_RIGHT -> {
                    canvas.drawLine(
                        progressWidth / 2 + (progressBgRect.right) * (1 - percentRate),
                        measuredHeight / 2f,
                        progressBgRect.right,
                        measuredHeight / 2f,
                        progressHoverPaint
                    )
                }
                PROGRESS_ORIGIN_BOTTOM -> {
                    canvas.drawLine(measuredWidth / 2f,
                        progressWidth / 2 + (progressBgRect.bottom) * (1 - percentRate),
                    measuredWidth / 2f,
                        progressBgRect.bottom,
                    progressHoverPaint)
                }
            }
        }
    } else if (progressType == PROGRESS_TYPE_SEMICIRCLE) {
        if (progressOrigin == PROGRESS_ORIGIN_LEFT) {
            // PI ~ 2PI
            canvas.drawArc(progressBgRect, 180f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                180f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            canvas.translate(-progressBgRect.width() / 2, 0f)
            // 3/2PI ~ 2PI, 0 ~ PI/2
            canvas.drawArc(progressBgRect, 270f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                270f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            canvas.translate(0f, -progressBgRect.height() / 2)
            // 2PI ~ PI
            canvas.drawArc(progressBgRect, 0f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                0f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            // PI/2 ~ 3/2PI
            canvas.drawArc(progressBgRect, 90f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                90f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        }
    } else if (progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {
        if (progressOrigin == PROGRESS_ORIGIN_LEFT) {
            canvas.translate(0f, -progressBgRect.height() / 2)
            // PI ~ 2PI
            canvas.drawArc(progressBgRect, 180f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                180f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            // 3/2PI ~ PI/2
            canvas.drawArc(progressBgRect, 270f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                270f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            // 2PI ~ PI
            canvas.drawArc(progressBgRect, 0f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                0f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            canvas.translate(-progressBgRect.width() / 2, 0f)
            // PI/2 ~ 2PI, 2PI ~ 3/2PI
            canvas.drawArc(progressBgRect, 90f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                90f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        }
    } else if (progressType == PROGRESS_TYPE_CIRCLE) {
        val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            90f
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            180f
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            270f
        } else {
            0f
        }
        canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)
        canvas.drawArc(
            progressBgRect,
            180f + deltaAngle,
            angle.toFloat(),
            false,
            progressHoverPaint
        )
    } else if (progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
        val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            90f
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            180f
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            270f
        } else {
            0f
        }
        canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)
        canvas.drawArc(
            progressBgRect,
            180f + deltaAngle,
            -angle.toFloat(),
            false,
            progressHoverPaint
        )
    }
}

绘图除了需要Android的基础绘图知识外,还需要一定的数学计算的功底,比如基本的几何图形的点的计算你要清楚。怎么让绘制的角度变化起来呢?这个问题问的好。这个就牵扯出我们动画的一个关键类,TypeEvaluator,这个接口可以让我们只需要指定边界值,就可以根据动画执行的时长,来动态计算出当前的渐变值。

private inner class AnimationEvaluator : TypeEvaluator<Float> {
    override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
        return if (endValue > startValue) {
            startValue + fraction * (endValue - startValue)
        } else {
            startValue - fraction * (startValue - endValue)
        }
    }
}

百分比渐变的固定写法,是不是应该记个笔记,方便以后CP?那么现在我们条件都成熟了,只需要将初始角度的百分比改变一下,我们写一个改变角度百分比的方法。

fun setPercentRate(rate: Float) {
    if (animator == null) {
        animator = ValueAnimator.ofObject(
            AnimationEvaluator(),
            percentRate,
            rate
        )
    }
    animator?.addUpdateListener { animation: ValueAnimator ->
        val value = animation.animatedValue as Float
        angle =
            if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
                (value * 360).toInt()
            } else if (progressType == PROGRESS_TYPE_SEMICIRCLE || progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {
                (value * 180).toInt()
            } else {
                0   // 线不需要求角度
            }
        percentRate = value
        invalidate()
    }
    animator?.interpolator = LinearInterpolator()
    animator?.setDuration(animationTime.toLong())?.start()
    animator?.addListener(object : Animator.AnimatorListener {
        override fun onAnimationStart(animation: Animator) {}
        override fun onAnimationEnd(animation: Animator) {
            percentRate = rate
            listener?.onComplete()
        }

        override fun onAnimationCancel(animation: Animator) {}
        override fun onAnimationRepeat(animation: Animator) {}
    })
}

这里牵扯到了Animator。有start就一定不要忘了异常中断的情况,我们可以写一个reset的方法来中断动画执行,恢复到初始状态。

fun reset() {
    percentRate = 0f
    animator?.cancel()
}

如果你不reset,想连续执行动画,则两次调用的时间间隔一定要大于动画时长,否则就应该先取消动画。

涉及到的Android绘图知识点

我们归纳一下完成这个自定义View需要具备的知识点。

  1. 基本图形的绘制,这里主要是扇形
  2. 测量和画板的平移变换
  3. 自定义属性的定义和解析
  4. Animator和动画估值器TypeEvaluator的使用

思路和灵感来自于系统化的基础知识

这个控件其实并不难,主要就是动态配置一些参数,然后在计算上稍微复杂一些,需要一些数学的功底。那么你为什么没有思路呢?你没有思路最可能的原因主要有以下几个可能。

  1. 自定义View的基础绘图API不熟悉
  2. 动画估值器使用不熟悉
  3. 对自定义View的基本流程不熟悉
  4. 看的自定义View的源码不够多
  5. 自定义View基础知识没有系统学习,导致是一些零零碎碎的知识片段
  6. 数学功底不扎实

我觉得往往不是你不会,这些基础知识点你可能都看到过很多次,但是一到自己写就没有思路了。思路和灵感来自于大量源码的阅读和大量的实践。大前提就是你得先把自定义View的这些知识点系统学习一下,先保证都见过,然后才是将它们融会贯通,用的时候信手拈来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/668910.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【图解IO与Netty系列】Reactor模型

Reactor模型 Reactor模型简介三类事件与三类角色Reactor模型整体流程 各种Reactor模型单Reactor单线程模型单Reactor多线程模型主从Reactor模型 Reactor模型简介 Reactor模型是服务器端用于处理高并发网络IO请求的编程模型&#xff0c;与传统的一请求一线程的同步式编程模型不…

day05-多任务-正则-装饰器

一、多任务 1-进程和线程 进程是操作系统分配资源的最小单元 线程执行程序的的最小单元 线程依赖进程&#xff0c;可以获取进程的资源 一个程序执行 先要创建进程分配资源&#xff0c;然后使用线程执行任务 默认情况下一个进程中有一个线程 2-多任务介绍 运行多个进程或线程执…

Day44 动态规划part04

背包问题 01背包问题&#xff1a;每件物品只能用一次完全背包问题&#xff1a;每件物品可以使用无数次 01背包问题 暴力解法&#xff1a;每一件物品其实只有两个状态&#xff0c;取或者不取&#xff0c;所以可以使用回溯法搜索出所有的情况&#xff0c;那么时间复杂度就是 o…

【LeetCode刷题】二分查找:寻找旋转排序数组中的最小值、点名

【LeetCode刷题】Day 14 题目1&#xff1a;153.寻找旋转排序数组中的最小值思路分析&#xff1a;思路1&#xff1a;二分查找&#xff1a;以A为参照思路2&#xff1a;二分查找&#xff0c;以D为参照 题目2&#xff1a;LCR 173.点名思路分析&#xff1a;思路1&#xff1a;遍历查找…

【显示方案IC-速显微】

最近偶然间接触到“速显微”的显示方案&#xff0c;个人体验了一把感觉还是挺顺手的&#xff0c;虽然手里没有板子没有上手测试一番。 这是他们的官网链接&#xff1a; https://www.thorsianway.com/product/chip 从官网可以看到有两颗个系列的IC已经量产&#xff1a;GC9005和G…

物联网实战--平台篇之(十一)设备管理后台

目录 一、设备数据库 二、添加设备 三、排序设备 四、重命名设备 五、删除设备 六、移动设备 本项目的交流QQ群:701889554 物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html 物联网实战--驱动篇https://blog.csdn.net/ypp240124016/categ…

词法分析器的设计与实现--编译原理操作步骤,1、你的算法工作流程图; 2、你的函数流程图;3,具体代码

实验原理&#xff1a; 词法分析是编译程序进行编译时第一个要进行的任务&#xff0c;主要是对源程序进行编译预处理之后&#xff0c;对整个源程序进行分解&#xff0c;分解成一个个单词&#xff0c;这些单词有且只有五类&#xff0c;分别时标识符、关键字&#xff08;保留字&a…

【匹配线段问题】

问题&#xff1a; 如下图所示。图中有两行正整数&#xff0c;每行中有若干个正整数。如果第一行的某个数r与第二行的某个数相同&#xff0c;这样就可以在这两个正整数之间划一条线&#xff0c;并称之为r-匹配线段。下图中存在3-匹配线段和2-匹配线段。 请编写完整程序&#xf…

[12] 使用 CUDA 加速排序算法

使用 CUDA 加速排序算法 排序算法被广泛用于计算应用中有很多排序算法&#xff0c;像是枚举排序或者说是秩排序、冒泡排序和归并排序&#xff0c;这些排序算法具有不同的&#xff08;时间和空间&#xff09;复杂度&#xff0c;因此对同一个数组来说也有不同的排序时间&#xf…

9款实用而不为人知的小众软件推荐!

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/ 在电脑软件的浩瀚海洋中&#xff0c;除了那些广为人知的流行软件外&#xff0c;还有许多简单、干净、功能强大且注重实用功能的小众软件等待我们…

Ubuntu中PDF阅读器和编辑器

1. 福昕PDF编辑器 1.1. 下载地址 PDF阅读器下载_PDF编辑器下载_PDF软件官方下载_福昕软件官网 1.2. 安装 sudo dpkg -i signed_com.foxit.foxitpdfeditor_xxx_amd64_UOS.deb 2. WPS DPF 2.1. 下载地址 WPS Office 2019 for Linux-支持多版本下载_WPS官方网站 2.2. 使用 …

【LeetCode算法】第111题:二叉树的最小深度

目录 一、题目描述 二、初次解答 三、官方解法 四、总结 一、题目描述 二、初次解答 1. 思路&#xff1a;二叉树的先序遍历。求出左子树的最小高度&#xff0c;求出右子树的最小高度&#xff0c;最终返回左子树和右子树的最小高度1。关键&#xff1a;若左子树的高度为0&…

【Linux】写一个日志类

文章目录 1. 源代码2. 函数功能概览3. 代码详细解释3.1 头文件和宏定义3.2 Log类定义3.3 打印日志的方法3.4 操作符重载和析构函数3.5 可变参数函数的原理 4. 测试用例 1. 源代码 下面代码定义了一个 Log 类&#xff0c;用于记录日志信息。这个类支持将日志信息输出到屏幕、单…

[无监督学习] 11.详细图解LSA

LSA LSA&#xff08;Latent Semantic Analysis&#xff0c;潜在语义分析&#xff09;是一种自然语言处理技术。作为一种降维算法&#xff0c;它常被用于信息搜索领域。使用 LSA 能够从大量的文本数据中找出单词之间的潜在关联性。 概述 LSA 是在 1988 年被提出的算法&#xff…

Java(七)——Clonable接口与深拷贝

文章目录 Clonable接口与深拷贝克隆对象深拷贝 Clonable接口与深拷贝 克隆对象 考虑&#xff1a;怎样将对象克隆一份&#xff1f; 答案就在本文&#xff0c;我们先给出一步一步的思考过程&#xff0c;然后总结&#xff1a; 首先设置情景&#xff1a;我们有一个Person类&#x…

Wireshark Lua插件入门

摘要 开发中经常通过抓包分析协议&#xff0c;对于常见的协议如 DNS wireshark 支持自动解析&#xff0c;便于人类的理解&#xff0c;对于一些私有协议&#xff0c;wireshark 提供了插件的方式自定义解析逻辑。 1 动手 废话少说&#xff0c;直接上手。 第一步当然是装上wiresh…

[C++]vector的模拟实现

下面是简单的实现vector的功能&#xff0c;没有涉及使用内存池等复杂算法来提高效率。 一、vector的概述 &#xff08;一&#xff09;、抽象数据类型定义 容器&#xff1a;向量&#xff08;vector&#xff09;vector是表示大小可以变化的数组的序列容器。像数组一样&#xf…

GPT-4o vs. GPT-4 vs. Gemini 1.5 性能评测,谁更胜一筹!

OpenAI 最近推出了 GPT-4o&#xff0c;OpenAI有一次火爆了&#xff0c;其图像、音频、视频的处理能力非常强。 最令人印象深刻的是&#xff0c;它支持用户与 ChatGPT 实时互动&#xff0c;并且能够处理对话中断。 而且&#xff0c;OpenAI 免费开放了 GPT-4o API 的访问权限。…

[ROS 系列学习教程] 建模与仿真 - 使用 Xacro 优化 urdf

ROS 系列学习教程(总目录) 本文目录 一、使用属性表示常量二、使用公式三、使用宏定义四、include 其他文件五、优化实践 对于前文介绍的 urdf 模型&#xff0c;我们可以使用 xacro 来优化&#xff0c;使其更易于维护。 优化点&#xff1a; 多次用到的尺寸用常量定义计算使用…

嵌入式linux系统中图片处理详解

大家好,今天给大家分享一下,嵌入式中如何进行图像处理,常见的处理方式有哪几种?这次将详细分析一下 第一:BMP图形处理方式 图形的基本特点,所有的图像文件,都是一种二进制格式文件,每一个图像文件,都可以通过解析文件中的每一组二进制数的含义来获得文件中的各种信息…