ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸿蒙自绘UI:RoundRect圆角矩形的创建与圆角设置全指南

鸿蒙自绘UI:RoundRect圆角矩形的创建与圆角设置全指南 做UI的应该都有这种感觉圆角矩形是界面里出现频率最高的图形从按钮、卡片到弹窗、标签几乎无处不在一开始我接触鸿蒙自绘UI的时候也以为圆角矩形就是给矩形加个半径的事真正上手之后才发现里面有不少细节——RoundRect怎么创建、圆角怎么设、单位怎么算、为什么有时候圆角画出来是方的这些坑不踩一遍真的记不住。这篇文章我就把RoundRect的创建和圆角设置完整梳理一遍包含代码示例和调参经验给同样在做鸿蒙应用开发的朋友做个参考。1. 先搞懂RoundRect圆角矩形在鸿蒙里是什么1.1 从普通矩形到圆角矩形到底多了什么普通矩形在鸿蒙的图形体系里叫Rect它只有四个值left、top、right、bottom拼出来就是一个四四方方的框。RoundRect从名字上就能看出来它是在Rect的基础上多了圆角这个属性也就是说它既保留了矩形的定位和宽高信息又额外记录了四个顶点处倒圆角的弧度大小。你可以这么理解Rect是一张没裁剪过的直角照片RoundRect是拿圆角刀给四个角修了一遍边的相框。修边之后矩形四个角不再是尖的而是变成了一段段圆弧同时原本的直线部分还是保持直线。这个直线加圆弧的组合就是RoundRect最核心的几何特征。很多刚接触图形绘制的开发者会问为什么不能用Rect拼一个圆角出来理论上你可以用四条直线加四段圆弧拼一个Path但这样做的问题是代码量多、计算繁琐、而且不同分辨率下圆弧的拟合效果还不一定好。RoundRect就是系统帮我们封装好的标准答案构造的时候直接给矩形和半径剩下的事情框架处理。1.2 RoundRect能解决哪些设计问题圆角矩形在现代UI设计中已经变成了一种视觉语言。之前的界面设计倾向于直角看起来硬朗但用户普遍反馈直角给人感觉太生硬尤其是大屏设备上大面积直角会让界面显得冷冰冰的。圆角从视觉上消解了这种尖锐感也让界面更柔和、更有亲近感。在HarmonyOS应用里RoundRect最常见的应用场景包括卡片的背景轮廓、按钮的形状、头像的裁剪区域、弹窗的遮罩边界以及各种列表项的按压态背景。除了美观之外圆角还有一个容易被忽视的作用——防止内容溢出时戳出尖锐的角。比如一张图片或者一段文字如果容器是直角内容一旦超出边界就可能出现像素扎出来的刺眼效果但容器是圆角的话即使内容溢出边缘也会被圆角兜住视觉上不容易穿帮。另外在做阴影和描边的时候RoundRect也很有用。鸿蒙的阴影计算支持基于RoundRect的外轮廓生成这样阴影的自然度和圆角完全匹配而不是拿一个大矩形硬套。2. 创建RoundRect的几种姿势2.1 最直接的构造方式给矩形加统一圆角RoundRect的创建方式并不复杂最基础的方法就是传入一个Rect再传两个半径值分别代表水平方向和垂直方向的圆角半径。从数学上讲圆角其实是一个1/4椭圆弧水平半径rx决定弧在x方向上的宽度垂直半径ry决定弧在y方向上的高度。如果rx等于ry那么圆角就是正圆的四分之一如果二者不相等圆角就是椭圆弧整个形状看起来会有一点点扁。import { Rect, RoundRect } from kit.ArkGraphics2D; // 创建一个从(0,0)到(200,100)的矩形统一圆角半径16 let rect: Rect new Rect(0, 0, 200, 100); let roundRect: RoundRect new RoundRect(rect, 16, 16);这里的16表示圆角在x和y方向上都是16最终画出来的四个角是半径16的1/4圆弧。实际操作里绝大多数场景rx和ry是相等的也就是正圆角只有当你有意做成椭圆的特殊观感时才会把rx和ry设成不同的值。我自己的习惯是先把Rect定义成具名变量再传给RoundRect而不是直接new一个匿名的Rect。原因很简单后续如果要调整圆角矩形的位置或大小直接复用Rect变量改值就行不用反复创建新对象。在频繁刷帧的绘制场景里减少对象创建对性能是有帮助的。2.2 精确到角用CornerRadius控制四个角的半径统一圆角虽然简单但现实中很多界面并不会四个角都一样。最典型的就是聊天气泡通常是左上、右上、右下三个角是圆角左下角是直角甚至有的气泡左下角还有一个小尾巴。这种情况如果只靠uniform的圆角半径是做不出来的。这时就需要CornerRadius出场了。CornerRadius直译过来就是角半径它用来描述一个RoundRect的四个角各自的圆角半径。鸿蒙的RoundRect构造函数支持直接接收一个CornerRadius对象从而实现对四个角的精细控制。import { CornerRadius, Rect, RoundRect } from kit.ArkGraphics2D; let rect: Rect new Rect(0, 0, 200, 100); // 分别指定左上、右上、右下、左下的圆角半径 let radii: CornerRadius new CornerRadius(16, 16, 0, 16); let roundRect: RoundRect new RoundRect(rect, radii);注意圆角半径的顺序按我上面的写法依次是左上(topLeft)、右上(topRight)、右下(bottomRight)、左下(bottomLeft)。这个顺序方向和Android的GradientDrawable不完全一样和CSS的border-radius倒是一致都是从左上角开始顺时针转一圈。写代码的时候一定要确认清楚不然调出来的形状跟你预期正好是镜像的排查半天才发现顺序错了。CornerRadius的用法在需要实现非对称圆角时几乎是必备的。比如列表卡片偶尔要把底部两个角改成直角以便和底部的操作栏对齐或者某个浮层只想在顶部显示圆角底部平贴屏幕。这些自适应细节用CornerRadius都能轻松搞定。2.3 从Rect转换与对象复用在一些场景下你手头已经有一个Rect想把它变成圆角矩形但又不想改变Rect本身的数值这时就可以通过RoundRect的构造方法把Rect包一层。RoundRect内部会持有这个Rect的引用所以你生成的RoundRect在逻辑上就是原Rect的圆角版本。如果你想从一个已有的RoundRect身上取回原始的Rect信息可以通过它的getRect方法拿到。这在你需要判断某个坐标点是否落在圆角矩形内部时很有用。鸿蒙的图形模块提供了一些现成的包含关系判断方法但如果你自己实现通常会先对比Rect的边界再对四个圆角区域做距离判断。关于复用我有一点实际体会在自绘UI中如果每个组件都在onDraw里new一个RoundRect那么每次刷新都会产生垃圾对象帧率一高就可能触发频繁的gc。真要追求性能可以在组件初始化时把RoundRect缓存起来只在尺寸变化时才重建。但要注意RoundRect内部收的Rect如果改变了数值RoundRect并不会自动感知需要重建RoundRect或者调用它的更新方法。3. 圆角设置的细节最容易被坑的就是这里3.1 单位问题vp和px别搞混鸿蒙开发中单位是个非常容易翻车的点。ArkUI组件布局用的尺寸单位是vpvirtual pixel也就是虚拟像素它在不同密度的设备上会对应不同数量的物理像素。而Drawing图形模块里的RoundRect包括Rect的四个坐标值、圆角半径用的都是物理像素px。这意味着什么如果你从组件尺寸里拿到的是vp值直接丢给RoundRect去画在高密度屏上就会画出一个比实际组件小很多的圆角矩形。比如一个宽度200vp的卡片在2倍密度设备上其实是400px如果你拿200传给Rect那画出来的背景只有卡片实际宽度的一半。解决办法也不复杂在进行绘制之前把vp转成px。鸿蒙提供了px2vp和vp2px相关的转换能力你可以通过UIAbilityContext的配置或者直接调用系统方法拿到当前屏幕的密度用物理像素 虚拟像素 × density来计算。// 假设density为2.0200vp对应的px为400px let px: number 200 * density; let rect: Rect new Rect(0, 0, px, px); let roundRect: RoundRect new RoundRect(rect, 16 * density, 16 * density);还有个细节圆角半径同样需要做换算。很多人在Rect的宽高上记得转px圆角半径直接写了16结果小屏上看起来圆角特别小大屏上又突然变圆了。这其实都是单位不一致导致的。我的建议是把所有从UI设计稿里拿到的vp值统一换算成px再绘制保持整个绘制链路单位一致就不会出这种问题了。3.2 圆角半径的边界条件圆角半径并不是设置多大就能画多大这里有个几何约束某个角上的水平半径不能超过矩形宽度的一半垂直半径不能超过矩形高度的一半。如果你设置的值超过了这个边界不同平台的处理逻辑不一样有的会自动截断有的会出现奇怪的渲染效果。鸿蒙的RoundRect在这方面相对稳健但也不是无脑处理。我遇到过的情况是把一个高度只有40的胶囊按钮圆角半径设到了40甚至60结果渲染出来圆角过大显得非常怪异。正确的做法是如果想让一个矩形看起来像胶囊两端半圆圆角的半径应该设置为高度的一半。比如高度40px半径20px就是标准的胶囊形超过20px之后视觉上不会变得更好看反而可能产生不可预期的结果。这里还有一个性能上的考虑圆角半径过大时RoundRect内部的圆弧拟合计算会更复杂一些。虽然现代设备的GPU算这个毫无压力但如果你在做高频动画一帧要绘制几十个RoundRect还是建议把半径控制在合理范围内。圈里流传的能用小圆角就不用大圆角的说法不完全是为了视觉美观也是为了渲染性能。3.3 不同API版本的兼容性处理HarmonyOS的API迭代速度比较快RoundRect相关的接口也经历了模块路径的调整。早期的版本中你要用ohos.graphics.drawing这个方式来导入图形模块现在更推荐使用kit.ArkGraphics2D。两者指向底层其实是一套能力但包名规范变了新工程默认用kit的导入方式老工程如果不升级还能继续用旧路径。如果你的应用还需要兼容API 11或更早的版本那么有一点要注意CornerRadius类的构造方式在不同版本里可能略有差异。旧版本里可能只接受一个半径参数不支持四角独立设置新版本才放开为多参数。我在实际开发中采取的策略是在工具类里封装一个创建RoundRect的工厂函数内部判断当前API版本如果支持四角半径就用CornerRadius否则退化为统一圆角。这样一来上层调用方不用关心版本差异也方便后续维护。另外导入模块的时候务必注意包名别写错了。kit.ArkGraphics2D导出的是整个2D图形能力包括Canvas、Paint、Rect、RoundRect、CornerRadius等但你仍然需要在文件顶部按需import。IDE的自动补全一般能帮你生成正确的import语句但如果你手写容易把Rect和RoundRect的引入路径弄混。4. 实操把RoundRect画到界面上4.1 在Canvas上绘制圆角矩形卡片创建RoundRect只是第一步真正要让它显示出来还得通过Canvas绘制。鸿蒙的Canvas组件分为两种上下文一种是偏向Web API风格的CanvasRenderingContext2D另一种是基于底层Drawing能力的DrawingRenderingContext。如果你要画的东西比较复杂或者需要直接操作RoundRect、Paint这类底层对象推荐使用DrawingRenderingContext。下面是一个完整的示例在Canvas的onReady回调里创建一个RoundRect再用Paint来填充绘制。import { Canvas, DrawingRenderingContext } from kit.ArkUI; import { Rect, RoundRect, Paint, PaintStyle, Canvas as DrawingCanvas } from kit.ArkGraphics2D; Entry Component struct RoundRectDemo { private drawingContext: DrawingRenderingContext new DrawingRenderingContext(); build() { Column({ space: 20 }) { Canvas(this.drawingContext) .width(100%) .height(200) .onReady(() { let canvas: DrawingCanvas this.drawingContext.canvas; let rect: Rect new Rect(20, 20, 300, 140); let roundRect: RoundRect new RoundRect(rect, 20, 20); let paint: Paint new Paint(); paint.setStyle(PaintStyle.FILL_STYLE); paint.setColor(0xFF007DFF); paint.setAntiAlias(true); canvas.drawRoundRect(roundRect, paint); }); } .width(100%) .height(100%) .padding(20) } }这段代码运行起来你会看到左上方出现一个圆角为20的蓝色矩形。关键点在于Paint的style设置如果只想显示圆角矩形的边框就把PaintStyle改成STROKE_STYLE同时用setStrokeWidth设置边框宽度如果想填充就用FILL_STYLE也可以两者都保留让图形既有填充又有描边。有一点需要特别提醒Canvas的onReady回调在组件初始化完成后会触发但如果你在回调里依赖了某些异步获取的数据比如从网络加载的图片尺寸那就得注意回调时机。因为Canvas可能已经绘制完一帧数据才回来这时你需要主动调用invalidate或者触发一次重绘否则画面不会更新。4.2 用RoundRect给组件做裁剪与背景在ArkUI里除了自绘Canvas其实还有更简便的方式用到RoundRect给普通组件设置圆角背景、裁剪区域。最常见的做法是直接在组件链式调用里用borderRadius属性但borderRadius内部也是生成RoundRect或类似的圆角描述。不过有一些场景用borderRadius并不方便。比如你需要根据组件实际尺寸动态计算圆角半径或者想用同一个圆角矩形既做背景又做裁剪这时直接使用clipShape会更加灵活。Entry Component struct ClipDemo { State cardWidth: number 200; State cardHeight: number 120; State radius: number 24; build() { Column({ space: 20 }) { Column() { Text(卡片内容) .fontSize(16) .fontColor(Color.White) } .width(this.cardWidth) .height(this.cardHeight) .backgroundColor(#FF3B30) .clipShape(new RoundRect( new Rect(0, 0, this.cardWidth, this.cardHeight), this.radius, this.radius )) .borderWidth(1) .borderColor(Color.Gray) } .width(100%) .padding(20) } }注意clipShape的Rect参数坐标是相对于组件自身坐标系而言的所以你写Rect(0, 0, cardWidth, cardHeight)就能覆盖整个组件区域。这样写的好处是当你动态改变cardWidth或者radius时裁剪区域会跟着变不需要手动更新其他属性。在实际项目里我最常用到clipShape的地方是头像和卡片圆角。头像一般用圆形其实圆形可以理解成圆角半径为宽高一半的RoundRect但用Circle单独处理更准确而卡片圆角则直接用RoundRect来裁剪保证裁剪区域和阴影、描边的轮廓完全一致视觉上不会出现圆角里套着直角的割裂感。4.3 圆角动画让切换更自然圆角不一定是静态的很多交互场景中需要让圆角动起来。比如某个卡片在正常状态下是直角矩形点击后变成圆角矩形或者菜单项在选中时圆角从小到大撑开产生一种气泡弹开的效果。在ArkUI里实现圆角动画思路并不复杂。你把圆角半径作为一个State变量然后通过animateTo或者动画参数驱动它的变化对应的组件属性或绘制参数就会跟着自动过渡。Entry Component struct RadiusAnimationDemo { State radius: number 4; State isExpanded: boolean false; build() { Column({ space: 20 }) { Column() .width(160) .height(80) .backgroundColor(#34C759) .clipShape(new RoundRect( new Rect(0, 0, 160, 80), this.radius, this.radius )) .onClick(() { animateTo({ duration: 300, curve: Curve.EaseOut }, () { this.isExpanded !this.isExpanded; this.radius this.isExpanded ? 40 : 4; }); }) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }这样点击绿色方块圆角就会从4平滑变化到40视觉上像一块平板慢慢变成了胶囊。这里有个小技巧圆角动画的起止值差异越大动画效果就越明显如果差异太小用户几乎感知不到变化那还不如不加动画。动画过程中也需要注意性能。如果你在onReady等回调中重新创建RoundRect对象并且每帧都触发重绘那GC压力会比较大。更好的做法是如果动画只是改变圆角半径尽量复用同一个RoundRect对象每次只更新半径值减少对象创建。5. 常见问题与排查记录5.1 圆角不生效先检查这几个地方做图形开发最烦的一件事就是代码写完运行起来发现圆角矩形画出来变成了普通矩形或者圆角被裁成了直角。这种事我遇到过不下三次每次排查的位置基本都在下面几个方向。第一个检查Rect的宽高。如果Rect的宽度或高度为0或者坐标赋值错误导致矩形区域不可见那画出来肯定什么角都没有。建议在调试阶段打印一下Rect的四个值确认left小于right、top小于bottom。第二个检查Paint的设置。如果Paint的style是STROKE_STYLE并且描边宽度特别大圆角看起来可能被吃掉一部分。另外有些Paint默认开了抗锯齿有些没开没开抗锯齿的边缘在圆角处会非常明显锯齿感会让你怀疑圆角没生效。第三个检查绘制顺序。如果你在画RoundRect之后又用普通Rect覆盖了一层或者绘制前没有重置Canvas的裁剪区域那圆角被覆盖掉也是很常见的。我习惯在绘制前先保存Canvas状态绘制完再恢复避免下一次绘制受上一次的影响。第四点是单位问题。前面提到过如果你在vp单位下直接使用Rect的坐标绘制出来的尺寸可能和组件本身不匹配。建议把所有输入统一成px再绘制。5.2 没有真机怎么办DevEco Studio也可以看到效果很多新手在学习鸿蒙开发的时候手里没有真机又装不了模拟器担心自己没法验证绘制效果。我的经验是不一定要真机也能做大部分基础调试。DevEco Studio内置的Previewer预览器可以直接预览ArkUI的声明式布局效果。虽然Previewer对Canvas自绘内容的支持不是100%很多底层的Drawing能力在预览器里可能表现不完整但RoundRect的创建和基本绘制一般在预览器里是可以看到大概效果的。你可以在Previewer里修改圆角半径观察整体的布局变化这比盲写代码高效得多。如果是较复杂的自绘场景Previewer支持不了那我建议你用模拟器试试。本地模拟器虽然没有真机的全部传感器能力但图形渲染管线是完整的RoundRect这类基础绘制都能正常跑。真机和模拟器之间确实存在渲染差异但差异主要体现在字体渲染、颜色精度等细节上对于圆角这种基础几何形状模拟器跑出来的结果有很高的参考价值。另外如果你的场景里只是给普通组件设置圆角背景那么Previewer的预览效果基本等于真机效果因为borderRadius、clipShape这些属性在预览器里都支持得不错。只有当你走到了Canvas自绘、操作Paint和RoundRect这一步才需要依赖模拟器或者真机来做最终验证。5.3 我的避坑清单我把实际开发中遇到的RoundRect相关坑点整理成了一张表方便你对照排查问题现象可能原因解决方案圆角画出来是直角Rect宽高为0或坐标顺序错误调试打印Rect的left/top/right/bottom圆角半径看起来不对vp与px单位混用统一换算成px后再设置四个角的圆角方向反了CornerRadius参数顺序不对按左上、右上、右下、左下顺序传入圆角有锯齿感Paint没有开启抗锯齿调用paint.setAntiAlias(true)圆角被后续矩形覆盖绘制顺序问题调整绘制顺序或使用save/restore动画中圆角跳变生硬状态变量更新没有加动画使用animateTo并设置合适的曲线圆角太大形状异常半径超过短边一半半径控制在min(width, height)/2以内低版本设备上没有四角独立圆角系统版本不支持CornerRadius封装工具类版本降级处理这些坑并不复杂但都很隐蔽。其中最容易被忽略的还是单位问题因为它不一定每次都会出问题只有在特定密度屏幕下才会暴露容易让人误以为是偶发bug。写在最后说实话RoundRect并不是一个很难的API但因为它处在一个偏底层的位置一旦涉及自绘和动画就会出现很多意想不到的细节。我的经验是做这类基础图形开发一定要把数据单位理清楚、把坐标边界调试好再谈效果优化。圆角这个东西设置对了一点都不起眼设置错了整个界面质感都会掉一个档次。先拿几个示例跑通基础流程再根据自己项目的需求去封装工具方法这套思路比上来就追求复杂效果要稳妥得多。
返回列表