115
97

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Goodbye `shape` - AndroidのMaterialButtonがすごい良かった話

Last updated at Posted at 2018-09-28

概要

Material Design Theming対応を各アプリで進めるのは、AndroidX対応と依存して難しいところはありますが、そもそも対応するとどういいのかみたいな情報があまりなかったので、同じことをするにしても新しいのだとどうよくなるのか?みたいな話を書きたいと思ってます。

Material Deisgnのライブラリで、MaterialButtonとか他にもStyleが入ってきて、今まで作ってきていたオレオレのshape drawable(XMLで作っていたDrawable系のファイルを纏めて指している)を作らなくてもだいたいの事ができるようになったよねという話です。

Material Themingとは?

MaterialDesignを、実際のアプリに合わせてカスタマイズできるように少しやわらかくしたもの。

MaterialThemingを実装した各プラットフォームごとの実装などをも提供されている

公式サイト:https://material.io/design/material-theming/

MaterialButton

MaterialDesign Guidline[^1]のボタンを実装したビュークラス。

material-designのライブラリの一部として提供されている。

実装は、AppCompatButtonをベースにしているが、ガイドラインの基本的な実装が入っておりかなり高機能。

Document: https://material.io/develop/android/components/material-button/

MaterialButton デフォルトで実装できるもの

material-buttons.png

よく使うスタイル

ボタン スタイル
角丸ボタン @style/Widget.MaterialComponents.Button
アイコンボタン @style/Widget.MaterialComponents.Button.Icon
テキストボタン @style/Widget.MaterialComponents.Button.TextButton
外枠ボタン @style/Widget.MaterialComponents.Button.OutlinedButton

MaterialButtonのよく使うプロパティ

カテゴリ property 役割
大きさ android:inset タッチ領域内での大きさを変えられる。デフォルトで、topとbottomに4dpついている。
アイコン app:icon アイコンを表示させられる。drawableLeftの上位互換。
app:iconTint アイコンの色を変えられる。modeも別で指定できる。
app:iconGravity アイコンの位置をテキストに合わせるか、全体に合わせるか決められる。
app:iconPadding アイコンとの距離を変えられる。
背景 app:backgroundTint 背景の色を変えられる。modeも別で指定できる。
app:cornerRadius ボタンの角のRadiusを変えられる。
枠線 app:strokeColor 線の色を変えられる。
app:strokeWidth 太さを変えられる。
アニメーション app:rippleColor Rippleの色を変えられる。
android:stateListAnimator 押した時など、ステートごとのアニメーションを変えられる。

ビューのスペースの話

  • margin : Viewの領域外とのスペース
  • padding : View内のコンテンとのスペース(Buttonだと、TextなどコンテンツはViewによる)
  • inset : Viewの領域とDrawableとのスペース※影は含まない

insetプロパティも、全部にきくわけじゃない?自作のdrawableなどでもInsetでないと効かないかもしれない :thinking:

padding-inset-margin.png

まとめ

  • MaterialButtonすごい
  • だいたいはプロパティとスタイルで実装できる
  • アニメーションとかもいい感じに付くので、できるだけスタイルとプロパティを使う

ところで、これどうやるの。。。?

mio-design%2Fassets%2F0B6xUSjjSulxcN21PWXZ6VHZtMFk%2Fshapingmaterial-hero-1.png

実例ShrineShape : https://material.io/design/material-studies/shrine.html#shape

Welcome new shape :handshake:

MaterialComponentのShapeの状況

スクリーンショット 2018-09-28 11.22.30.png

RoadMap : https://github.com/material-components/material-components/blob/develop/ROADMAP.md

Experimentalでも使えるShape

BottomAppBarではすでに使われてる。使い方も、BottomAppBarを見た。

  • MaterialShapeDrawable : 画像情報。色とかMaterialDesignっぽいものを指定できる。
  • ShapePathModel : パス情報だけ管理してるモデル
  • (Treatment)
    • CornerTreatment : 角のパス整形ができる。
    • EdgeTreatment : 辺のパス整形ができる。

Document : https://github.com/material-components/material-components-android/blob/8f7dc21a27880eed391f45548a40de8189f31172/docs/theming/Shape.md

使ってみた

diamond_cut.png

矢印ボタンの実際のコード

val shapePathModel = ShapePathModel().apply {
    // CutCornerTreatimentと、RoundedCornerTreatmentが用意されている
    val cornerTreatment = CutCornerTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size))
    topRightCorner = cornerTreatment
    bottomRightCorner = cornerTreatment
    // TriangleEdgeTreatmentが用意されていて、三角に切り抜ける。
    leftEdge = TriangleEdgeTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size), true)
}
val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
    // MaterialButtonと同じ様な設定ができる
    setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
}
// toRippleはExtension。インタラクションつけるのはかなり大変。
sharp_cut_button.background = shapeDrawable.toRipple()

問題は色々ある

  • BackgroundのDrawableをコードで指定する必要がある
  • コードで指定すると、InsetとかTintとか、steteList~~とか色々無視される
  • 自作のCornerTreatmentとEdgeTreatmentを作るのは難しい

Comming Soonなもの

Styleで全体で、Cutにするかどうかなどを指定できるようになる?

MaterialDesignの話で、こういうShapeの計上などはアプリのブランドで一貫性をもたせないとダメだという話もあったので、Styleで設定できるのが自然そうです。

<style name="Theme.MyApp" parent="Theme.MaterialComponents.Light">
  ...
  <item name="cornerRadiusPrimary">8dp</item>
  <item name="cornerStylePrimary">cut</item>
  <item name="cornerRadiusSecondary">4dp</item>
  <item name="cornerStyleSecondary">cut</item>
  ...
</style>

Document : https://github.com/material-components/material-components-android/blob/8f7dc21a27880eed391f45548a40de8189f31172/docs/theming/Shape.md

まとめ

  • ShapeのStyleでのサポートが待ち遠しい
  • どうしてもカットしたかったら、MaterialShapeDrawableを使う
  • アニメーションなど色々と難しいのでボタンで使用するなら、 自分でパス書き出してVectorDrawableの方が楽

おまけ

サンプルコードとか

GitHub:https://github.com/Reyurnible/android-shape-sample

MaterialShapeDrawableの例

private fun setSharpCutButton() {
    val shapePathModel = ShapePathModel().apply {
        topLeftCorner = CutCornerTreatment(resources.getDimension(R.dimen.btn_cut_corner_size))
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    sharp_cut_button.background = shapeDrawable.toRipple()
}

private fun setEdgeButton() {
    val shapePathModel = ShapePathModel().apply {
        setAllEdges(TriangleEdgeTreatment(resources.getDimension(R.dimen.btn_cut_edge_size), true))
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    edge_button.background = shapeDrawable.toRipple()
}

private fun setAllowCutButton() {
    val shapePathModel = ShapePathModel().apply {
        val cornerTreatment = CutCornerTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size))
        topRightCorner = cornerTreatment
        bottomRightCorner = cornerTreatment
        leftEdge = TriangleEdgeTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size), true)
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    allow_cut_button.background =shapeDrawable.toRipple()
}

private fun setRoundedCornerButton() {
    val shapePathModel = ShapePathModel().apply {
        val cornerTreatment = RoundedCornerTreatment(resources.getDimension(R.dimen.btn_half_cut_corner_size))
        topLeftCorner = cornerTreatment
        topRightCorner = cornerTreatment
    }
    val shapeDrawable = MaterialShapeDrawable(shapePathModel).apply {
        setTint(ContextCompat.getColor(this@MainActivity, R.color.colorAccent))
    }
    rounded_corner_button.background = shapeDrawable.toRipple()
}

private fun Drawable.toRipple(): RippleDrawable =
        RippleDrawable(
                ContextCompat.getColorStateList(this@MainActivity, R.color.mtrl_btn_ripple_color),
                this,
                null
        )

Buttonのよく使う自作スタイル:

<!-- Buttons -->
<style name="Widget.AppTheme.Button" parent="Widget.MaterialComponents.Button" />

<style name="Widget.AppTheme.Button.Rectangle">
    <item name="android:insetLeft">0dp</item>
    <item name="android:insetRight">0dp</item>
    <item name="android:insetTop">0dp</item>
    <item name="android:insetBottom">0dp</item>
    <item name="cornerRadius">0dp</item>
</style>

<style name="Widget.AppTheme.Button.Rectangle.Rounded">
    <item name="cornerRadius">4dp</item>
</style>

<style name="Widget.AppTheme.Button.Rounded">
    <item name="android:insetLeft">@dimen/button_horizontal_inset</item>
    <item name="android:insetRight">@dimen/button_horizontal_inset</item>
    <item name="android:insetTop">@dimen/button_vertical_inset</item>
    <item name="android:insetBottom">@dimen/button_vertical_inset</item>
    <item name="android:minHeight">@dimen/view_size_normal</item>
    <item name="android:textStyle">bold</item>
    <item name="android:paddingStart">@dimen/button_rounded_horizontal_padding</item>
    <item name="android:paddingEnd">@dimen/button_rounded_horizontal_padding</item>
    <item name="cornerRadius">@dimen/button_rounded_radius</item>
</style>

stateListAnimatorの例:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Pressed viewAction -->
    <item
        android:state_enabled="true"
        android:state_pressed="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:valueTo="2dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Hover viewAction. This is triggered via mouse. -->
    <item
        android:state_enabled="true"
        android:state_hovered="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:valueTo="2dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Focused viewAction. This is triggered via keyboard. -->
    <item
        android:state_enabled="true"
        android:state_focused="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:valueTo="2dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Base viewAction (enabled, not pressed) -->
    <item android:state_enabled="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:propertyName="translationZ"
                android:startDelay="100"
                android:valueTo="0dp"
                android:valueType="floatType"
                tools:ignore="UnusedAttribute" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>

    <!-- Disabled viewAction -->
    <item>
        <set>
            <objectAnimator
                android:duration="0"
                android:propertyName="translationZ"
                android:valueTo="0dp"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="0"
                android:propertyName="elevation"
                android:valueTo="0dp"
                android:valueType="floatType" />
        </set>
    </item>

</selector>
115
97
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
115
97

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?