背景
前段时间UI提了一个需求,要把RecyclerView的第一个Item始终切成上方圆角
因为页面是可配置的并不知道RecyclerView的第一个Item是哪一个View,
如果要每个item都判断的话, 会显得很繁琐,
所以我们要寻找一种通用的切割圆角的方式.
本项目地址RCVIew, 该项目demo是模仿了Android官方Demo- Sunflower写的
寻找
在各个博客寻找已有的方案时, 发现现有的切割的方案都有一定的局限性,
比如GcsSloop大佬 的rclayout, 大佬只封装了两个View, ImageView
以及RelativeLayout
,
公司项目中新写的layout
已经大部分都是ConstraintLayout
, 如果要修改所有的布局的话代价太大, 而且自定义View不够灵活.
在这时候找到了**API 21+**官方提供的一个API, 而且是View类里面的方法--setOutlineProvider(ViewOutlineProvider provider)
,
然后就使用该API的通用性来实现了一个非常通用的圆角布局
ViewOutlineProvider介绍
根据官方APi介绍, 我们可以了解到ViewOutlineProvider
是设置VIew的投影以及剪切View轮廓, 并且是在Android 5.0添加的.
现在把源码贴上来:
/**
* Interface by which a View builds its {@link Outline}, used for shadow casting and clipping.
*/
public abstract class ViewOutlineProvider {
/**
* Called to get the provider to populate the Outline.
*
* This method will be called by a View when its owned Drawables are invalidated, when the
* View's size changes, or if {@link View#invalidateOutline()} is called
* explicitly.
*
* The input outline is empty and has an alpha of <code>1.0f</code>.
*
* @param view The view building the outline.
* @param outline The empty outline to be populated.
*/
public abstract void getOutline(View view, Outline outline);
}
我们使用的就是getOutline
来规定View的轮廓, 来实现剪切View, 并且如果view的size变化了, 也会回调该方法, 这样也就可以剪切RecyclerView等size会变化的View
剪切圆角View
view.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
0,
0,
view.width,
view.height,
radius
)
}
}
view.clipToOutline = true
剪切圆形View
view.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setOval(
0,
0,
view.width,
view.height
)
}
}
view.clipToOutline = true
两句代码我们就实现了一个四周圆角的View, 是不是很简单方便.
有些同学会想要直接在xml中设置属性, 所以就有下面的封装
RCView
1.使用DataBinding
使用DataBiding提供的BIndingAdapter, 可以最灵活, 最通用性的实现圆角布局, 直接贴上代码
@BindingAdapter(
"app:usePadding",
"app:cornerRadiusSize",
"app:cornerRadiusType",
requireAll = false
)
fun setRoundCornerOutline(
view: View,
cornerRadiusSize: Float,
cornerRadiusType: Int = 0,
usePadding: Boolean = false
) {
view.outlineProvider = if (usePadding) {
getRoundCornerOutlineWithPadding(view.context, cornerRadiusSize, sRadiusType[cornerRadiusType])
} else {
getRoundCornerOutline(view.context, cornerRadiusSize, sRadiusType[cornerRadiusType])
}
view.clipToOutline = true
}
ViewUtils.kt
val sRadiusType = arrayOf(
RadiusType.ALL,
RadiusType.TOP,
RadiusType.LEFT,
RadiusType.BOTTOM,
RadiusType.RIGHT
)
fun getRoundCornerOutline(
radius: Float,
radiusType: RadiusType
): ViewOutlineProvider {
val ceilRadius = ceil(radius).toInt()
return object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
when (radiusType) {
RadiusType.ALL -> {
outline.setRoundRect(
0,
0,
view.width - view.paddingLeft,
view.height - view.paddingTop,
radius
)
}
RadiusType.TOP -> {
outline.setRoundRect(
0,
0,
view.width - view.paddingLeft,
view.height - view.paddingTop + ceilRadius,
radius
)
}
RadiusType.BOTTOM -> {
outline.setRoundRect(
0,
-ceilRadius,
view.width - view.paddingLeft,
view.height - view.paddingTop,
radius
)
}
RadiusType.LEFT -> {
outline.setRoundRect(
0,
0,
view.width - view.paddingLeft + ceilRadius,
view.height - view.paddingTop,
radius
)
}
RadiusType.RIGHT -> {
outline.setRoundRect(
-ceilRadius,
0,
view.width - view.paddingLeft,
view.height - view.paddingTop,
radius
)
}
}
}
}
}
fun getRoundCornerOutlineWithPadding(
radius: Float,
radiusType: RadiusType
): ViewOutlineProvider {
val ceilRadius = ceil(radius).toInt()
return object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
when (radiusType) {
RadiusType.ALL -> {
outline.setRoundRect(
view.paddingLeft,
view.paddingTop,
view.width - view.paddingLeft,
view.height - view.paddingTop,
radius
)
}
RadiusType.TOP -> {
outline.setRoundRect(
view.paddingLeft,
view.paddingTop,
view.width - view.paddingLeft,
view.height - view.paddingTop + ceilRadius,
radius
)
}
RadiusType.BOTTOM -> {
outline.setRoundRect(
view.paddingLeft,
view.paddingTop - ceilRadius,
view.width - view.paddingLeft,
view.height - view.paddingTop,
radius
)
}
RadiusType.LEFT -> {
outline.setRoundRect(
view.paddingLeft,
view.paddingTop,
view.width - view.paddingLeft + ceilRadius,
view.height - view.paddingTop,
radius
)
}
RadiusType.RIGHT -> {
outline.setRoundRect(
view.paddingLeft - ceilRadius,
view.paddingTop,
view.width - view.paddingLeft,
view.height - view.paddingTop,
radius
)
}
}
}
}
}
xml中使用
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:background="#ffffff"
app:cornerRadiusSize="@{@dimen/dp4}"
app:cornerRadiusType="@{@integer/corner_radius_all}">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/product_image"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:scaleType="centerCrop"
app:cornerRadiusSize="@{@dimen/dp4}"
app:cornerRadiusType="@{@integer/corner_radius_all}"
app:imageUrl="@{product.imageUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
优点:
- 无需多余的封装, 使用自定义View, 即可适用于任意View切割圆角
- 使用简单, 执行效率高
缺点:
-
因为BindingAdapter的原因, 在xml使用的必须要用
@{}
括起来 -
指定conerRadiusType和cornerRadiusSize的要使用提前定义好的value, 如果直接使用Float, 或者Int,比如
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/product_image" android:layout_width="100dp" android:layout_height="100dp" android:layout_marginStart="4dp" android:layout_marginTop="4dp" android:layout_marginBottom="4dp" android:scaleType="centerCrop" app:cornerRadiusSize="@{8f}" app:cornerRadiusType="@{0}" app:imageUrl="@{product.imageUrl}" />
这样就不便于理解, 所以必须使用提前定义好的value, 但是这样Android Studio又不会自动提示
2.使用自定义View
可能有些同学在项目中并没有使用DataBinding,那么这时候就需要自定义VIew
比如
class RoundCornerImageView(
context: Context,
attrs: AttributeSet
) : AppCompatImageView(context, attrs) {
@Px
private var radius: Float = 0f
private var radiusType: RadiusType = RadiusType.ALL
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.RoundCornerView)
radius = ta.getDimension(R.styleable.RoundCornerView_cornerRadiusSize, 0f)
val index = ta.getInt(R.styleable.RoundCornerView_cornerRadiusType, 0)
radiusType = sRadiusType[index]
setRadiusType(radiusType)
ta.recycle()
}
fun setRadiusType(radiusType: RadiusType) {
this.radiusType = radiusType
outlineProvider = getRoundCornerOutlineWithPadding(radius, radiusType)
clipToOutline = true
}
fun setRadius(@Px radius: Float) {
this.radius = radius
outlineProvider = getRoundCornerOutline(radius, radiusType)
clipToOutline = true
}
}
使用
<io.kailuzhang.github.rcview.RoundCornerImageView
android:id="@+id/product_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:scaleType="centerCrop"
app:imageUrl="@{product.imageUrl}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:cornerRadiusSize="4dp"
app:cornerRadiusType="top"
tools:background="#aaa" />
优点:
- 无需使用DataBinding
- 写法与通常写的xml相同
缺点:
- 没需要给一个View圆角都要自定义一个View, 不够灵活
3. kotlin代码实现
代码实现上方已经写过了, 代码实现优点就是也是很灵活的, 缺点就是还是要在代码中写的
原理
上方那么多全都是基于ViewOutlineProvider来实现的, 关于该圆角切割的原理我还是不是很清楚, 看了源码, 最后调用的是方法
@CriticalNative
private static native boolean nSetOutlineRoundRect(long renderNode, int left, int top,
int right, int bottom, float radius, float alpha);
这里是调用的native层的方法, 具体原理也不得而知, 希望有大神可以告知
总结
Android其实还有更多好用便捷的API, 我们还没有发现, 所以我们在学习技术的时候 更多的去看谷歌官方提供的API与文档. 还有看Android官方Demo真的能学到很多.