LVGL 指针表盘动态效果设计与实现
1. 从零开始理解LVGL指针表盘的核心大家好我是老张在嵌入式UI这块摸爬滚打十多年了从早期的ucGUI玩到现在风头正劲的LVGL。今天咱们不聊那些复杂的控件组合就聚焦一个特别经典又有点“炫技”的玩意儿——指针表盘。你是不是也觉得在智能手表或者一些工业HMI界面上一个流畅转动的指针表盘比干巴巴的数字时钟有质感多了没错这种动态效果是提升产品“高级感”的利器。你可能看过一些教程知道要用lv_img_set_angle来旋转图片。但真到自己动手问题就来了为什么我的指针转起来位置不对像是绕着尾巴在甩为什么秒针一跳一跳的一点都不丝滑别急这些问题我当年都踩过坑。今天我就带你从原理到代码手把手实现一个带实时动态效果的LVGL指针表盘。我们不只满足于“能动”更要追求“动得漂亮、动得高效”。我会把那些官方手册里一笔带过但实际开发中至关重要的细节掰开揉碎了讲给你听。首先咱们得达成一个共识在LVGL里实现指针表盘本质上就是控制一张图片指针绕着某个特定的点轴心进行旋转。听起来简单吧但这里藏着三个关键点图片资源、旋转轴心Pivot Point和角度计算与刷新。原始文章提到了这些概念但咱们要钻得更深。比如图片为什么一定要带Alpha通道旋转轴心坐标到底怎么算是像素点还是百分比实时更新时如何避免GUI卡顿这些才是做出商用级效果必须搞明白的。接下来咱们就一步步拆解。2. 打好地基图片资源的准备与处理俗话说“巧妇难为无米之炊”咱们的“米”就是指针和表盘的图片。这一步如果没处理好后面怎么写代码都白搭。原始文章提到了用LVGL官方的在线转换工具这没错但里面有几个坑我必须给你指出来。2.1 图片设计的门道为什么必须是PNG透明底首先请务必使用PNG格式且背景透明的指针图片。就像原始文章里用的秒针图只有指针本体是有颜色的其余部分全是透明。为什么非得这样你想啊如果指针图片是一个带着白色方形背景的JPG旋转的时候那个方形的背景块也会跟着转岂不是把下面的表盘都给挡住了那画面简直没法看。透明背景就是为了让指针“无缝”叠加在表盘上。在LVGL的图像转换工具里颜色格式Color format的选择至关重要。这里我强烈建议对于指针这类需要叠加显示的图片无脑选择CF_TRUE_COLOR_ALPHA。我来解释一下CF_TRUE_COLOR是RGB888一个像素占3个字节红、绿、蓝。而CF_TRUE_COLOR_ALPHA是ARGB8888占4个字节多出来的那个AAlpha通道就是用来存储透明度的。0代表完全透明255代表完全不透明。有了这个通道LVGL在渲染时才能知道哪些部分该显示哪些部分该透出下面的图层。我见过有新手为了省那么一点内存指针图也用了不带Alpha的格式结果指针边缘全是锯齿状的毛边或者背景色去不干净非常影响美观。在资源紧张的嵌入式设备上我们可以后期用压缩算法但在源头上格式一定要选对。2.2 转换工具的高级玩法与本地化处理LVGL的在线转换工具lvgl.io/tools/imageconverter确实方便但它依赖网络而且对于大量图片或者需要自动化集成的项目就不太友好了。这里我分享一个更“极客”的做法使用LVGL提供的本地Python转换脚本。通常在你下载的LVGL源码包里在lvgl/scripts目录下可以找到lv_img_conv.py这个脚本。它的功能比网页版更强大。你可以写一个批处理脚本一键转换整个目录的图片。比如下面这个命令python lv_img_conv.py -f -c4 -o ./output ./images/pointer.png我来解释一下参数-f表示输出为C文件。-c4就是指定颜色格式为CF_TRUE_COLOR_ALPHA通常4对应ARGB8888。-o ./output指定输出目录。最后是输入图片路径。用脚本的好处是能集成到你的编译系统比如Makefile或CMake里实现图片资源的自动化管理。修改了设计稿重新编译一下图片资源自动就更新了非常省心。转换成功后你会得到一个.c文件里面就是一个巨大的数组存储了图片的所有像素信息。按照原始文章的方法把它放到你的工程目录比如gui/assets/下并在需要使用它的地方用LV_IMG_DECLARE(pointer_img)声明一下这个资源就准备好了。记住这个声明只是告诉编译器“有这么个图片变量”它实际不占你当前文件的RAM数据是存在Flash里的。3. 搭建舞台创建图像对象与层级管理资源准备好了接下来就是“搭台唱戏”。我们要把表盘背景图和三个指针时、分、秒在屏幕上正确地显示出来并且安排好它们的“父子关系”。3.1 创建表盘背景一切的基石首先我们创建表盘背景。这个最简单因为它不需要动。// 声明表盘背景图片假设已转换并添加 LV_IMG_DECLARE(dial_background_img); // 创建图像对象 lv_obj_t * dial_bg lv_img_create(lv_scr_act(), NULL); // 以当前屏幕为父对象 lv_img_set_src(dial_bg, dial_background_img); lv_obj_align(dial_bg, NULL, LV_ALIGN_CENTER, 0, 0);这里lv_img_create的第一个参数是父对象我们传lv_scr_act()表示创建在当前活跃的屏幕上。lv_obj_align将它居中。表盘背景通常就是屏幕的中心所以对齐方式用LV_ALIGN_CENTER偏移量给0。3.2 创建指针对象关键在于父子关系重头戏来了——创建指针。这里有一个非常重要的技巧不要把所有指针都直接创建在屏幕lv_scr_act()上你应该以表盘背景图dial_bg作为父对象来创建指针。这样做有两大好处自动层级管理子对象指针会天然显示在父对象表盘之上不用担心被遮挡。坐标系统简化子对象的坐标原点是父对象的左上角。当我们后续设置指针的旋转轴心时计算坐标会变得非常直观因为都是在“表盘”这个坐标系下进行的。LV_IMG_DECLARE(hour_hand_img); LV_IMG_DECLARE(minute_hand_img); LV_IMG_DECLARE(second_hand_img); // 创建时针以表盘为父对象 lv_obj_t * hour_hand lv_img_create(dial_bg, NULL); lv_img_set_src(hour_hand, hour_hand_img); lv_obj_align(hour_hand, NULL, LV_ALIGN_CENTER, 0, 0); // 先居中放置 // 创建分针同样以表盘为父对象 lv_obj_t * minute_hand lv_img_create(dial_bg, NULL); lv_img_set_src(minute_hand, minute_hand_img); lv_obj_align(minute_hand, NULL, LV_ALIGN_CENTER, 0, 0); // 创建秒针 lv_obj_t * second_hand lv_img_create(dial_bg, NULL); lv_img_set_src(second_hand, second_hand_img); lv_obj_align(second_hand, NULL, LV_ALIGN_CENTER, 0, 0);现在三个指针和表盘在视觉上是完全重叠居中的。别急这只是一个初始状态我们马上就让它们“各就各位”。4. 灵魂所在旋转轴心Pivot Point的精确设置这是实现指针表盘最核心、最容易出错的一步。lv_img_set_pivot这个函数决定了你的指针绕着哪一点旋转。如果设错了指针就会像没安好的风扇叶片一样胡乱摆动。4.1 理解Pivot的坐标系lv_img_set_pivot(img, x, y)里的x和y是相对于图片对象自身左上角的坐标。单位是像素。那么问题来了对于一个指针图片它的旋转轴心应该在哪里答案是在指针的“根部”也就是它应该被固定在表盘中心的那个点。举个例子假设你有一张秒针图片它长150像素指针的“根部”位于图片从上往下数的第5像素、从左往右数的第75像素的位置假设指针是水平方向根部在中间。那么它的旋转轴心(pivot_x, pivot_y)就应该是(75, 5)。如何确定这个点我常用的笨办法但很有效用Photoshop、GIMP甚至Windows画图打开你的指针图片把鼠标移动到你觉得应该是旋转中心的位置记下软件状态栏显示的坐标。这个坐标就是(pivot_x, pivot_y)。4.2 在代码中设置轴心知道了坐标设置起来就很简单了// 设置时针的旋转轴心假设其根部在图片的 (25, 5) 位置 lv_img_set_pivot(hour_hand, 25, 5); // 设置分针的旋转轴心假设在 (20, 4) lv_img_set_pivot(minute_hand, 20, 4); // 设置秒针的旋转轴心假设在 (75, 5) lv_img_set_pivot(second_hand, 75, 5);重要提示设置pivot的时机必须在设置图片源lv_img_set_src之后。因为LVGL需要先知道图片的尺寸才能正确理解你给的坐标。我建议在创建对象、设置图片源、对齐之后紧接着就设置轴心。设置完成后虽然指针还没转但它已经“锚定”在了正确的位置。此时你再观察指针的根部应该正好对准了表盘的中心点。5. 让指针动起来角度计算与实时更新轴心设好了现在就该让指针根据时间旋转了。这里涉及两部分角度计算和定时刷新。5.1 时间获取与角度换算首先你需要一个可靠的时间源。在原始文章的SDK环境里是从RTC实时时钟模块获取一个rtc_tm结构体。在其他平台你可能用time()函数、NTP网络对时或者从系统Tick里推算。这里我们假设你已经有了时、分、秒的整数变量hour,minute,second。角度的计算要遵循钟表的物理规律秒针一圈360度60秒走完所以每秒走360 / 60 6度。分针同理每分钟也是走6度。但是为了更精细分针的移动最好也能关联秒数实现“顺滑”移动即每分钟走6度每秒钟走6 / 60 0.1度。时针一圈360度12小时走完所以每小时走360 / 12 30度。同时时针也应该随分钟移动每分钟走30 / 60 0.5度。因此计算当前精确角度的公式如下// 假设 hour 是24小时制我们转换为12小时制用于表盘 int hour_12 hour % 12; // 计算角度。LVGL的角度单位是0.1度所以最终结果要乘以10。 int second_angle second * 6 * 10; // 秒 * 6度 * 10 int minute_angle (minute * 6 second * 0.1) * 10; // (分*6 秒*0.1)度 * 10 int hour_angle (hour_12 * 30 minute * 0.5) * 10; // (时*30 分*0.5)度 * 105.2 实现平滑的动画刷新最粗暴的方法就是开一个1秒的定时器在回调函数里获取时间、计算角度、然后设置角度。但这样秒针是“跳格”的不够精致。我们可以利用LVGL自带的动画Animation或任务Task系统来实现更平滑的效果。这里我推荐使用lv_task_create创建一个高优先级任务比如每100毫秒甚至50毫秒执行一次。在这个任务里我们更新秒针和分针、时针的角度实现“扫秒”效果。// 定义一个任务回调函数 static void update_clock_task(lv_task_t * task) { // 1. 获取当前精确时间可能需要用到毫秒级精度 // 假设我们有一个函数 get_current_ms() 返回自启动以来的毫秒数 static uint32_t last_ms 0; uint32_t current_ms get_current_ms(); uint32_t elapsed current_ms - last_ms; if(elapsed 100) return; // 控制刷新频率大约100ms一次 last_ms current_ms; // 2. 计算总秒数带小数 // 假设从RTC获取了时、分、秒的整数部分 // 我们可以用一个全局变量记录从某个起点开始的总秒数并随时间累加0.1秒 static float total_seconds 0.0; total_seconds elapsed / 1000.0; int hours, minutes; float seconds; // 将 total_seconds 分解为时、分、秒带小数 // ... 这里需要你的时间逻辑 ... // 3. 计算精确角度 float second_angle_f (seconds) * 6.0; // 秒针角度度 float minute_angle_f (minutes seconds/60.0) * 6.0; // 分针角度 float hour_angle_f (hours % 12 minutes/60.0) * 30.0; // 时针角度 // 转换为LVGL单位0.1度 int second_angle (int)(second_angle_f * 10); int minute_angle (int)(minute_angle_f * 10); int hour_angle (int)(hour_angle_f * 10); // 4. 应用旋转 lv_img_set_angle(second_hand, second_angle); lv_img_set_angle(minute_hand, minute_angle); lv_img_set_angle(hour_hand, hour_angle); } // 在初始化函数中创建任务 void clock_ui_init() { // ... 创建表盘和指针对象 ... // ... 设置旋转轴心 ... // 创建更新任务优先级较高周期100ms lv_task_create(update_clock_task, 100, LV_TASK_PRIO_HIGH, NULL); }通过这种方式你的秒针将会平滑地扫过表盘而不是卡顿地跳动视觉效果会提升一个档次。对于分针和时针由于它们移动很慢每100ms更新一次也足以保证平滑性。6. 性能优化与高级技巧如果你的设备性能足够上面那样做已经很好。但在资源紧张的MCU比如STM32F1或者中科蓝讯这类蓝牙芯片上我们还得琢磨优化。6.1 局部刷新与脏矩形每次调用lv_img_set_angleLVGL默认会标记整个图像对象区域为“无效”从而触发整个区域的重新绘制。对于重叠在一起的三个指针和表盘这会导致大量不必要的像素重绘严重降低帧率。优化策略启用LVGL的局部刷新Partial Refresh并利用脏矩形Invalidate Area。但更直接的办法是我们可以手动计算指针旋转前后所覆盖的区域只刷新这两个区域的并集。LVGL 8.x版本对局部刷新的支持更好。在7.x中一个更务实的做法是确保你的表盘背景图是不变的并且将其设置为“不无效化not invalidate”。但这需要修改LVGL底层机制比较复杂。对于大多数应用一个简单的优化是将秒针、分针、时针放在不同的图层layer或者至少确保它们是不重叠的独立对象这显然不成立。实际上更有效的做法是使用lv_img_set_angle的“兄弟”函数lv_img_set_angle_start和lv_img_set_angle_end如果版本支持来设置动画让LVGL的动画系统去处理中间帧的刷新它内部可能做了优化。6.2 图片资源优化缩小与压缩指针图片往往是一个细长的矩形但透明区域很大。转换时可以尝试在在线工具或脚本中勾选“裁剪Crop”选项LVGL会自动剪掉图片四周全透明的区域只保留有像素的部分。这能显著减少存储图片的数组大小。另外对于颜色数较少的指针比如纯黑色可以考虑使用CF_INDEXED_1/2/4/8BIT这类索引色格式配合调色板能极大压缩体积。不过这会增加一点解码开销需要权衡。6.3 应对屏幕撕裂双缓冲与垂直同步当指针快速平滑移动时你可能会在屏幕上看到“撕裂”现象即同一帧里显示了上一帧和下一帧的部分内容。这是因为绘制速度跟不上刷新速度。在有能力的情况下启用LVGL的双缓冲Double Buffering。这需要你的显示驱动支持。原理是LVGL在后台缓冲区off-screen buffer完成一整帧的绘制然后一次性交换到前台显示避免撕裂。如果无法实现双缓冲可以尝试在任务更新后调用lv_refr_now(NULL)强制立即刷新。但这可能会打乱LVGL自身的刷新节奏要谨慎使用。7. 调试与常见问题排查实现过程中你肯定会遇到各种奇怪的问题。这里我分享几个最常见的“坑”和解决办法。问题一指针位置不对没在中心旋转。检查1确认lv_img_set_pivot的坐标是否正确。用画图软件打开指针图片仔细核对旋转中心点的像素坐标。检查2确认指针对象的父对象是不是表盘背景dial_bg。如果父对象是屏幕那么轴心坐标的参考系就变了计算会非常混乱。检查3在设置轴心后可以暂时给指针设置一个明显的角度比如90度看看它是不是绕着预期的点转。问题二指针旋转时边缘有残留色块或锯齿。检查1图片转换时是否选择了CF_TRUE_COLOR_ALPHA格式只有这个格式支持真正的透明。检查2原始PNG图片的边缘是否处理干净有时在Photoshop里需要将图层样式“清除”掉并确保背景层是隐藏的。问题三界面很卡动画不流畅。检查1刷新任务lv_task的周期是否太短尝试调到200ms或500ms看是否改善。先保证功能再优化流畅度。检查2是否开启了LVGL的日志LV_USE_LOG关掉日志能提升不少性能。检查3检查你的LVGL刷新周期配置LV_DISP_DEF_REFR_PERIOD。太短会增加CPU负担。检查4使用性能分析工具如SEGGER SystemView查看CPU使用率确认瓶颈是在图形绘制还是你的应用逻辑。问题四时间不准有累积误差。检查你的时间源是否准确如果是软件模拟的Tick误差会很大。尽量使用硬件RTC或高精度定时器作为时间基准。在更新任务中应以真实流逝的硬件时间来计算角度而不是简单地在每次回调中加一个固定值。最后别忘了LVGL官方文档和论坛是你最好的朋友。遇到诡异的问题去forum.lvgl.io上用英文描述清楚你的现象、平台和代码片段社区里有很多热心的大神。实现一个完美的指针表盘就像是给设备注入了灵魂虽然过程会遇到不少挑战但看到指针最终平稳优雅地转动起来的那一刻所有的调试都是值得的。希望我的这些经验能帮你少走弯路更快地做出令人满意的效果。

相关新闻

NHANES数据库实战:从数据合并到加权分析

NHANES数据库实战:从数据合并到加权分析

1. 为什么NHANES分析必须加权?一个真实的故事 如果你刚接触NHANES数据库,可能会觉得它和普通的调查数据没什么两样,无非是下载、合并、跑个回归。我刚开始也是这么想的,结果差点在第一个项目上翻车。那会儿我分析的是美国成年人的…

2026/7/5 12:07:54 阅读更多 →
Windows系统下OpenCV摄像头调用全攻略:从设备管理器到cv2.VideoCapture的正确姿势

Windows系统下OpenCV摄像头调用全攻略:从设备管理器到cv2.VideoCapture的正确姿势

Windows系统下OpenCV摄像头调用全攻略:从设备管理器到cv2.VideoCapture的正确姿势 你是否也曾在Windows上兴致勃勃地打开代码,准备用OpenCV调用USB摄像头大展身手,却被一行简单的cv2.VideoCapture(0)卡住,屏幕上只留下一串令人困惑…

2026/5/17 12:32:16 阅读更多 →
BERT、RoBERTa与DeBERTa:预训练语言模型的演进与应用

BERT、RoBERTa与DeBERTa:预训练语言模型的演进与应用

1. 从“单向”到“双向”:BERT如何改变了NLP的游戏规则 如果你在2018年之前接触过自然语言处理,那你一定记得那个“痛苦”的年代。那时候,想让机器理解一句话的意思,我们得费尽心思地设计各种特征,或者用循环神经网络&…

2026/5/17 12:32:15 阅读更多 →

最新新闻

V4L2 零拷贝与内存分配机制

V4L2 零拷贝与内存分配机制

在 Linux 嵌入式多媒体与 AI 边缘计算(如 RK3588 平台)中,为了实现极低延迟和降低 CPU 占用,通常需要打通摄像头(Camera)、图像格式转换模块(RGA/GPU)、AI 加速器(NPU&am…

2026/7/6 1:01:30 阅读更多 →
KYC形同虚设?揭秘黑产绕过金融机构身份核验全套手法

KYC形同虚设?揭秘黑产绕过金融机构身份核验全套手法

KYC(Know Your Customer,了解你的客户)并非信贷行业的专属课题,而是数字经济时代每一个需要建立"信任关系"的商业场景所共有的核心命题。无论是金融、电商、出行还是短视频,当平台试图确认"站在对面的究…

2026/7/6 1:01:30 阅读更多 →
Agentic Testing实战:自主AI测试代理架构与实现

Agentic Testing实战:自主AI测试代理架构与实现

# Agentic Testing实战:自主AI测试代理架构与实现## 一、背景与挑战:传统测试自动化的天花板当CI/CD流水线每天触发数百次测试执行,当微服务架构的API变更频率以分钟计,传统基于录制回放或关键字驱动的测试框架逐渐暴露出结构性缺…

2026/7/6 1:01:30 阅读更多 →
Windows上的安卓应用安装神器:APK安装器完整指南

Windows上的安卓应用安装神器:APK安装器完整指南

Windows上的安卓应用安装神器:APK安装器完整指南 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 想在Windows电脑上轻松安装安卓应用吗?APK安装…

2026/7/6 0:59:29 阅读更多 →
基于STM32单片机宠物项圈 宠物防丢定位系统 电子围栏防丢报警32(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于STM32单片机宠物项圈 宠物防丢定位系统 电子围栏防丢报警32(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于STM32单片机宠物项圈 宠物防丢定位系统 电子围栏防丢报警32(设计源文件万字报告讲解)(支持资料、图片参考_相关定制)_ 功能说明 :通过STM32单片机进行数据处理OLED液晶显示当前经纬度、蓝牙状态:断开/连接通过GPS模块定位当前…

2026/7/6 0:59:29 阅读更多 →
基于STM32单片机智能窗帘控制系统智能晾衣架设计定时雨滴光线32(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于STM32单片机智能窗帘控制系统智能晾衣架设计定时雨滴光线32(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于STM32单片机智能窗帘控制系统智能晾衣架设计定时雨滴光线32(设计源文件万字报告讲解)(支持资料、图片参考_相关定制)_ 版本1:光线温湿度舵机控制风扇降温除湿自动/手动模式 ★. 光敏采集当前环境光照强度 ★. DHT11传感器检测环境温度和湿…

2026/7/6 0:59:29 阅读更多 →

日新闻

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2与MySQL单元测试兼容性:5个关键SQL语句差异与规避方案1. 单元测试中的数据库兼容性挑战在Java开发领域,单元测试是保证代码质量的重要环节。当应用涉及数据库操作时,测试环境的搭建往往成为开发者的痛点。H2数据库因其轻量级、内存模式和快…

2026/7/6 0:01:17 阅读更多 →
Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘 【免费下载链接】rbtray A fork of RBTray from http://sourceforge.net/p/rbtray/code/. 项目地址: https://gitcode.com/gh_mirrors/rb/rbtray 你是否厌倦了Windows任务栏上密密麻麻的图标&…

2026/7/6 0:01:17 阅读更多 →
Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C 运行时库一键安装终极指南:告别DLL缺失烦恼 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况:下载了…

2026/7/6 0:05:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻