圓角升級啦,來手把手一起實現自定義ViewGroup的各種圓角與背景

語言: CN / TW / HK

theme: smartblue highlight: agate


我報名參加金石計劃1期挑戰——瓜分10萬獎池,這是我的第3篇文章,點擊查看活動詳情

定製圓角與背景的自定義ViewGroup實現

前言

目前線上的一些第三方圓角容器大部分都只支持四周固定圓角,我們一些使用場景只需要頂部圓角,或者底部圓角,或者一個角圓角。

(話説為什麼我們的UI這麼喜歡各種奇葩圓角,想哭。。。)

對於這些定製化的圓角需求,我們如何自定義實現呢?又有哪些實現方式呢?

之前我們講過圓角圖片的自定義,那麼和我們的自定義圓角容器又有哪些區別呢?

帶着這些問題,我們一步一步往下看。

技術選型

可能有同學問了,用shape畫一個圓角不就行了嗎?各種的圓角都能畫,實在不行還可以找UI要各種圓角的切圖。有必要用自定義ViewGroup來實現嗎?

確實在一部分場景中我們可以通過這樣設置圓角背景的方式來解決問題,一般設計都有內間距,我們設置了背景,然後再通過設置間距來確保內部的控件不會和父容器交叉重疊。

因為這樣設置的背景只是欺騙了視覺,並沒有裁剪控件,如果在特定的場景下,如挨着角落佈局,或者滾動起來的時候,就會發現內部的控件'超過'了父容器的範圍。

一句話説不清楚,大家看下面這張圖應該能理解:

我使用自定義的 FrameLayout 設置異性圓角,並且設置異性圓角的圖片背景,然後內部添加一個子View,那麼子View就不會超過左上角的圓角範圍。

如果在這樣的特殊場景下,要達到這樣的效果,我們就需要自定義View的方式來裁剪父容器,讓它真正的就是那樣的形狀!

在之前我們實現的圓角ImageView的過程中,我們瞭解到裁剪View的幾種方式。

一共有 ClipPath Xfermode Shader 另外還有一種 Outline 的方式。

之前我們的圖片裁剪是利用 Shader 來實現的。現在我們裁剪ViewGroup我們最方便的方式是 Outline 但是我們需要對一些 Outline 實現不了的版本和效果,我們使用 Shader 做一些兼容處理即可。

需求整理

首先在動手之前我們理清一下思路,我們需要哪些功能,以及如何實現。

  1. 通過策略模式來管理不同版本的裁剪實現
  2. 通過一個接口來封裝邏輯管理這些策略
  3. 通過實現不同的自定義View來管理接口實現類間接的通過不同的策略來裁剪
  4. 使用自定義屬性來動態的配置需要的屬性
  5. 裁剪完成之後需要接管系統的背景的繪製,由自己實現
  6. 使用Shader的方式繪製背景
  7. 對原生背景屬性的兼容處理

説明:

根據不同的版本和需求,使用不同的策略來裁剪 ViewGroup,需要考慮到不同的圓角,統一的圓角和圓形的裁剪。

裁剪完成之後在部分方案中我們設置背景還是會覆蓋到已裁剪的區域,這時候我們統一處理背景的繪製。

由於系統 View 自帶背景的設置,和我們的背景繪製有衝突,我們需要接管系統的 View 的背景繪製,並且需要處理 Xml 中設置背景與 Java 代碼中設置背景的兼容性問題。

最後使用 Shader 的方式繪製各種形狀的背景繪製。需要注意處理不同的圓角,圓角和圓形的繪製方式。

整體框架的大致構建圖如下:

下面跟着我一步一步的來實現吧。

使用策略模式兼容裁剪的方式

其實市面上大部分的裁剪都是使用的 Outline 的方式,這是一種極好的方案。我也是使用這種方案,那我為什麼不直接使用第三方庫算了。。。 就是因為兼容性問題和一些功能性問題不能解決。

Outline可以繪製圓形和統一圓角,但是它無法設置異形的圓角。並且它只能在5.0以上的系統才能使用。所以我們需要對異形的圓角和低版本做兼容處理。

核心代碼如下: ```kotlin private fun init(view: View, context: Context, attributeSet: AttributeSet?, attrs: IntArray, attrIndexs: IntArray) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        //5.0版本以上與5.0一下的兼容處理

        //判斷是否包含自定義圓角
        val typedArray = context.obtainStyledAttributes(attributeSet, attrs)
        val topLeft = typedArray.getDimensionPixelOffset(attrIndexs[2], 0).toFloat()
        val topRight = typedArray.getDimensionPixelOffset(attrIndexs[3], 0).toFloat()
        val bottomLeft = typedArray.getDimensionPixelOffset(attrIndexs[4], 0).toFloat()
        val bottomRight = typedArray.getDimensionPixelOffset(attrIndexs[5], 0).toFloat()
        typedArray.recycle()

        roundCirclePolicy = if (topLeft > 0 || topRight > 0 || bottomLeft > 0 || bottomRight > 0) {
            //自定義圓角使用兼容方案
            RoundCircleLayoutShaderPolicy(view, context, attributeSet, attrs, attrIndexs)
        } else {
            //使用OutLine裁剪方案
            RoundCircleLayoutOutlinePolicy(view, context, attributeSet, attrs, attrIndexs)
        }
    } else {
        // 5.0以下的版本使用兼容方案
        roundCirclePolicy = RoundCircleLayoutShaderPolicy(view, context, attributeSet, attrs, attrIndexs)
    }

}

```

我們需要對5.0一下的版本使用 clipPath 的方案裁剪,5.0以上的方案實現 Outline的方案裁剪。

Outline的裁剪:

```kotlin

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun beforeDispatchDraw(canvas: Canvas?) {
    //5.0版本以上,採用ViewOutlineProvider來裁剪view
    mContainer.clipToOutline = true
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun afterDispatchDraw(canvas: Canvas?) {
    //5.0版本以上,採用ViewOutlineProvider來裁剪view
    mContainer.outlineProvider = object : ViewOutlineProvider() {
        override fun getOutline(view: View, outline: Outline) {

            if (isCircleType) {
                //如果是圓形裁剪圓形
                val bounds = Rect()
                calculateBounds().roundOut(bounds)
                outline.setRoundRect(bounds, bounds.width() / 2.0f)

// outline.setOval(0, 0, mContainer.width, mContainer.height); //兩種方法都可以

            } else {
                //如果是圓角-裁剪圓角
                if (mTopLeft > 0 || mTopRight > 0 || mBottomLeft > 0 || mBottomRight > 0) {
                    //如果是單獨的圓角
                    val path = Path()
                    path.addRoundRect(
                        calculateBounds(),
                        floatArrayOf(mTopLeft, mTopLeft, mTopRight, mTopRight, mBottomRight, mBottomRight, mBottomLeft, mBottomLeft),
                        Path.Direction.CCW
                    )

                    //不支持2階的曲線
                    outline.setConvexPath(path)

                } else {
                    //如果是統一圓角
                    outline.setRoundRect(0, 0, mContainer.width, mContainer.height, mRoundRadius)
                }

            }
        }
    }
}

```

clipPath 方案的核心代碼 ``` kotlin override fun beforeDispatchDraw(canvas: Canvas?) { canvas?.clipPath(mPath) }

override fun afterDispatchDraw(canvas: Canvas?) {
}

//裁剪的路徑
private fun setupRoundPath() {
    mPath.reset()

    if (isCircleType) {

        mPath.addOval(0f, 0f, mContainer.width.toFloat(), mContainer.height.toFloat(), Path.Direction.CCW)

    } else {

        //如果是圓角-裁剪圓角
        if (mTopLeft > 0 || mTopRight > 0 || mBottomLeft > 0 || mBottomRight > 0) {

            mPath.addRoundRect(
                calculateBounds(),
                floatArrayOf(mTopLeft, mTopLeft, mTopRight, mTopRight, mBottomRight, mBottomRight, mBottomLeft, mBottomLeft),
                Path.Direction.CCW
            )

        } else {

            mPath.addRoundRect(mDrawableRect, mRoundRadius, mRoundRadius, Path.Direction.CCW)
        }

    }

}

```

其中調用的時機我們通過策略的接口,定義的一系列的鈎子函數。 ``` kotlin // 策略的接口定義 interface IRoundCirclePolicy {

fun beforeDispatchDraw(canvas: Canvas?)

fun afterDispatchDraw(canvas: Canvas?)

fun onDraw(canvas: Canvas?): Boolean

fun onLayout(left: Int, top: Int, right: Int, bottom: Int)

} ```

RoundCircleViewImpl:

``` kotlin fun beforeDispatchDraw(canvas: Canvas?) { roundCirclePolicy.beforeDispatchDraw(canvas) }

fun afterDispatchDraw(canvas: Canvas?) {
    roundCirclePolicy.afterDispatchDraw(canvas)
}

fun onDraw(canvas: Canvas?): Boolean {
    return roundCirclePolicy.onDraw(canvas)
}

```

最終在具體的ViewGroup中實現。 ``` kotlin override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) roundCircleViewImpl.onLayout(changed, left, top, right, bottom) }

override fun dispatchDraw(canvas: Canvas?) {
    roundCircleViewImpl.beforeDispatchDraw(canvas)
    super.dispatchDraw(canvas)
    roundCircleViewImpl.afterDispatchDraw(canvas)
}

override fun onDraw(canvas: Canvas?) {
    if (roundCircleViewImpl.onDraw(canvas)) {
        super.onDraw(canvas)
    }
}

```

在繪製,繪製前,繪製後我們都有對應的攔截和實現。通過上面的裁剪核心代碼我們就能實現不同功能不同版本的具體策略實現。

到此我們就能裁剪ViewGroup完成,並且能裁剪到指定的形狀。

抽取自定義屬性配置

這裏我們把常用的自定義屬性抽取出來,然後再我們抽象的策略類中拿到對應的屬性,取出設置的一些值,然後再具體的策略實現類中就可以操作使用了。

自定義屬性定義如下: xml <attr name="is_circle" format="boolean" /> <attr name="round_radius" format="dimension" /> <attr name="topLeft" format="dimension" /> <attr name="topRight" format="dimension" /> <attr name="bottomLeft" format="dimension" /> <attr name="bottomRight" format="dimension" /> <attr name="round_circle_background_color" format="color" /> <attr name="round_circle_background_drawable" format="reference" /> <attr name="is_bg_center_crop" format="boolean" /> <declare-styleable name="RoundCircleConstraintLayout"> <attr name="is_circle" /> <attr name="round_radius" /> <attr name="topLeft" /> <attr name="topRight" /> <attr name="bottomLeft" /> <attr name="bottomRight" /> <attr name="round_circle_background_color" /> <attr name="round_circle_background_drawable" /> <attr name="is_bg_center_crop" /> </declare-styleable> ...

在具體的ViewGroup中我們把屬性封裝到對象中,最終傳遞給策略類去取出來實現 ```kotlin private fun init(view: View, context: Context, attributeSet: AttributeSet?) { roundCircleViewImpl = RoundCircleViewImpl( view, context, attributeSet, R.styleable.RoundCircleNestedScrollView, intArrayOf( R.styleable.RoundCircleNestedScrollView_is_circle, R.styleable.RoundCircleNestedScrollView_round_radius, R.styleable.RoundCircleNestedScrollView_topLeft, R.styleable.RoundCircleNestedScrollView_topRight, R.styleable.RoundCircleNestedScrollView_bottomLeft, R.styleable.RoundCircleNestedScrollView_bottomRight, R.styleable.RoundCircleNestedScrollView_round_circle_background_color, R.styleable.RoundCircleNestedScrollView_round_circle_background_drawable, R.styleable.RoundCircleNestedScrollView_is_bg_center_crop, )

    )
    nativeBgDrawable?.let {
        roundCircleViewImpl.setNativeDrawable(it)
    }
}

```

這裏實現了 roundCircleViewImpl 對象, roundCircleViewImpl 對象內部又持有策略的對象,我們就可以在策略類中拿到屬性。

```kotlin internal abstract class AbsRoundCirclePolicy( view: View, context: Context, attributeSet: AttributeSet?, attrs: IntArray, attrIndex: IntArray ) : IRoundCirclePolicy {

var isCircleType = false
var mRoundRadius = 0f
var mTopLeft = 0f
var mTopRight = 0f
var mBottomLeft = 0f
var mBottomRight = 0f
var mRoundBackgroundDrawable: Drawable? = null
var mRoundBackgroundBitmap: Bitmap? = null
var isBGCenterCrop = true;

val mContainer: View = view

init {
    initialize(context, attributeSet, attrs, attrIndex)
}

private fun initialize(context: Context, attributeSet: AttributeSet?, attrs: IntArray, attrIndexs: IntArray) {
    val typedArray = context.obtainStyledAttributes(attributeSet, attrs)

    isCircleType = typedArray.getBoolean(attrIndexs[0], false)

    mRoundRadius = typedArray.getDimensionPixelOffset(attrIndexs[1], 0).toFloat()

    mTopLeft = typedArray.getDimensionPixelOffset(attrIndexs[2], 0).toFloat()
    mTopRight = typedArray.getDimensionPixelOffset(attrIndexs[3], 0).toFloat()
    mBottomLeft = typedArray.getDimensionPixelOffset(attrIndexs[4], 0).toFloat()
    mBottomRight = typedArray.getDimensionPixelOffset(attrIndexs[5], 0).toFloat()

    val roundBackgroundColor = typedArray.getColor(attrIndexs[6], Color.TRANSPARENT)
    mRoundBackgroundDrawable = ColorDrawable(roundBackgroundColor)
    mRoundBackgroundBitmap = getBitmapFromDrawable(mRoundBackgroundDrawable)

    if (typedArray.hasValue(attrIndexs[7])) {
        mRoundBackgroundDrawable = typedArray.getDrawable(attrIndexs[7])
        mRoundBackgroundBitmap = getBitmapFromDrawable(mRoundBackgroundDrawable)
    }

    isBGCenterCrop = typedArray.getBoolean(attrIndexs[8], true)

    typedArray.recycle()
}

```

抽象的策略類拿到了屬性值之後,在具體的策略裁剪類中我們就可以使用這些定義的屬性了。

圓角背景的處理

我們在自定義屬性中設置了背景的屬性,顏色和圖片的背景,此時我們需要拿到這些Bitmap去繪製出來。

繪製的代碼我們之前在RoundImageView中有詳細的講過,通過BitmapShader的方式繪製。

```kotlin private fun initViewData() { mContainer.setWillNotDraw(false)

    mDrawableRect = RectF()
    mPath = Path()
    mBitmapPaint = Paint()
    mShaderMatrix = Matrix()

}

//設置畫筆和BitmapShader等
private fun setupBG() {

    mDrawableRect.set(calculateBounds())

    if (mRoundBackgroundDrawable != null && mRoundBackgroundBitmap != null) {

        mBitmapWidth = mRoundBackgroundBitmap!!.width
        mBitmapHeight = mRoundBackgroundBitmap!!.height

        mBitmapShader = BitmapShader(mRoundBackgroundBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)

        if (mRoundBackgroundBitmap!!.width != 2) {
            updateShaderMatrix()
        }

        mBitmapPaint.isAntiAlias = true
        mBitmapPaint.shader = mBitmapShader

    }

}

```

需要注意的是ViewGroup默認是不走 onDraw 方法的,我們通過 setWillNotDraw(false) 的方法,允許ViewGroup能繪製。

然後我們在onDraw的鈎子函數中使用Canves來繪製

```kotlin override fun onDraw(canvas: Canvas?): Boolean {

    if (isCircleType) {

        canvas?.drawCircle(
            mDrawableRect.centerX(), mDrawableRect.centerY(),
            Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f), mBitmapPaint
        )

    } else {
        if (mTopLeft > 0 || mTopRight > 0 || mBottomLeft > 0 || mBottomRight > 0) {
            //使用單獨的圓角

            val path = Path()
            path.addRoundRect(
                mDrawableRect, floatArrayOf(mTopLeft, mTopLeft, mTopRight, mTopRight, mBottomRight, mBottomRight, mBottomLeft, mBottomLeft),
                Path.Direction.CW
            )
            canvas?.drawPath(path, mBitmapPaint)

        } else {
            //使用統一的圓角
            canvas?.drawRoundRect(mDrawableRect, mRoundRadius, mRoundRadius, mBitmapPaint)

        }
    }

    //是否需要super再繪製
    return true
}

```

這裏需要注意的是,在我們設置 BitmapShader 的 Matrix 時候,我們需要設置縮放,這時候設置的圖片背景是從左上角開始的,並沒有居中。

所以我們需要自定義屬性來配置,是否需要背景圖片居中展示,默認讓背景圖片居中顯示,核心代碼如下:

```kotlin private fun updateShaderMatrix() { var scale = 1.0f var dx = 0f var dy = 0f

    mShaderMatrix.set(null)

    if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
        scale = mDrawableRect.height() / mBitmapHeight.toFloat()
        dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f
    } else {
        scale = mDrawableRect.width() / mBitmapWidth.toFloat()
        dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f
    }

    mShaderMatrix.setScale(scale, scale)

    if (isBGCenterCrop) {
        mShaderMatrix.postTranslate((dx + 0.5f).toInt() + mDrawableRect.left, (dy + 0.5f).toInt() + mDrawableRect.top)
    }

    mBitmapShader?.let {
        it.setLocalMatrix(mShaderMatrix)
    }
}

```

可以看一下裁剪控件和繪製背景之後的效果圖:

這些效果都是ViewGroup,不是ImageView加載的,其中圖二是故意設置為背景不居中展示的效果。在圖一中我們內部添加子View就可以看到裁剪的效果與背景的效果。

原生背景屬性的處理

雖然我們簡單的實現了控件的裁剪和背景的繪製,但是我們的健壯性還不夠,當我們再xml裏面設置background的時候,而不使用自定義屬性,就會沒效果。

我們需要接管系統View的setBackground的一些方法,讓它走到我們自定義的背景繪製中來。

例如:

```xml

<RoundCircleConstraintLayout
    android:id="@+id/layout_2"
    android:layout_width="@dimen/d_150dp"
    android:layout_height="@dimen/d_150dp"
    android:layout_marginTop="@dimen/d_10dp"
    app:is_circle="true"
    app:round_circle_background_color="#ff00ff"
    app:round_radius="@dimen/d_40dp"/>

```

我們直接設置 android:background 的時候我們需要重寫這些方法,然後取到其中的值,然後再交給策略類去具體的繪製。

核心代碼如下: ```xml private fun init(view: View, context: Context, attributeSet: AttributeSet?) { roundCircleViewImpl = RoundCircleViewImpl( view, context, attributeSet, R.styleable.RoundCircleFrameLayout, intArrayOf( R.styleable.RoundCircleFrameLayout_is_circle, R.styleable.RoundCircleFrameLayout_round_radius, R.styleable.RoundCircleFrameLayout_topLeft, R.styleable.RoundCircleFrameLayout_topRight, R.styleable.RoundCircleFrameLayout_bottomLeft, R.styleable.RoundCircleFrameLayout_bottomRight, R.styleable.RoundCircleFrameLayout_round_circle_background_color, R.styleable.RoundCircleFrameLayout_round_circle_background_drawable, R.styleable.RoundCircleFrameLayout_is_bg_center_crop, )

    )
    nativeBgDrawable?.let {
        roundCircleViewImpl.setNativeDrawable(it)
    }
}

private var nativeBgDrawable: Drawable? = null
override fun setBackground(background: Drawable?) {
    if (!this::roundCircleViewImpl.isInitialized) {
        nativeBgDrawable = background
    } else {
        roundCircleViewImpl.setBackground(background)
    }
}

override fun setBackgroundColor(color: Int) {
    if (!this::roundCircleViewImpl.isInitialized) {
        nativeBgDrawable = ColorDrawable(color)
    } else {
        roundCircleViewImpl.setBackground(background)
    }
}

override fun setBackgroundResource(resid: Int) {
    if (!this::roundCircleViewImpl.isInitialized) {
        nativeBgDrawable = context.resources.getDrawable(resid)
    } else {
        roundCircleViewImpl.setBackground(background)
    }
}

override fun setBackgroundDrawable(background: Drawable?) {
    if (!this::roundCircleViewImpl.isInitialized) {
        nativeBgDrawable = background
    } else {
        roundCircleViewImpl.setBackground(background)
    }
}

```

我們對Java中設置的背景與xml中設置的背景單獨的處理。

``` kotlin internal abstract class AbsRoundCirclePolicy( view: View, context: Context, attributeSet: AttributeSet?, attrs: IntArray, attrIndex: IntArray ) : IRoundCirclePolicy {

var isCircleType = false
var mRoundRadius = 0f
var mTopLeft = 0f
var mTopRight = 0f
var mBottomLeft = 0f
var mBottomRight = 0f
var mRoundBackgroundDrawable: Drawable? = null
var mRoundBackgroundBitmap: Bitmap? = null
var isBGCenterCrop = true;

val mContainer: View = view


override fun setNativeDrawable(drawable: Drawable) {
    mRoundBackgroundDrawable = drawable
    mRoundBackgroundBitmap = getBitmapFromDrawable(mRoundBackgroundDrawable)
}

} ```

在xml中設置的背景最終會調用到策略的抽象類中,賦值給Bitmap,然後我們的策略具體實現類就會繪製出背景。

而Java中的手動設置背景則會走到我們策略接口定義的方法中 ```kotlin // 策略的接口定義 interface IRoundCirclePolicy {

fun isCustomRound(): Boolean

fun beforeDispatchDraw(canvas: Canvas?)

fun afterDispatchDraw(canvas: Canvas?)

fun onDraw(canvas: Canvas?): Boolean

fun onLayout(left: Int, top: Int, right: Int, bottom: Int)

fun setBackground(background: Drawable?)

fun setBackgroundColor(color: Int)

fun setBackgroundResource(resid: Int)

fun setBackgroundDrawable(background: Drawable?)

fun setNativeDrawable(drawable: Drawable)

} ```

而它的具體實現不是由抽象策略類實現,是交給策略的具體實現類去實現,因為需要及時的刷新,所以是具體實現類去實現這些方法。

核心代碼如下: ```kotlin //手動設置背景的設置 override fun setBackground(background: Drawable?) { setRoundBackgroundDrawable(background) }

override fun setBackgroundColor(color: Int) {
    val drawable = ColorDrawable(color)
    setRoundBackgroundDrawable(drawable)
}

override fun setBackgroundResource(resid: Int) {
    val drawable: Drawable = mContainer.context.resources.getDrawable(resid)
    setRoundBackgroundDrawable(drawable)
}

override fun setBackgroundDrawable(background: Drawable?) {
    setRoundBackgroundDrawable(background)
}

//重新設置Drawable
private fun setRoundBackgroundDrawable(drawable: Drawable?) {
    mRoundBackgroundDrawable = drawable
    mRoundBackgroundBitmap = getBitmapFromDrawable(mRoundBackgroundDrawable)

    setupBG()

    //重繪
    mContainer.invalidate()
}

```

也是同樣的賦值操作,只是多了手動刷新的功能。

到處xml中的背景設置 和 Java 中手動設置我們就都接管了過來自己繪製了。

試試吧!

xml裏面設置的背景可以正常顯示,那我們設置一個點擊事件,換一下圖片背景試試

kotlin findViewById<ViewGroup>(R.id.layout_2).click { it.background = drawable(R.drawable.chengxiao) }

注意由於我們接管了背景的繪製,這裏我們使用的是View原生的方法即可

第二張圖就換成了圖片背景,內部的子View也能正常的顯示,也是顯示在正常的位置,符合我們的要求。

到此基本上就完成了我們的自定義圓角ViewGroup了。但是對應一些列表與滾動的容器我們能不能做同樣的裁剪呢?

對RecyclerView和ScrollView的支持

除了一些常用的容器,我們還有列表的處理,在一些場景中我們常見一些圓角的列表,比如 RecyclerView、 ScrollView 等。

都是可以實現的,其實它們擴展起來非常的方便。我們只需要加上對應的自定義屬性,只需要修改獲取自定義屬性的方法,其他的方法都是一樣的。

例如:

```kotlin class RoundCircleScrollView : ScrollView, IRoundCircleView {

private lateinit var roundCircleViewImpl: RoundCircleViewImpl

constructor(context: Context) : this(context, null)

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    init(this, context, attrs)
}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    init(this, context, attrs)
}

private fun init(view: View, context: Context, attributeSet: AttributeSet?) {
    roundCircleViewImpl = RoundCircleViewImpl(
        view,
        context,
        attributeSet,
        R.styleable.RoundCircleScrollView,
        intArrayOf(
            R.styleable.RoundCircleScrollView_is_circle,
            R.styleable.RoundCircleScrollView_round_radius,
            R.styleable.RoundCircleScrollView_topLeft,
            R.styleable.RoundCircleScrollView_topRight,
            R.styleable.RoundCircleScrollView_bottomLeft,
            R.styleable.RoundCircleScrollView_bottomRight,
            R.styleable.RoundCircleScrollView_round_circle_background_color,
            R.styleable.RoundCircleScrollView_round_circle_background_drawable,
            R.styleable.RoundCircleScrollView_is_bg_center_crop,
        )

    )
    nativeBgDrawable?.let {
        roundCircleViewImpl.setNativeDrawable(it)
    }
}

 ...

} ```

使用起來也是和普通的容器是一樣樣的。

xml <RoundCircleNestedScrollView android:layout_width="match_parent" android:layout_height="wrap_content" app:round_radius="@dimen/d_30dp">

xml <RoundCircleRecyclerView android:id="@+id/recyclerView" app:topRight="@dimen/d_20dp" app:topLeft="@dimen/d_20dp" android:layout_width="match_parent" android:layout_height="wrap_content" />

換上Scrollview的具體效果:

RecyclerView是內部的Item滾動,效果相對更好:

RV帶上頭佈局與腳佈局一樣不影響圓角的裁剪

錄製GIF的時候好像錄製範圍有點問題,導致錄製出來的GIF的圓角有一點裁剪的感覺,其實真實效果和ViewGroup是一樣的效果。

如何使用? 其實如果大家使用Scrollview的話,最好是用普通的圓角容器包裹 RoundCircleScrollView ,這樣可以達到圓角固定的效果,或者使用shape設置背景也可以,大家可以靈活選擇。

RV由於是內部的Item滾動就可以完美的裁剪,可以實現一些特殊的圓角需求。

如果想擴展更多的ViewGroup,或者自己的自定義ViewGroup,可以直接擴展即可,定義對應的自定義屬性,封裝成對象給 RoundCircleViewImpl 即可。具體可以參考源碼。

到此我們就全部實現完畢了,哪裏要彎就彎哪裏,媽媽再也不用擔心圓角的實現了。

總結

使用Shape圓角的背景或圖片背景和使用自定義ViewGroup裁剪其實是兩種不同的思路,關鍵是看需求,是否需要貼邊的時候需要保持圓角效果。大家按需選擇即可。

關於自定義View的裁剪方案,其實上面説了有多種實現,我使用了兼容性和效果都相對比較好的兩種方案 Outline 和 Shader ,當然了,如果有更好的方案也歡迎大家一起討論。

使用自定義ViewGroup的方式,算是解決了我開發中的一些痛點,特別是RV的一些裁剪,在一些特定的場景下很好用,我就不需要對Item做一些特別的處理。

好了本文的全部代碼與Demo都已經開源。有興趣可以看這裏

如果想直接使用,我已經傳到 Maven 倉庫,大家直接依賴即可。 implementation "com.gitee.newki123456:round_circle_layout:1.0.0"

具體用法可以看 Maven 項目,源碼在這裏,也已經全部開源。

慣例,我如有講解不到位或錯漏的地方,希望同學們可以指出交流。

如果感覺本文對你有一點點的啟發,還望你能點贊支持一下,你的支持是我最大的動力。

Ok,這一期就此完結。

「其他文章」