Android

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


概要

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>