圓角升級啦,來手把手一起實現自定義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,這一期就此完結。

「其他文章」