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

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

レイアウト作成

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

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

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

<!-- 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>

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

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

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

// 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に埋め込んでみましょう。

<!-- activity_main.xml -->

....

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

...

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

// 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

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

CustomButtonViewにonClickの処理を追加

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

// 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);
    }
}

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

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

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

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

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

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

// 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());
    }
}

完成!

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