Android纯Canvas手绘风思维导图控件实现

简介:这是一份面向Android中高级开发者的思维导图自定义控件实战项目,聚焦树形结构可视化难题,解决节点动态绘制、交互响应与状态清除等核心痛点。项目通过递归算法遍历多层节点、堆栈结构保障绘制/清除顺序一致性,并基于自定义View实现高效绘图与触摸事件处理,适用于知识管理类App、学习笔记工具等场景。压缩包共43个文件(279KB),含5个核心Java类(实现递归绘制逻辑与节点管理)、11个XML布局与资源文件、12个PNG图标素材及2个JPG示例图,辅以Gradle构建配置与README说明文档,目录结构清晰,模块职责分明。目前已有259人学习下载,读者可直接复用其递归+堆栈的节点渲染范式、触摸交互状态机设计、轻量级树形数据模型及性能优化思路,快速构建可扩展的移动端思维导图功能。

1. 一个在 Android 上“手绘感”十足的思维导图控件:不用 WebView、不依赖第三方 SDK,纯 Canvas + 递归 + 堆栈搞定动态折叠/展开与节点重绘

你有没有试过在 Android 原生项目里嵌一个真正可交互、能拖拽、支持无限层级折叠、且缩放后文字依然清晰的思维导图?不是用 WebView 加载 JS 库(性能卡顿、手势冲突、调试黑匣子),也不是调用商业 SDK(授权贵、定制难、源码不可控)。这个项目就是答案:它用纯 Java/Kotlin 实现了一个自定义View,核心逻辑仅靠递归遍历树结构 + 双堆栈管理绘制状态 + Canvas 层级偏移控制,就把节点定位、连线路径、文字居中、折叠动画、清除逻辑全拿下了。它不追求炫酷动效,但每一步绘制都可 debug、每一处坐标都可推演——适合需要深度定制导图行为(比如和本地知识库联动、做考试错题分支标记、嵌入教育类 App 的笔记模块)的中高级 Android 开发者。如果你正被「导图太重」「缩放糊字」「折叠后连线错位」这类问题反复折磨,这个项目就是你该立刻 clone 下来跑通的最小可行原型。


2. 架构设计与核心模块拆解:从 MindMapNode 到 Canvas 绘制链路的完整闭环

2.1 数据模型:MindMapNode 是树形结构的唯一载体,无 ORM、无 JSON 封装开销

整个项目的数据层极度轻量,只定义了一个MindMapNode类,它既是数据容器,也是绘制单元。没有引入 Gson 或 Jackson,所有节点通过parent和children字段构成双向树,避免了序列化/反序列化的性能损耗和兼容性风险。每个节点携带最简必要字段:text(显示文本)、level(层级深度,用于计算缩进和颜色梯度)、isExpanded(折叠状态)、x/y(当前屏幕坐标,非绝对位置,而是相对父节点的偏移量)。特别注意:x/y不是最终绘制坐标,而是递归过程中由父节点传递下来的布局参考值,真正落画布前会经calculatePosition()动态修正。

public class MindMapNode { public String text; public MindMapNode parent; public List<MindMapNode> children = new ArrayList<>(); public int level = 0; public boolean isExpanded = true; public float x, y; // relative to parent, NOT absolute canvas coords public MindMapNode(String text) { this.text = text; } // 递归计算当前节点及其子树所有节点的最终屏幕坐标 public void calculatePosition(float parentX, float parentY, float horizontalGap, float verticalGap) { this.x = parentX; this.y = parentY + verticalGap; float childXOffset = horizontalGap * (float) Math.pow(1.2, level); // 指数级拉开子节点横向间距 int childIndex = 0; for (MindMapNode child : children) { if (child.isExpanded) { float childX = this.x + childXOffset * (childIndex - (children.size() - 1) / 2f); child.calculatePosition(childX, this.y, horizontalGap, verticalGap); childIndex++; } } } }

提示:horizontalGap和verticalGap是外部传入的布局参数,不是写死常量。这意味着你可以根据屏幕密度(density)或用户设置的“紧凑模式”动态调整,而无需修改节点逻辑。

2.2 绘制引擎:双堆栈驱动 Canvas 状态管理,解决“折叠后子节点残留”这一经典翻车点

很多自定义导图控件崩溃在折叠操作后——子节点明明isExpanded = false,但 Canvas 上还留着旧的连线和文字。本项目用两个独立堆栈彻底规避:drawStack存储待绘制节点(按 DFS 顺序压入),positionStack存储对应节点的x/y坐标(与drawStack同步 push/pop)。关键在于:onDraw()中只遍历drawStack,而drawStack的构建发生在updateDrawStack()方法里,该方法严格遵循isExpanded状态进行 DFS 遍历,未展开的子树节点根本不会进入drawStack。这就从源头上杜绝了“残留绘制”。

// 在自定义 View 中 private Stack<MindMapNode> drawStack = new Stack<>(); private Stack<PointF> positionStack = new Stack<>(); private void updateDrawStack(MindMapNode root) { drawStack.clear(); positionStack.clear(); dfsBuildDrawStack(root, new PointF(0, 0)); } private void dfsBuildDrawStack(MindMapNode node, PointF pos) { drawStack.push(node); positionStack.push(new PointF(pos.x, pos.y)); if (node.isExpanded && !node.children.isEmpty()) { float centerY = pos.y; float startX = pos.x + 120; // 从父节点右侧开始布局子节点 float gapX = 80f; int childCount = node.children.size(); float offsetX = startX - (childCount - 1) * gapX / 2f; for (int i = 0; i < childCount; i++) { MindMapNode child = node.children.get(i); if (child.isExpanded) { PointF childPos = new PointF(offsetX + i * gapX, centerY); dfsBuildDrawStack(child, childPos); } } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (rootNode == null) return; // 清空画布(仅清空内容区,保留背景) canvas.save(); canvas.clipRect(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); canvas.restore(); // 逐个绘制 drawStack 中的节点 while (!drawStack.isEmpty()) { MindMapNode node = drawStack.pop(); PointF pos = positionStack.pop(); drawNode(canvas, node, pos.x, pos.y); if (!node.children.isEmpty() && node.isExpanded) { drawConnections(canvas, node, pos.x, pos.y); } } }

注意:drawConnections()绘制的是从父节点到每个已展开子节点的贝塞尔曲线,其控制点坐标由pos.x/y和子节点坐标动态计算,确保折叠/展开时连线自动伸缩,无需手动重置路径。

2.3 交互逻辑:触摸事件的三层拦截——区分点击、长按、拖拽,且支持多点缩放

交互不是简单setOnClickListener,而是重写onTouchEvent(),用MotionEvent.getActionMasked()精确识别动作类型:

  • 单击:ACTION_UP时计算触摸点是否落在某个节点文字矩形内(用Paint.getTextBounds()获取文字宽高,再结合node.x/y计算屏幕区域),命中则触发onNodeClick(node)回调;
  • 长按:ACTION_DOWN后启动Handler.postDelayed(),500ms 未移动则判定为长按,弹出折叠/展开菜单;
  • 拖拽:ACTION_MOVE时记录deltaX/deltaY,平移整个rootNode的x/y,并调用invalidate()触发重绘;
  • 双指缩放:用ScaleGestureDetector监听,缩放因子作用于mScaleFactor,并在onDraw()中对canvas.scale(mScaleFactor, mScaleFactor, pivotX, pivotY)。

所有交互均不改变节点树结构,只影响isExpanded状态或视图变换矩阵,保证数据与 UI 严格分离。


3. 运行与调试:从零配置到真机验证的完整流程

3.1 环境准备:最低 API 21,无需额外依赖,Gradle 配置极简

项目基于 AndroidX 构建,无任何第三方 UI 依赖(如 Material Components 也未强制使用),因此兼容性极强。build.gradle(Module: app)只需确认以下基础配置:

android { compileSdk 34 defaultConfig { applicationId "com.example.mindmapdemo" minSdk 21 // 关键:API 21 即可运行,因未使用 ViewBinding 或 Compose targetSdk 34 versionCode 1 versionName "1.0" } } dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' // ⚠️ 注意:无其他 implementation!连 constraintlayout 都非必需 }

提示:minSdk 21是硬性要求,因为Canvas.clipRect()的Region.Op参数在 API 21 才稳定支持,而本项目用它精确裁剪重绘区域,避免闪烁。

3.2 快速集成:三步将 MindMapView 嵌入任意 Activity

  1. 复制核心文件:将MindMapView.java(或.kt)和MindMapNode.java复制到你的src/main/java/com/yourpackage/下;
  2. XML 布局声明:在activity_main.xml中添加自定义 View,并预留足够空间(导图需自由缩放,建议用ConstraintLayout包裹):
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.mindmapdemo.MindMapView android:id="@+id/mindMapView" android:layout_width="0dp" android:layout_height="0dp" android:padding="24dp" <!-- 关键:padding 为根节点提供安全边距 --> app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
  1. Activity 中初始化数据:在MainActivity.java的onCreate()里构建树并设置:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MindMapView mindMapView = findViewById(R.id.mindMapView); // 构建测试树:中心主题 → 3 个一级分支 → 每个分支下 2 个二级节点 MindMapNode root = new MindMapNode("核心概念"); root.level = 0; MindMapNode branch1 = new MindMapNode("定义与特征"); branch1.level = 1; root.children.add(branch1); MindMapNode sub1 = new MindMapNode("抽象性"); sub1.level = 2; branch1.children.add(sub1); MindMapNode sub2 = new MindMapNode("结构性"); sub2.level = 2; branch1.children.add(sub2); // ... 添加 branch2, branch3 及其子节点(代码略) // 设置根节点并触发首次绘制 mindMapView.setRootNode(root); }

注意:setRootNode()内部会调用root.calculatePosition()计算初始坐标,再调用updateDrawStack()构建绘制栈,最后invalidate()。整个过程无异步,确保 UI 立即响应。

3.3 调试技巧:用 Log + 坐标快照定位“节点飘移”和“连线断裂”

当发现节点位置偏移或连线指向错误时,不要盲目改gap参数。先开启调试日志,在MindMapView.java的drawNode()和drawConnections()开头加入:

Log.d("MindMap", String.format("DRAW NODE: %s, x=%.1f, y=%.1f, level=%d", node.text, pos.x, pos.y, node.level)); Log.d("MindMap", String.format("CONNECTION FROM (%.1f, %.1f) TO (%.1f, %.1f)", pos.x, pos.y, child.x, child.y));

然后在真机上操作(折叠/展开/缩放),观察 Logcat 输出。你会发现:

  • 若pos.x在折叠后仍输出子节点坐标,说明updateDrawStack()未生效 → 检查isExpanded是否被误设;
  • 若CONNECTION日志中child.x为0.0,说明子节点calculatePosition()未执行 → 检查isExpanded为true时是否遗漏了dfsBuildDrawStack()调用;
  • 若pos.x数值极大(如12000.0),说明horizontalGap未随缩放缩放 → 检查onDraw()中canvas.scale()后是否忘了对pos.x/y做反向缩放(本项目未做,因pos是布局参考值,绘制时已用canvas.translate()处理)。

这种“日志即坐标”的调试法,比断点单步更高效,是我处理复杂 Canvas 布局的血泪经验。


4. 常见问题排查:5 条真实踩坑记录,覆盖从编译到交互的全链路

4.1 现象:App 启动后导图空白,Logcat 无报错

原因:MindMapView的onMeasure()未重写,导致wrap_content时getMeasuredWidth()/Height()返回0,onDraw()中canvas无有效绘制区域。
解决:在MindMapView.java中添加:

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else { width = getSuggestedMinimumWidth(); // 至少显示 padding 宽度 width = Math.max(width, widthSize); } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = getSuggestedMinimumHeight(); height = Math.max(height, heightSize); } setMeasuredDimension(width, height); }

4.2 现象:双指缩放时,导图中心点剧烈跳动,无法稳定聚焦

原因:ScaleGestureDetector的pivotX/pivotY使用了event.getX()/getY(),但该坐标是相对于View左上角,而canvas.scale()的锚点需是相对于Canvas坐标系(即getScrollX()/getScrollY()未参与计算)。
解决:在onScale()回调中,将触摸点转换为Canvas坐标:

@Override public boolean onScale(ScaleGestureDetector detector) { float focusX = detector.getFocusX() - getScrollX(); float focusY = detector.getFocusY() - getScrollY(); mScaleFactor *= detector.getScaleFactor(); mScaleFactor = Math.max(0.5f, Math.min(mScaleFactor, 3.0f)); // 限制缩放范围 invalidate(); return true; }

并在onDraw()开头添加:

canvas.save(); canvas.translate(getScrollX(), getScrollY()); // 先平移,再缩放 canvas.scale(mScaleFactor, mScaleFactor, focusX, focusY); // ... 后续绘制 canvas.restore();

4.3 现象:折叠某个节点后,其子节点的文字仍可见,但连线消失

原因:drawNode()中未检查node.isExpanded,导致即使节点被折叠,只要它在drawStack中就会被绘制;而drawConnections()有检查,所以连线消失造成视觉割裂。
解决:在drawNode()开头添加显式判断:

private void drawNode(Canvas canvas, MindMapNode node, float x, float y) { if (!node.isExpanded && node != rootNode) { // 折叠的非根节点不绘制文字,但根节点永远显示 return; } // ... 后续绘制逻辑 }

4.4 现象:快速连续点击多个节点,onNodeClick()回调丢失或重复触发

原因:onTouchEvent()中未做点击防抖,ACTION_UP事件可能被上一次ACTION_DOWN的延迟任务干扰。
解决:添加简易防抖机制,在MindMapView中:

private long lastClickTime = 0; private static final long CLICK_INTERVAL = 300; // 300ms 内只认第一次点击 @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { if (System.currentTimeMillis() - lastClickTime < CLICK_INTERVAL) { return true; // 忽略抖动 } lastClickTime = System.currentTimeMillis(); // ... 执行点击检测逻辑 } return super.onTouchEvent(event); }

4.5 现象:在RecyclerView的item中嵌入MindMapView,滑动时内存飙升甚至 OOM

原因:MindMapView每次onDraw()都新建Paint对象(如mTextPaint = new Paint()),而RecyclerView复用View时未回收这些对象。
解决:将Paint声明为成员变量,在init()中一次性创建:

private Paint mTextPaint; private Paint mLinePaint; private void init() { mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(14 * getResources().getDisplayMetrics().scaledDensity); mTextPaint.setColor(Color.BLACK); mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mLinePaint.setStrokeWidth(2f); mLinePaint.setColor(Color.parseColor("#666666")); }

5. 二次开发实战:如何接入本地 SQLite 知识库,并实现“点击节点 → 跳转详情页”的闭环

5.1 数据层扩展:为 MindMapNode 绑定业务 ID,打通与数据库的映射

MindMapNode当前是纯展示模型,要关联真实业务数据(如某知识点的knowledge_id),需扩展字段。不要修改原MindMapNode类(破坏封装),而是采用组合模式:新建KnowledgeNode继承MindMapNode,或更推荐——用SparseArray建立node.hashCode()到业务实体的映射。我选后者,因其零侵入、易维护:

// 在 Activity 或 ViewModel 中 private SparseArray<KnowledgeEntity> nodeToKnowledge = new SparseArray<>(); // 构建树时,为每个节点绑定知识实体 KnowledgeEntity entity = knowledgeDao.getById(1001L); MindMapNode node = new MindMapNode(entity.title); nodeToKnowledge.put(node.hashCode(), entity); // 在 onNodeClick() 回调中 mindMapView.setOnNodeClickListener(node -> { KnowledgeEntity entity = nodeToKnowledge.get(node.hashCode()); if (entity != null) { Intent intent = new Intent(this, KnowledgeDetailActivity.class); intent.putExtra("knowledge_id", entity.id); startActivity(intent); } });

注意:node.hashCode()是安全的,因MindMapNode未重写hashCode(),默认使用对象内存地址,同一节点实例生命周期内不变。

5.2 动态加载:实现“滚动到底部 → 自动加载下一级子节点”的懒加载策略

原项目一次性加载全部节点,大数据量时内存吃紧。我们改造updateDrawStack(),使其支持分页加载。核心思路:为每个节点增加isLoaded标志,isExpanded = true且!isLoaded时,触发网络/数据库请求,成功后再addChild()并invalidate()。

// 修改 MindMapNode public boolean isLoaded = false; // 在 MindMapView 中新增方法 public void loadChildrenForNode(MindMapNode node) { if (node.isLoaded || node.children.isEmpty()) return; // 模拟异步加载(实际应替换为 Retrofit 或 Room 查询) new Thread(() -> { List<KnowledgeEntity> children = knowledgeDao.getChildren(node.knowledgeId); for (KnowledgeEntity childEntity : children) { MindMapNode childNode = new MindMapNode(childEntity.title); childNode.knowledgeId = childEntity.id; node.children.add(childNode); } node.isLoaded = true; // 切回主线程刷新 post(() -> { updateDrawStack(rootNode); invalidate(); }); }).start(); } // 在 onNodeClick() 中,若点击的是未加载的展开节点,则触发加载 if (node.isExpanded && !node.isLoaded) { loadChildrenForNode(node); }

5.3 样式定制表:5 个关键 Paint 参数及其业务含义,改一处即全局生效

参数名默认值业务含义修改建议
mTextPaint.setTextSize()14sp节点文字大小教育类 App 可设为16sp提升可读性;儿童类 App 可设为18sp
mTextPaint.setColor()#000000文字主色暗色主题下改为#FFFFFF,并同步改mLinePaint.setColor()
horizontalGap(传入calculatePosition)80f同级节点横向间距知识点密集时调小至50f;强调层级时调大至100f
verticalGap(传入calculatePosition)60f父子节点纵向间距流程图场景可增大至80f避免连线交叉;笔记场景可减小至40f省空间
mScaleFactor初始值1.0f默认缩放级别新用户引导时设为0.8f,让整棵树完整可见

提示:所有参数都应通过MindMapView的setConfig()方法集中管理,而非散落在各处。例如:

public void setConfig(Config config) { this.config = config; invalidate(); // 强制重绘以应用新样式 } public static class Config { public float textSize = 14f; public int textColor = Color.BLACK; public float horizontalGap = 80f; // ... 其他参数 }

从那以后我每次接手新需求,只要涉及“树形结构可视化”,第一反应就是翻出这个思维导图项目,把MindMapNode和双堆栈逻辑抄过来,再根据业务字段微调calculatePosition()和drawNode()。它不花哨,但像一把瑞士军刀——削铅笔、拧螺丝、开罐头,样样都能干,而且刀刃永远锃亮。希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

Unity 2020+ 原生 iOS 集成:UnityFramework 完整接入指南

Unity 2020+ 原生 iOS 集成:UnityFramework 完整接入指南

如果只是做纯 Unity 手游&#xff0c;Unity 自己出包就行&#xff0c;根本不涉及原生集成这件事。但 2020 年之后&#xff0c;我接到的咨询越来越集中于同一个问题&#xff1a;“如何把 Unity 集成到原生 iOS 应用里”。典型场景是&#xff1a;团队手里已经有一个成熟的原生 Ap…

2026/10/20 12:54:45 阅读更多 →
从零安装RockyLinux 8.6:核心命令实战指南(1/2)

从零安装RockyLinux 8.6:核心命令实战指南(1/2)

前一阵子给一台新机器做系统选型&#xff0c;翻了一圈发行版之后&#xff0c;还是决定把生产环境定在RockyLinux 8.6上。很多朋友问为什么不用更新的版本&#xff0c;其实 8.6 这个版本在整个 8 系列里属于仓库最成熟、生态兼容性最好的节点之一&#xff0c;用来学习 Linux 核心…

2026/10/20 12:53:45 阅读更多 →
Day 12 | 优惠券、秒杀与拼团:电商三把刀

Day 12 | 优惠券、秒杀与拼团:电商三把刀

摘要&#xff1a;商城的订单&#xff08;Day10&#xff09;和支付&#xff08;Day11&#xff09;都搞定了&#xff0c;今天上促销三板斧——优惠券、秒杀、拼团。优惠券搞定生成/领取/核销/过期的完整生命周期&#xff1b;秒杀用 Redis 预减库存加异步下单扛住高并发&#xff1…

2026/10/20 12:53:45 阅读更多 →

最新新闻

智能驾驶感知算法岗面试高频考点与实战指南

智能驾驶感知算法岗面试高频考点与实战指南

智能驾驶感知算法岗的面试&#xff0c;跟我平时帮人做模拟面试时看到的准备方式&#xff0c;几乎不在一个频道上。很多人拿到一份八股题集就开始背&#xff1a;Transformer结构、Focal Loss公式、NMS流程、BEV感知演进……背得滚瓜烂熟&#xff0c;结果一进面试间&#xff0c;被…

2026/10/20 13:38:46 阅读更多 →
Hyper-Laplacian先验快速去卷积:从原理到工程实现

Hyper-Laplacian先验快速去卷积:从原理到工程实现

简介&#xff1a;这份资源是图像去模糊方向经典论文《Fast Image Deconvolution using Hyper-Laplacian Priors》的配套Matlab实现&#xff0c;面向从事图像恢复、计算摄影与视觉研究的学生和开发者&#xff0c;用于解决模糊图像复原中卷积核未知、边缘细节难以保持的问题。压缩…

2026/10/20 13:38:46 阅读更多 →
TensorFlow从零手写ResNet18:80行代码详解残差连接与BasicBlock实现

TensorFlow从零手写ResNet18:80行代码详解残差连接与BasicBlock实现

1. 为什么80行代码值得认真拆解ResNet18这个网络结构&#xff0c;搞深度学习的人几乎没有不知道的。2015年那篇残差学习的论文出来之后&#xff0c;基本上把深层网络训练难的问题给解决了。但很多人第一次接触ResNet&#xff0c;要么是直接调tf.keras.applications.ResNet50&am…

2026/10/20 13:38:46 阅读更多 →
Hyper-Laplacian先验快速去卷积:查找表加速非凸优化

Hyper-Laplacian先验快速去卷积:查找表加速非凸优化

简介&#xff1a;这份资源是图像去模糊领域经典论文《Fast Image Deconvolution using Hyper-Laplacian Priors》的配套Matlab实现&#xff0c;面向从事图像恢复、计算摄影与视觉研究的学生和开发者&#xff0c;用于解决模糊图像复原及盲去卷积中卷积核与清晰图像联合估计的问题…

2026/10/20 13:38:46 阅读更多 →
10月arXiv机器学习论文盘点:强化学习、长尾分布与数据效率

10月arXiv机器学习论文盘点:强化学习、长尾分布与数据效率

1. 另类入口&#xff1a;当arXiv变成一座每周更新的森林如果说学术搜索像一张精准的地图&#xff0c;那arXiv的cs.LG板块就更像一片每周定时更新的森林——你永远知道自己能在这里找到最新的机器学习论文&#xff0c;但也正因为"最新"这两个字&#xff0c;每次走进来…

2026/10/20 13:38:46 阅读更多 →
AI编程工具进化:从代码补全到能独立交付项目的“许愿机”

AI编程工具进化:从代码补全到能独立交付项目的“许愿机”

说实话&#xff0c;我已经不太想用“Coding Agent”来称呼手上这批AI开发工具了。不是矫情&#xff0c;是最近连续几周用它们做真正的项目——不是教它写个冒泡排序或者生成一段样板代码&#xff0c;而是从需求梳理、数据结构设计到部署脚本、线上监控告警的完整链路走下来——…

2026/10/20 13:37:45 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →