カスタムビューでボタンを作る

例えば以下のようなボタンを作りたいとします。

<img width="266" alt="custom_button_view.png" src="https://qiita-image-store.s3.amazonaws.com/0/94929/ffdd97e4-8341-1415-0f64-bdc78f339fbf.png">

デフォルトのボタンでもアイコンは付けられるみたいですが、複雑なレイアウトを作るのはできないみたいです。
なので、カスタムビューを使って実現します。

# レイアウト作成

まずはボタンにしたいカスタムビューのレイアウトファイルを作成します。

<img width="400" alt="custom_button_view_layout.png" src="https://qiita-image-store.s3.amazonaws.com/0/94929/99a4cbee-1d02-5369-7476-c6f79f7467a7.png">

ルートレイアウトはなんでもいいのですが、今回はConstraintLayoutを使用しました。

次にこのレイアウトをボタンのような外見にします。
手順は**ルートレイアウトのstyle属性にボタンのスタイルを設定**するだけです。

```xml
<!-- view_custom_buttom.xml -->

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    style="@style/Widget.AppCompat.Button">
    <!-- ↑ここ! -->
    
    ...

</android.support.constraint.ConstraintLayout>
```

これで、見た目はボタンそっくり、押したときのリップルもちゃんと表示されデフォルトと瓜二つになりました。

<img width="400" alt="custom_button_view_style.png" src="https://qiita-image-store.s3.amazonaws.com/0/94929/dd3d5d52-599f-261b-46a5-36b32682b84d.png">

# カスタムビュークラス作成

次にカスタムビューのクラスを作成します。

```java
// CustomButtonView.java

public class CustomButtonView extends ConstraintLayout {
    public CustomButtonView(Context context) {
        this(context, null);
    }

    public CustomButtonView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.view_custom_button, this);
    }
}
```

# 完成？

これでいったんActivityに埋め込んでみましょう。


```xml
<!-- activity_main.xml -->

....

<***.***.CustomButtonView
        android:id="@+id/custom_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

...
```

onClickリスナーもセットしておきます。

```java
// MainActivity.java

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // インスタンスを取得
        CustomButtonView customButton = findViewById(R.id.custom_button);
        
        // onClickにリスナーを設定
        customButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "Click!", Toast.LENGTH_LONG).show();
            }
        });
    }
}
```

さて、これで上手くいきそう思えましたが、動かしてみるとカスタムボタンを**押してもonClickリスナーが呼ばれません。**

調べてみると以下の記事が参考になりました。

[CustomViewのOnClickが反応しない時の対処方法 - Qiita](https://qiita.com/konifar/items/518fcd6c90252a53bd86)

どうやらカスタムビューの場合はOnClickの処理は自分で書かないといけないみたいです。

# CustomButtonViewにonClickの処理を追加

上記の記事を参考に以下を追加します。

```java
// CustomButtonView.java

public class CustomButtonView extends ConstraintLayout {
    ...

    @Nullable
    private OnClickListener listener;

    @Override
    public void setOnClickListener(@Nullable OnClickListener l) {
        this.listener = l;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            if (listener != null) {
                post(new Runnable() {
                    @Override
                    public void run() {
                        listener.onClick(CustomButtonView.this);
                    }
                });
            }
        }

        return super.dispatchTouchEvent(ev);
    }
}
```

<img width="200" alt="custom_button_view_click.png" src="https://qiita-image-store.s3.amazonaws.com/0/94929/3793466d-a3d4-3634-b7fb-07a42ae0b936.png">

これでonClickが走るようになりました。

# ボタンの範囲外にずらしたときの動作を修正

これで完成のように思えますがデフォルトのボタンとは微妙に異なる動作があります。

* ボタンをタップ
* ボタンを押したまま指をボタンの範囲外にずらす
* 画面から離す

上の手順を行うとデフォルトのボタンではonClickは起こらないのですが、作成したCustomButtonViewではイベントが発生してしまいます。

これを修正するには以下のように`ACTION_UP`のときの座標がボタンの範囲内にあるかをチェックして、範囲内の場合にだけリスナーを呼ぶようにします。

```java
// CustomButtonView.java

class CustomButtonView extends ConstraintLayout {
    ...
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            // 範囲内のチェックを追加
            if (listener != null && checkInside(ev)) {
                post(new Runnable() {
                    @Override
                    public void run() {
                        listener.onClick(CustomButtonView.this);
                    }
                });
            }
        }

        return super.dispatchTouchEvent(ev);
    }

    private boolean checkInside(MotionEvent ev) {
        int[] point = new int[2];

        getLocationOnScreen(point);

        int x = point[0];
        int y = point[1];

        return (ev.getRawX() >= x && ev.getRawX() <= x + getWidth()) &&
                (ev.getRawY() >= y && ev.getRawY() <= y + getHeight());
    }
}
```

# 完成!

これで、見た目も動作もデフォルトのボタンとほぼ同様なカスタムビューボタンを作ることができました。