Processing math: 0%

2016年10月8日土曜日

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

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

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


 理論
ベクトルを使って押下判定を組む
三角形の頂点座標をOAB、タッチしたときの座標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_1k_2の値で三角形の内側か外側かを判定できる
証明は省略するが こんな感じ
表1. 係数による座標位置
条件 座標Tの位置
k_1=0かつk_2=0O
k_1=1かつk_2=0A
k_1=0かつk_2=1B
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_1k_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の実行条件はボタンが押された状態から、リリースされた時
なので、この三角形押下判定をボタン押下判定時に仕込むと、条件の絞り込みが楽になる
  1. /**
  2. * 三角形の中でのみ押下判定ができるImageViewクラス
  3. */
  4. public class MyImageView extends ImageView {
  5. // 三角形の座標情報
  6. protected final PointF O; // 頂点O
  7. protected final PointF A; // 頂点A
  8. protected final PointF B; // 頂点B
  9.  
  10. { // イニシャライザ。画像サイズを1×1として、座標位置を指定しておく
  11. O = new PointF(0.5f, 0.0f);
  12. A = new PointF(0f, 1f);
  13. B = new PointF(1f, 1f);
  14. }
  15.  
  16. // コントラクタ群
  17. public MyImageView(Context context) { super(context);}
  18. public MyImageView(Context context, AttributeSet attrs) {super(context, attrs);}
  19. public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}
  20.  
  21. @Override
  22. public boolean onTouchEvent(MotionEvent event) {
  23. if(event.getAction()== MotionEvent.ACTION_DOWN) {
  24. float x = event.getX(), y = event.getY();
  25. // 押下イベント通知のとき、条件判定を行い、三角形外であれば押下を処理しない
  26. // これにより、リリース時に、むやみにClick処理が動作しない
  27. if(!isTouch(new PointF(x, y))) {
  28. return false;
  29. }
  30. }
  31. return super.onTouchEvent(event);
  32. }
  33.  
  34. /**
  35. * 押下判定
  36. * タッチ座標が三角形の内側か判定する
  37. * @param T タッチ座標
  38. * @return <li>true:内側<li>false:外側
  39. */
  40. private boolean isTouch(final PointF T) {
  41. final PointF OA, OB, OO, OT;
  42.  
  43. // ベクトル化
  44. int w = getWidth(), h = getHeight();
  45. OO = new PointF(w * O.x, h * O.y);
  46. OA = new PointF(w * A.x - OO.x, h * A.y - OO.y);
  47. OB = new PointF(w * B.x - OO.x, h * B.y - OO.y);
  48. OT = new PointF(T.x - OO.x, T.y - OO.y);
  49.  
  50. // 連立方程式に持ち込んで、kを取得
  51. float a = OA.x * OB.y - OB.x * OA.y;
  52. if (a == 0) throw new IllegalStateException("座標計算ができません.");
  53. float k1, k2;
  54. k1 = (OT.x * OB.y - OB.x * OT.y) / a;
  55. k2 = (OA.x * OT.y - OT.x * OA.y) / a;
  56. return 0 <= k1 && 0 <= k2 && k1 + k2 <= 1;
  57. }
  58. }
あとは、普通に表示処理、押されたときの処理を組んでいく
クリック処理はこんなもん
  1. public class MainActivity extends AppCompatActivity {
  2.  
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main);
  7. // クリックでトースト表示
  8. findViewById(R.id.my_button).setOnClickListener(v -> {Toast.makeText(v.getContext(), "テスト", Toast.LENGTH_SHORT).show();});
  9. }
  10. }
layout設定やら
  1. # layout/activity_main.xml
  2. <?xml version="1.0" encoding="utf-8"?>
  3. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  4. xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  5. android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
  6. android:paddingRight="@dimen/activity_horizontal_margin"
  7. android:paddingTop="@dimen/activity_vertical_margin"
  8. android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
  9.  
  10. <TextView android:text="Hello World!" android:layout_width="wrap_content"
  11. android:layout_height="wrap_content" />
  12.  
  13. <com.example.kazuki.myapplication.MyImageView
  14. android:id="@+id/my_button"
  15. android:src="@drawable/mybtn_serect"
  16. android:layout_width="wrap_content"
  17. android:layout_height="wrap_content" />
  18. </RelativeLayout>
一応セレクタで押下画像を設定して、押している状態がわかるようにしとく
  1. # drawable/mybtn_serect.xml
  2. <?xml version="1.0" encoding="UTF-8"?>
  3. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  4. <item android:state_pressed="true" android:drawable="@drawable/image2" />
  5. <item android:drawable="@drawable/image1" />
  6. </selector>
できた!!

0 件のコメント:

コメントを投稿