2016年10月8日土曜日

三角形ボタンの押下判定方法

基本的にViewで作成されるレイアウトは四角形だ
そこにいろんな形の画像でボタンを作るには、背景を透過にした絵のイメージ画像を使う
この方法で実装した際、Viewの絵の部分をクリックしたときに動作させたいが、画像の余白(透過)部分を押しても反応してしまう問題がある

三角形の画像を想定したとき、
Viewの余白を押したときは反応せず、画像部分を押したときに反応する処理を考える


 理論
ベクトルを使って押下判定を組む
三角形の頂点座標を$O$、$A$、$B$、タッチしたときの座標$T$とする
    $ \vec{OT} = \vec{t}, \vec{OA} = \vec{a},\vec{OB} = \vec{b} $ とおくと$\vec{t}$は以下のように表せられる
\[ \vec{t} = k_1 \vec{a} +k_2 \vec{b} \] この形にしたときの係数$k_1$、$k_2$の値で三角形の内側か外側かを判定できる
証明は省略するが こんな感じ
表1. 係数による座標位置
条件 座標$T$の位置
$k_1=0$かつ$k_2=0$点$O$
$k_1=1$かつ$k_2=0$点$A$
$k_1=0$かつ$k_2=1$点$B$
$0<k_1<1$かつ$k_2=0$線分$OA$上
$k_1=0$かつ$0<k_2<1$線分$OB$上
$0<k_1$ かつ $0<k_2$ かつ $k_1+k_2=1$線分$AB$上
$0<k_1$ かつ $0<k_2$ かつ $k_1+k_2<1$$\Delta OAB$ 内
上記以外$\Delta OAB$ 外

次に$k_1$、$k_2$の求め方
    $ \vec{t} = \begin{pmatrix} t_x \\ t_y \end{pmatrix} , \vec{a} = \begin{pmatrix} a_x \\ a_y \end{pmatrix} , \vec{b} = \begin{pmatrix} b_x \\ b_y \end{pmatrix} $ とする
\[ \vec{t} = k_1 \vec{a} +k_2 \vec{b} \\ \begin{pmatrix} t_x \\ t_y \end{pmatrix} = k_1 \begin{pmatrix} a_x \\ a_y \end{pmatrix} +k_2 \begin{pmatrix} b_x \\ b_y \end{pmatrix} \\ \begin{pmatrix} t_x \\ t_y \end{pmatrix} = \begin{pmatrix} a_x &b_x \\ a_y &b_y \end{pmatrix} \begin{pmatrix} k_1 \\ k_2 \end{pmatrix} \] よって、クラメルの公式より$k$ は以下のようになる
\[ k_1 = \frac{t_x b_y - t_y b_x}{a_x b_y - a_y b_x} \\ k_2 = \frac{a_x t_y - a_y t_x}{a_x b_y - a_y b_x} \]
 Androidで実装
まずは専用のImageViewを作る
AndroidはClick時の処理はView.OnClickListenerに実装する
このClickLisnerの実行条件はボタンが押された状態から、リリースされた時
なので、この三角形押下判定をボタン押下判定時に仕込むと、条件の絞り込みが楽になる
/**
 * 三角形の中でのみ押下判定ができるImageViewクラス
 */
public class MyImageView  extends ImageView {
        // 三角形の座標情報
        protected final PointF O; // 頂点O
        protected final PointF A; // 頂点A
        protected final PointF B; // 頂点B

        { // イニシャライザ。画像サイズを1×1として、座標位置を指定しておく
            O = new PointF(0.5f, 0.0f);
            A = new PointF(0f, 1f);
            B = new PointF(1f, 1f);
        }

        // コントラクタ群
        public MyImageView(Context context) { super(context);}
        public MyImageView(Context context, AttributeSet attrs) {super(context, attrs);}
        public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if(event.getAction()== MotionEvent.ACTION_DOWN) {
                float x = event.getX(), y = event.getY();
                // 押下イベント通知のとき、条件判定を行い、三角形外であれば押下を処理しない
                // これにより、リリース時に、むやみにClick処理が動作しない
                if(!isTouch(new PointF(x, y))) {
                    return false;
                }
            }
            return  super.onTouchEvent(event);
        }

        /**
         * 押下判定
         *   タッチ座標が三角形の内側か判定する
         * @param T タッチ座標
         * @return <li>true:内側<li>false:外側
         */
        private boolean isTouch(final PointF T) {
            final PointF OA, OB, OO, OT;

            // ベクトル化
            int w = getWidth(), h = getHeight();
            OO = new PointF(w * O.x, h * O.y);
            OA = new PointF(w * A.x - OO.x, h * A.y - OO.y);
            OB = new PointF(w * B.x - OO.x, h * B.y - OO.y);
            OT = new PointF(T.x - OO.x, T.y - OO.y);

            // 連立方程式に持ち込んで、kを取得
            float a = OA.x * OB.y - OB.x * OA.y;
            if (a == 0) throw new IllegalStateException("座標計算ができません.");
            float k1, k2;
            k1 = (OT.x * OB.y - OB.x * OT.y) / a;
            k2 = (OA.x * OT.y - OT.x * OA.y) / a;
            return 0 <= k1 && 0 <= k2 && k1 + k2 <= 1;
        }
}
あとは、普通に表示処理、押されたときの処理を組んでいく
クリック処理はこんなもん
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // クリックでトースト表示
        findViewById(R.id.my_button).setOnClickListener(v -> {Toast.makeText(v.getContext(), "テスト", Toast.LENGTH_SHORT).show();});
    }
}
layout設定やら
# layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView android:text="Hello World!" android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <com.example.kazuki.myapplication.MyImageView
        android:id="@+id/my_button"
        android:src="@drawable/mybtn_serect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>
一応セレクタで押下画像を設定して、押している状態がわかるようにしとく
# drawable/mybtn_serect.xml
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@drawable/image2" />
    <item android:drawable="@drawable/image1" />
</selector>
できた!!

0 件のコメント:

コメントを投稿