AndroidでViewを角丸にする4つの方法

タピオカ

こんにちは。タピオカです♪
初めての記事よろしくお願いします。
今回は、AndroidでViewを角丸にする方法についてご紹介します。

概要

AndroidでViewを角丸にすることは複雑な問題ではないですが、実装方法が様々な方法があります。
今回はよく使われる4つの方法について、それぞれの実装方法とメリット・デメリットをご紹介したいと思います。

1.背景を指定して角丸にする方法

XMLで実装する場合

Viewを角丸にするために <shape> を使うのが最もシンプルな方法であり、以下2つのステップで実装できます。

1.res/drawable に shape.xml を作る

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/black" />
    <corners android:radius="16dp" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/black" />
    <corners android:radius="16dp" />
</shape>

2.Viewの背景をshape.xmlに指定する

<View
    android:layout_height="48dp"
    android:layout_width="match_parent"
    android:background="@drawable/shape" />

topLeftRadiusbottomLeftRadiusなどを指定すると、角を個別に調整することができます。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/black" />
    <corners android:topLeftRadius="16dp"
        android:topRightRadius="8dp"
        android:bottomRightRadius="8dp" />
</shape>

shape.xmlの代わりに.9.pngを使うことで、複雑な形状を実装することができます。(e.g.チャットアプリの吹き出しなど)

コードで実装する場合

コードで実装する場合、GradientDrawableを利用し、以下2つのステップで実装できます。

1.layout.xmlでViewのidを指定する

<View android:id="@+id/view"
    android:layout_height="48dp"
    android:layout_width="match_parent" />
  1. Viewの背景にGradientDrawableを指定する
val view = findViewById<View>(R.id.view)
val shape = GradientDrawable()
shape.color = ColorStateList.valueOf(getColor(android.R.color.black))
shape.cornerRadius = resources.displayMetrics.density * 16 // 16dp
view.background = shape

Tips. backgroundTintを指定すると、shapeの色を変更できる

<View ... android:backgroundTint="@android:color/white" />

背景指定のメリットとデメリット

  • メリット
    • 実装が非常に簡単なこと
    • 角を個別に調整することができること
  • デメリット
    • Viewのクロッピングがないので、その他のプロパティやサブViewに制約がないこと
      サブViewやsrcforegroundなどはshape.xmlの範囲を超えられます。
    • 複雑な仕様で再利用するのは難しいこと
      例えば、backgroundTintにて色を指定すると全ての<shape>が指定した色になるため、それぞれの<shape>に色を変更したい場合は、指定したい色ごとにshape.xmlを作成する必要があります。また、角丸の形状をそれぞれ指定したい場合も、別々にshape.xmlを作成する必要があります。

2.ViewOutlineProviderを使って角丸にする方法

ViewOutlineProviderはViewをクリップする方法で、以下2つのステップで実装できます。
1.layout.xmlでViewのidを指定する

<View android:id="@+id/view"
    android:layout_height="48dp"
    android:layout_width="match_parent"
    android:background="@android:color/black" />

2.コードでViewOutlineProviderを設定する

view.clipToOutline = true
view.outlineProvider = object : ViewOutlineProvider() {
    override fun getOutline(v: View, outline: Outline) {
        outline.setRoundRect(0, 0, v.width, v.height, 16 * v.resources.displayMetrics.density)
    }
}

ViewOutlineProviderを使うメリットとデメリット

  • メリット
    • outlineProviderを指定して、clipToOutlinetrueに設定すると、サブViewやsrcforegroundbackgroundなどはOutlineの範囲を超えられること
  • デメリット
    • <shape>topLeftRadiusプロパティのような個別に角丸のサイズを設定できないこと
    • xmlで直接使用できないこと

3.CardViewを使って角丸にする方法

CardViewはGoogle公式のカスタムViewで、以下2つのステップで実装できます。
1.build.gradleCardViewを導入する

dependencies {
    implementation "androidx.cardview:cardview:1.0.0"
}

2.layout.xmlCardViewを使う

<androidx.cardview.widget.CardView
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    app:cardCornerRadius="16dp"
    app:cardElevation="0dp"
    app:cardBackgroundColor="@android:color/black">

    <View
        android:layout_width="match_parent"
        android:layout_height="48dp" />

</androidx.cardview.widget.CardView>

CardViewを使うメリットとデメリット

  • メリット
    • CardViewViewOutlineProviderに似ているが、サブViewやforegroundなどがCardViewの範囲を超えられないこと
  • デメリット
    • 追加のライブラリをインポートする必要があること
    • 角丸のサイズを個別に設定できないこと
    • 本質はFrameLayoutであるので、FrameLayoutが必要ない場合はレイアウト階層が増加すること

4.カスタムViewを作って角丸にする方法

カスタムViewの実現は以下2つのステップで実装できます。
1.カスタムViewを定義する

/** カスタムViewの例の一つは下の通りに */
class CustomLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    FrameLayout(context, attrs, defStyleAttr) {

    override fun dispatchDraw(canvas: Canvas) = with(context) {
        val radius = 16f * v.resources.displayMetrics.density
        val rect = RectF(0f, 0f, width.toFloat(), height.toFloat())
        val path = Path().apply { addRoundRect(rect, radius, radius, Path.Direction.CW) }
        canvas.clipPath(path)
        super.dispatchDraw(canvas)
    }

}

2.layout.xmlでカスタムViewを使う

<[パッケージ名].CustomLayout
    android:layout_height="wrap_content"
    android:layout_width="match_parent">
    <View
        android:layout_height="48dp"
        android:layout_width="match_parent"
        android:background="@android:color/black" />
</[パッケージ名].CustomLayout>

カスタムViewを使うメリットとデメリット

  • メリット
    • 仕様に合わせて角丸のサイズだけでなく、様々な形を設定することができること
  • デメリット
    • 実装が非常に複雑であること

Extra. databindingを活用して、より複雑な仕様に対応する方法

コードで実装する場合

より複雑な仕様に対応する場合、上記でご紹介した各方法では簡潔さに欠けてしまいます。
そこで、databindingを活用すると3つのステップでシンプルに実装できます。
※例えば、ボーダーを描画し、クリック効果があるボタンを角丸にしたい場合等

1.まず、databindingを導入する

apply plugin: "kotlin-kapt"

android {
    buildFeatures.dataBinding = true
}

2.ViewOutlineProviderを改造する
ViewOutlineProviderはxmlで直接使用できないとご紹介しましたが、このデメリットはdatabindingである程度補うことができます。
databindingのBindingAdapterアノテーションを利用することで、xmlで使用できる属性を定義できます。

@JvmStatic
@BindingAdapter("corner")
fun View.corner(radiusPixel: Float) {
    clipToOutline = true
    outlineProvider = ViewOutlineProvider { setRoundRect(0, 0, it.width, it.height, radiusPixel) }
}

呼び出し方法も簡単で、app:corner="@{@dimen/corner_size}"のように"@{...}"でxmlに定義されたリソースを呼び出すと、databindingは自動的@dimen/corner_sizeが表すdpの値をFloatのpxの値に変換することができます。

<Button
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:foreground="?selectableItemBackground"
    app:corner="@{@dimen/corner_size}" />

※直接dp値を指定する場合
"@{}"16dp16spのような値を受け取れないので、直接dp値を入力したい場合、数字をdp値として入力し、コードの中でpixelに変換する

@JvmStatic
@BindingAdapter("corner")
fun View.corner(radiusDp: Float) {
    clipToOutline = true
    // dpをpixelに変換する
    val radiusPixel = radiusDp * v.resources.displayMetrics.density
    outlineProvider = ViewOutlineProvider { setRoundRect(0, 0, it.width, it.height, radiusPixel) }
}
<Button
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:foreground="?selectableItemBackground"
    app:corner="@{16f}" /> <!--ここで、16dp/16spのような値を受け取れない->

3.ボーダーを描画する機能を追加する

@JvmStatic
@BindingAdapter("corner", "borderColor", "borderSize")
fun View.corner(radius: Float, stroke: ColorStateList, size: Float) {
    clipToOutline = true
    outlineProvider = object : ViewOutlineProvider() {
        override fun getOutline(v: View, outline: Outline) {
            outline.setRoundRect(0, 0, v.width, v.height, radius * v.resources.displayMetrics.density)
        }
    }
    post {
        background = GradientDrawable().apply {
            color = backgroundTintList
            setStroke((size * resources.displayMetrics.density).roundToInt(), stroke)
            cornerRadius = radius * resources.displayMetrics.density
        }
        backgroundTintList = null
    }
}

<Button
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:foreground="?selectableItemBackground"
    app:corner="@{16f}"
    app:borderSize="@{2f}"
    app:borderColor="@{@color/border_color}"
    android:backgroundTint="@color/background_color" />

※Styleをまとめたい場合
style.xmlで設定できないので、まとめたい場合は新しいBindingAdapterメソットを作ります。

@JvmStatic
@BindingAdapter("corner", "borderColor", "borderSize")
fun View.corner(radius: Float, stroke: ColorStateList, size: Float) {
		// ...
}

@JvmStatic
@BindingAdapter("style1")
fun View.style1(radius: Float) {
    backgroundTintList = ColorStateList.valueOf(context.getColor(R.color.background_color))
    corner(radius, ColorStateList.valueOf(context.getColor(R.color.border_color)), 1f)
}

<Button
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:layout_marginTop="4dp"
    android:foreground="?actionBarItemBackground"
    app:style1="@{16f}" />

databindingを使うメリットとデメリット

  • メリット
    • 複雑な要件を処理する際に、コード実装の柔軟性とxmlレイアウトの簡潔性があるため、テンプレートコードを減らせること
  • デメリット
    • 入力値に関して、カスタムViewのカスタム属性に比べて自由度が低いこと
      例えば"@{...}"では16dp16spのような値を受け取れないことや、BindingAdapterで拡張された属性はstyle.xmlで設定できない等

まとめ

Viewを角丸にする方法はたくさんありますが、完璧な方法はないので、状況に応じて最適な方法を選択する必要があります。

カテゴリー: スマホアプリ

タピオカ