1. 圆角需求看着简单,真正动手才发现路子这么多
这几年做Android客户端的同学应该都有体会,几乎每个版本的UI稿里都会出现圆角元素——卡片、按钮、输入框、弹窗、图片缩略图,十个界面里有八个要求"四角磨圆"。圆角这东西,视觉上就几个像素的事,但真正落到代码里,实现方式却五花八门:有人用ShapeDrawable写XML,有人用GradientDrawable在代码里new,有人直接上自定义View重写dispatchDraw,还有人接第三方的ShapeView库。目标都是圆角,但每种方案的上手成本、灵活性、性能表现差距很大。
这个标题"Android 圆角控件简单快速实现",说白了就是冲着"省事、够用、不容易出问题"这三个诉求去的。我最早做圆角是在一个电商项目里,UI要求所有商品卡片的背景图都带8dp圆角,同时按压时还要有深浅变化。当时图省事,直接每个ImageView外包一层CardView,结果列表滑动帧率掉了不少,嵌套层级一多,布局渲染耗时肉眼可见。后来才意识到,圆角控件的实现不能靠"能跑就行",得根据业务场景选方案,否则后面各种怪问题都会找上门。
这篇文章想聊的内容就是:把Android上实现圆角的常见路子都过一遍,告诉你什么场景该选什么方案,再把"简单快速"这一路的核心代码和踩坑点拆开讲清楚。无论你是刚接手项目的初级开发,还是被UI稿逼着改样式的老手,按着下面的思路走,基本能少走大半年的弯路。
2. 先分清主流实现方案的边界,别等写完代码才发现选错方向
2.1 静态圆角首选XML Shape,但它的短板也很明显
传统做法是在res/drawable目录下写一个shape.xml,用<corners>标签指定圆角半径,然后给View设置background。比如下面这样:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#FF6A00" /> <corners android:radius="12dp" /> </shape>然后直接在布局里引用:
<Button android:layout_width="wrap_content" android:layout_height="48dp" android:background="@drawable/bg_orange_round" android:text="确认支付" />这一套的优点是几乎没有学习成本、所见即所得,Android Studio的预览里能直接看到效果,改起来也直观。但它的问题同样突出:XML是静态的,一旦需要在代码里动态改圆角半径、改填充色,就得重新inflate一份Drawable,或者写多个XML轮流切换。如果业务上同一个控件在不同状态下需要不同圆角大小,XML方案就会变得非常臃肿。
另一个坑是StateListDrawable的叠加。一个按钮要圆角、要按压变色、要禁用置灰,就得写selector,selector里再嵌套shape,代码量翻倍不说,稍不注意还会出现"按压态圆角丢失"的诡异问题。我见过不少项目里存了一堆bg_btn_round_normal.xml、bg_btn_round_pressed.xml、bg_btn_round_disabled.xml,改一个圆角半径要同步改四五个文件,纯粹是拿人力堆维护成本。
2.2 自定义View裁剪方案适合复杂场景,但别为了圆角而圆角
自定义View实现圆角的原理不复杂,重写dispatchDraw或者onDraw,在画布上裁出一个圆角路径,或者用canvas.clipPath()处理。代码示例如下:
@Override protected void onDraw(Canvas canvas) { Path path = new Path(); float radius = dp2px(16); path.addRoundRect( new RectF(0, 0, getWidth(), getHeight()), new float[]{radius, radius, radius, radius, radius, radius, radius, radius}, Path.Direction.CW ); canvas.clipPath(path); super.onDraw(canvas); }这种方法的好处是灵活度极高,四个角的半径可以分别设置,还能配合ViewOutlineProvider做一些阴影和波纹效果;缺点也很直接:需要处理padding、需要兼容硬件加速限制、每次绘制都要重新创建Path和RectF对象,稍不注意就会引起不必要的性能开销。而且clipPath在部分Android版本的硬件加速环境下,对非矩形路径的支持并不是完全可靠,实测在个别低端机上会出现"圆角边缘锯齿"现象。
所以我个人的判断是:如果只是把某个View的背景变成圆角,根本没必要上自定义View。自定义View裁剪的适用场景应该是"整个View的内容都要被裁剪成不规则形状",比如头像圆形化、直播礼物特效这种,而不是单纯给背景加个圆角。杀鸡用牛刀,后续维护的人看了代码只想骂人。
2.3 第三方库的本质:帮你封装了Edge-to-Edge的活,但别盲目引入
GitHub上有很多圆角控件库,常见的如ShapeView、RoundRectLayout之类,它们的核心思路大多是把上面提到的GradientDrawable或者canvas裁剪封装成一个自定义控件,暴露一些XML属性让你在布局里直接写app:radius="8dp"。这类库上手确实快,搭好依赖就能用,但有两个问题:
一是增加包体积和依赖复杂度。一个圆角需求,引进来一整个库,里面可能还带了很多用不上的功能——阴影、边框、渐变、水波纹,到头来你只用了其中10%的能力,剩下的代码全躺在包里。
二是库的维护周期不可控。有些库几年不更新,compileSdk版本还停留在老版本,一旦你升级了项目里的Android Gradle Plugin,可能直接就编译不过去了。我在实际项目里遇到过不止一次,为了兼容新版AGP,被迫去fork源码自己改,反而比老老实实自己写工具类更浪费时间。
所以"简单快速"不等于"引库了事"。下面我会重点讲一种不依赖第三方库、不用写一坨自定义View、代码量还很少的方案,实测在项目里可以直接落地。
3. 简单快速实现的核心思路:GradientDrawable动态创建与封装复用
3.1 为什么GradientDrawable是"快速"的关键
GradientDrawable是Android官方提供的Drawable实现类,专门用来描述带渐变、填充色、描边、圆角的图形。它最大的优势在于:可以在运行时动态修改参数,不用预先写XML文件,改完调setBackground就能立刻生效。
GradientDrawable drawable = new GradientDrawable(); drawable.setShape(GradientDrawable.RECTANGLE); drawable.setColor(Color.parseColor("#FF6A00")); drawable.setCornerRadius(dp2px(12)); view.setBackground(drawable);这段代码就是"快速实现"的地基:一个对象,三行配置,完事。相比起XML方案,它省去了文件创建和资源加载的流程;相比起自定义View方案,它不需要重写绘制方法,性能上是纯Drawable级别的开销,非常轻量。
很多人可能会说,这有什么稀奇的,我也写过。但"会用"和"用得优雅"是两码事。实际项目里,圆角背景的控件往往还要同时支持边框、不同状态的颜色变化、甚至不同圆角半径的切换。如果每次都在代码里new一个GradientDrawable再setBackground,代码会非常散,而且频繁创建对象容易触发不必要的GC。更好的做法是封装一个工具类,把常见参数收敛到少数几个方法里。
3.2 封装一个可复用的圆角背景工具类
以我自己的项目习惯,我会维护一个叫RoundBgUtils的工具类,核心方法是这样:
public class RoundBgUtils { public static GradientDrawable create(int radiusDp, int solidColor) { return create(radiusDp, solidColor, 0, Color.TRANSPARENT); } public static GradientDrawable create(int radiusDp, int solidColor, int strokeWidthDp, int strokeColor) { GradientDrawable drawable = new GradientDrawable(); drawable.setShape(GradientDrawable.RECTANGLE); drawable.setColor(solidColor); drawable.setCornerRadius(dp2px(radiusDp)); if (strokeWidthDp > 0) { drawable.setStroke(dp2px(strokeWidthDp), strokeColor); } return drawable; } /** * 四角不同圆角半径的场景比较少见,但Android是支持的。 * 数组顺序依次是:左上、右上、右下、左下 */ public static GradientDrawable createWithDiffRadius( int topLeft, int topRight, int bottomRight, int bottomLeft, int solidColor) { GradientDrawable drawable = new GradientDrawable(); drawable.setShape(GradientDrawable.RECTANGLE); drawable.setColor(solidColor); drawable.setCornerRadii(new float[]{ dp2px(topLeft), dp2px(topLeft), dp2px(topRight), dp2px(topRight), dp2px(bottomRight), dp2px(bottomRight), dp2px(bottomLeft), dp2px(bottomLeft) }); return drawable; } }有了这个工具类,业务代码里只需要一行调用:
// 普通圆角背景 view.setBackground(RoundBgUtils.create(12, Color.parseColor("#FF6A00"))); // 带边框的圆角背景 view.setBackground(RoundBgUtils.create(12, Color.WHITE, 1, Color.parseColor("#EEEEEE"))); // 只圆左半边,比如"标签+箭头"这种组合 view.setBackground(RoundBgUtils.createWithDiffRadius(8, 0, 0, 8, Color.BLUE));这种封装带来的收益是实打实的:调用方不需要关心GradientDrawable的细节,也不需要到处new对象,工具类内部统一处理dp转换、颜色解析、参数校验。更重要的是,如果要全局统一圆角风格(比如产品某天拍板,所有卡片圆角从8dp改成10dp),你只需要改工具类里的默认值,而不是满工程地找XML文件。
这里有个细节必须提醒:setCornerRadius(float radius)是所有角统一大小,而setCornerRadii(float[] radii)则可以指定四个角不同的半径。数组长度要求是8,也就是每个角两个值(x半径和y半径),两两相等就可以做到圆角效果。很多人在网上看到代码直接抄,数组长度写错直接崩溃,根本不看注释,所以我在工具类里专门加了注释说明。
3.3 代码动态设置的另一个选择:ViewOutlineProvider
如果你需要的不只是"背景圆角",而是让整个View的裁剪、阴影、波纹都跟随圆角形状走,那么比起GradientDrawable,另一个更"原生"的做法是用ViewOutlineProvider。
view.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), dp2px(12)); } }); view.setClipToOutline(true);这个方案的底层逻辑是Android 5.0开始引入的Outline机制,它定义了View的轮廓形状,系统可以基于这个轮廓做裁剪和阴影投射。和GradientDrawable相比,它的优势是:elevation阴影会跟着圆角走,不会出现"背景是圆角但阴影还是矩形"的割裂感;同时setClipToOutline(true)可以把View的内容(包括子View)一起裁剪成圆角。
但注意,setClipToOutline不能被滥用。一旦开了裁剪,View的内容边界就被锁死在圆角范围内,如果你的布局里有超出圆角区域的子元素(比如一个延伸出去的角标、一条横贯左右的分割线),它们会被直接裁掉,而且这个问题排查起来非常隐蔽,视觉上就是"少了一块",但布局代码肉眼很难看出问题。所以OutlineProvider适合的场景是"整个View区域都必须被圆角裁剪",而不仅仅是背景。
4. 高频控件实战:按钮、输入框、图片、进度条怎么快速上圆角
4.1 Button与TextView:带按压态、选中态怎么做才不啰嗦
纯色圆角按钮是项目里的高发需求。如果只做到"圆角+纯色",用上面工具类的create()方法就能搞定。但按钮往往需要按压反馈——按下时颜色变深、抬起恢复、禁用置灰。如果用GradientDrawable硬写,就得在OnTouchListener里手动改颜色:
button.setOnTouchListener((v, event) -> { GradientDrawable bg = (GradientDrawable) v.getBackground(); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: bg.setColor(pressedColor); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: bg.setColor(normalColor); break; } return false; });这个写法能跑,但放实际项目里问题不少:OnTouchListener和原有点击事件并存时容易出冲突、快速滑动时ACTION_CANCEL可能没触发导致颜色卡住、"点击区域偏移"等触摸拦截异常都会让状态恢复失灵。更稳妥的方式是借助StateListDrawable来管理不同状态的背景Drawable:
StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState( new int[]{android.R.attr.state_pressed}, RoundBgUtils.create(12, pressedColor) ); stateListDrawable.addState( new int[]{android.R.attr.state_enabled}, RoundBgUtils.create(12, normalColor) ); stateListDrawable.addState( new int[]{}, RoundBgUtils.create(12, disabledColor) ); button.setBackground(stateListDrawable);StateListDrawable是Android系统级别的状态选择机制,TextView、Button、EditText等控件的状态变化会自动触发对应的Drawable切换,完全不需要手动监听触摸事件。它的底层实现其实和你写XML selector是一样的,只不过我们通过代码动态构建,避免了XML文件的冗余。
这里有一个很容易被忽略的点:StateListDrawable里状态的顺序很关键。系统在匹配状态时会按addState的先后顺序逐一比对,一旦某个状态的条件满足就停止查找。所以一般要把更具体、优先级更高的状态放在最前面,比如state_pressed要放在state_enabled之前,否则按压态可能被enable状态先截胡,导致点击时完全没有视觉反馈。
4.2 EditText:圆角不是问题,问题是光标和背景焦点的适配
输入框加圆角,背景处理本身和Button没有区别,但EditText有两个衍生问题需要注意。
第一个是光标颜色。默认光标是跟随主题色的,很多项目里EditText的背景改成圆角浅色后,光标颜色和背景色融为一体,用户根本看不清输入位置。你需要在布局里显式指定android:textCursorDrawable,或者通过setCursorDrawable来设置。
第二个是焦点状态。常见的交互是:输入框在未聚焦时是浅灰圆角背景,聚焦后变成主题色圆角背景并带一个细边框。用StateListDrawable同样可以解决:
StateListDrawable focusBg = new StateListDrawable(); focusBg.addState( new int[]{android.R.attr.state_focused}, RoundBgUtils.create(12, focusBgColor, 1, focusStrokeColor) ); focusBg.addState( new int[]{}, RoundBgUtils.create(12, normalBgColor) ); editText.setBackground(focusBg);另外补充一个经验:如果EditText左边有图标或者前缀文字,直接用GradientDrawable setBackground会导致背景从控件最左边开始画,图标会被圆角背景盖住一部分。遇到这种情况,可以在EditText外层套一个FrameLayout,把圆角背景设置在外层容器上,EditText自己保持透明背景,这样图标区域也能被圆角约束,布局更干净。
不过涉及到"外层容器加圆角背景+内部EditText透明"这种结构时,还有个细节:外层容器的focusable状态要和EditText联动,否则可能出现在外层容器上点击无法聚焦输入框的问题。一般做法是在外层布局设置android:focusableInTouchMode="false",EditText设置android:focusable="true",让焦点事件自然穿透到内部的EditText。
4.3 ImageView图片圆角:选对方案能避免解码锯齿和OOM
ImageView的圆角处理有两种高频思路,一种是给ImageView的background设置圆角Drawable,一种是直接裁剪图片。前者只能让背景圆角,如果你的图片本身是矩形的,背景圆角根本遮不住图片,必须在上面提到的setClipToOutline或者自定义裁剪上做文章。
我推荐的最轻量组合是:ImageView外包一层ViewGroup容器,容器用GradientDrawable做背景,同时用ViewOutlineProvider裁剪子层。
ViewGroup wrapper = findViewById(R.id.iv_wrapper); wrapper.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), dp2px(8)); } }); wrapper.setClipToOutline(true);这样设置之后,ImageView在wrapper里的任何内容都会被圆角约束,不管图片源文件是正方形还是长方形,都不会出现图片角溢出的问题。
对比之下,另一个常见做法是用第三方图片加载库的roundedCorners变换,通过BitmapTransformation在解码时对Bitmap做圆角处理。这个方案的效果也很好,但有代价:它会额外生成一张圆角Bitmap,增加内存占用,加载大图时尤其明显。比如一张1200x1200的Bitmap,圆角处理会新创建一张等大的内存Bitmap再绘制,峰值内存翻倍。所以我的建议是:普通尺寸的缩略图可以用图片库的变换方法,但列表里的大图、高清图,优先用View层裁剪,避免内存抖动。
以上两个方案还有一个差异要提:图片库的圆角变换是在Bitmap层面做的,得到的圆角图片本身四个角是透明的,如果那张ImageView被单独保存或者被分享到别的场景,这张"圆角图"看起来就是自带透明底的。而View层裁剪只影响显示,内存里的Bitmap不受影响,是原图。具体用哪个,取决于你后续有没有"保存图片"的需求。
4.4 ProgressBar进度条圆角:ProgressDrawable的层级改造
进度条的圆角改造是个冷门需求,但一旦遇到就很头疼,因为ProgressBar有自己的内部Drawable结构,不是简单setBackground就能解决的。
进度条的背景和进度条本身是两层Drawable——背景是layer 1,进度是layer 2,它们被封装在LayerDrawable里。如果你想做一个"浅灰圆角轨道+橙色圆角进度"的进度条,需要手动构建这个LayerDrawable:
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{ RoundBgUtils.create(16, trackColor), RoundBgUtils.create(16, progressColor) }); layerDrawable.setId(0, android.R.id.background); layerDrawable.setId(1, android.R.id.progress); progressBar.setProgressDrawable(layerDrawable);注意setId这一步不能省。ProgressBar内部通过ID来识别哪一层是背景、哪一层是进度,如果不设置ID,进度条会显示异常——很可能两层Drawable重叠在一起,看起来就像一块纯色长条。
还有一个兼容性的细节:从Android 5.0开始,ProgressBar的进度层会自动应用透明度插值,也就是说进度条在动画时,系统会不断改变progress层的alpha。如果你手动设置的进度层Drawable不支持alpha变化(某些直接填充纯色的Drawable在部分机型上表现不同),进度动画会显得一顿一顿。实测下来,用GradientDrawable作为进度层,alpha变化的兼容性是OK的,因为它本身实现了透明度属性,所以放心用。
5. 常见问题与排查实录:这几类圆角"翻车现场"几乎人人都遇到
5.1 圆角不生效:先分清View本身的尺寸和背景Drawable的关系
这是出现频率最高的问题。代码里明明设置了圆角背景,但界面上看起来就是一个直角矩形。排查思路一般是这样:
先确认背景Drawable是否真的被GradientDrawable覆盖。很多控件在主题里自带background属性(比如MaterialButton、Switch),如果你用theme里默认的background替代了自己的Drawable设置,那圆角当然不生效。现场判断方法很简单:在布局预览里放大看控件的四个角,如果有系统控件的默认水波纹或者底色,那说明你的setBackground跑到了默认背景的下层,被盖住了。
第二个常见原因是控件高度不够。圆角半径大于View高度的一半时,视觉上不会出现"完全圆角"的效果,而是一个被截断的窄条。举个例子,一个8dp高的标签,你给它设了20dp圆角,两侧看起来就是两条弧线,中间是直的。这种情况不是"不生效",而是圆角被View的尺寸限制住了。
第三个原因和GradientDrawable的setCornerRadius调用时机有关。如果你在View的尺寸还没确定之前(比如onCreate里)就设置背景,Drawable本身是正常的,但View在第一次layout时可能会根据padding重算背景区域,某些机型上出现圆角被拉伸变形。这种问题一般出现在非对称的圆角(比如setCornerRadii),解决办法是在onSizeChanged后再设置,或者直接在onResume里刷新一次。
5.2 View转Bitmap导出图片,圆角区域变成黑底
项目里有个需求:把一个圆角卡片View转换成Bitmap,分享到社交平台。当时写完,生成的图片四个角是纯黑色的,不是透明的。这个坑的核心原因是View默认的背景是黑色,draw(Canvas)的时候不会自动把背景变透明。你需要先把View的背景Drawable临时清空,或者用一个带透明背景的Bitmap作为画布:
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawColor(Color.TRANSPARENT); view.draw(canvas);注意这里的canvas.drawColor(Color.TRANSPARENT)很关键,它先把画布填充成完全透明,再执行view.draw(),这样View本身的背景(如果也是透明)就不会把黑色带进Bitmap。如果你用的View背景是一层GradientDrawable圆角Drawable,Drawable在Bitmap里的绘制结果会保留圆角之外的区域为透明——前提是View的父容器没给它设置不透明背景,或者子View自带了一张非透明的图片。
反过来还有一种场景:导出图片后圆角是有了,但图片背景变成白色而不是透明。这是因为View所在的窗口背景是白色,draw()时窗口背景也被一并绘制进去。解决办法是先用setBackgroundColor(Color.TRANSPARENT)把View及其父容器的背景都改透明后再导出,导出完再还原。
5.3 列表滑动卡顿:圆角问题背后的过度绘制
很多开发者忽略了圆角View对列表性能的影响,尤其是RecyclerView里每个item都用setBackground创建新的GradientDrawable,还加上setClipToOutline(true)。这两种操作叠在一起,会带来两类开销:一是每个item滑动时都要重新创建Drawable对象,触发频繁的对象分配和GC;二是setClipToOutline会破坏系统原本的图层优化,让View需要更复杂的绘制指令。
我自己在项目里优化过这种情况:RecyclerView的item背景统一在ViewHolder绑定数据时做,如果item的圆角样式不变,就只在onCreateViewHolder时创建一次Drawable,复用给所有item;如果item的圆角半径和颜色会动态变化,就使用setColor而不是重新setBackground——GradientDrawable是支持直接改颜色的,性能开销比重新创建对象低得多。
GradientDrawable bg = (GradientDrawable) holder.itemView.getBackground(); bg.setColor(item.getBgColor());这个写法比holder.itemView.setBackground(RoundBgUtils.create(...))快很多,因为省去了Drawable对象的创建和View的setBackground内部校验逻辑。在千级列表滑动场景下,帧率差距能拉开5%-8%,实测没有明显掉帧。
另外一个值得注意的细节:如果你用setClipToOutline把item的根View裁剪成圆角,那么在RecyclerView的item复用过程中,系统会对裁剪区域做额外计算。实测在低端机上,如果item背景本身已经是圆角Drawable,就不要再开setClipToOutline,否则等于做了两遍圆角计算,完全是浪费。
5.4 Activity切换动画时的圆角"闪烁"
有时候圆角View在Activity转场动画过程中会出现边缘闪烁,表现为四个角有一瞬间的白色描边或者细闪线。这个问题的根源是转场动画期间,View的硬件图层被临时做了一次离屏渲染,圆角裁剪区域外的透明像素在GPU合成时和窗口背景产生了混合。
解决办法有两个方向:一是给Activity主题关闭或调整转场动画的windowEnterTransition相关属性,但这会影响全局动画风格,一般不推荐;更精确的方案是给圆角View单独设置layerType为LAYER_TYPE_HARDWARE,强制View在独立硬件图层上绘制,避免和转场动画合成时产生边缘像素穿插。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { view.setLayerType(View.LAYER_TYPE_HARDWARE, null); }注意,用了LAYER_TYPE_HARDWARE之后,View的内存占用会增加一张离屏缓冲,如果页面里大量使用,反而会拖慢启动速度和内存占用。所以我一般是针对性地加在个别动画频繁的圆角控件上,而不是全局无脑加。
6. 我个人的选型习惯:一套圆角方案跑完整个业务线
说回到"简单快速"这四个字。我认为在Android项目里做圆角控件,最省心、最稳定的组合不是某一种单一技术,而是分场景的三板斧:
- 静态、不常变的背景圆角,比如固定样式按钮、固定卡片背景,直接用GradientDrawable在代码里构建后setBackground,配合StateListDrawable处理点击态,比写一堆XML文件省事,等比稿调整时也只用改一个工具类。
- 需要内容也被裁剪成圆角的地方,比如图片区域、特殊布局布局容器,用ViewOutlineProvider + setClipToOutline,控制好使用范围就不用担心性能问题。
- 真正复杂的自定义形状(比如同时带圆角、描边、阴影、渐变、不规则轮廓),才建议单独写自定义View或者引入成熟的三方库,不要为了省几行代码硬扛。
这个思路我后面又用到过其他业务模块:报名活动的卡片、个人中心的头像框、直播间的礼物气泡,基本都是同一套工具类加少量定制,新需求来时不需要再从零想方案。
还有一点是大家容易忽略的——代码里设置圆角背景之后,尽量在布局文件里给View一个tools:background预览值,否则Studio的预览窗口会一直显示默认的灰白色块,UI在角落里拿着设计稿对半天,最后发现只是预览问题,白白浪费沟通时间。
至于有人关心的"圆角能不能跑在低版本Android上"——GradientDrawable从API 1就有,OutlineProvider需要API 21,但如果你用androidx的ViewOutlineProvider兼容类,低版本也能自动降级为无裁剪模式。所以兼容性不是阻碍方案选型的理由。真正影响判断的,永远是你的业务场景是"只贴一层背景"还是"需要整体裁剪",把这两类需求分清,圆角控件这事就算拿捏住了。