官术网_书友最值得收藏!

The most basic virtual keypad

The simplest we can go is to build a simple keypad in the shape of a cross on the left-hand side of the screen and a fire button on its right-hand side. For this layout, we are going to create a new file under the layout folder and call it view_keypad.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_gravity="bottom"
  android:padding="@dimen/keypad_size"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">

  <Button
    android:id="@+id/keypad_up"
    android:layout_alignParentTop="true"
    android:layout_toRightOf="@+id/keypad_left"
    android:layout_width="@dimen/keypad_size"
    android:layout_height="@dimen/keypad_size" />

  <Button
    android:id="@+id/keypad_down"
    android:layout_below="@+id/keypad_left"
    android:layout_toRightOf="@+id/keypad_left"
    android:layout_width="@dimen/keypad_size"
    android:layout_height="@dimen/keypad_size" />

  <Button
    android:id="@+id/keypad_left"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/keypad_up"
    android:layout_width="@dimen/keypad_size"
    android:layout_height="@dimen/keypad_size" />

  <Button
    android:id="@+id/keypad_right"
    android:layout_toRightOf="@+id/keypad_up"
    android:layout_below="@+id/keypad_up"
    android:layout_width="@dimen/keypad_size"
    android:layout_height="@dimen/keypad_size" />

  <Button
    android:id="@+id/keypad_fire"
    android:layout_alignParentRight="true"
    android:layout_alignTop="@+id/keypad_left"
    android:layout_width="@dimen/keypad_size"
    android:layout_height="@dimen/keypad_size" />
</RelativeLayout>

We have a relative layout that covers the screen's full width. It has a layout_gravity set to bottom, so we are assured that it will be properly aligned.

We have our four-button pad arranged in a RelativeLayout. The left button is aligned to the left of the layout and the up button is aligned to the top of the layout. Then, the top and bottom buttons are set to the right of the left button. The right one is set below and to the right of the up button. Finally, the left one is set below the up button and the down button is just below the left. Sounds a bit too complicated, but the image is much clearer.

On the other side of the screen, aligned to the right of the parent and to the top of the left button, we have a fire button.

You may have noticed that all the buttons are using a special dimension named keypad_size. This is a very important point, not only to make them all look the same, but for usability in general. We are setting it to 42 dp, which is the recommended minimum size for a touch target.

Note

The smallest size a touchable item should have is 42 dp.

Feel free to play with the size of the buttons and observe by yourself that a smaller size button is very hard to touch. In fact, for a game, we should always use large-sized touch targets, sometimes larger than the area that provides visual feedback. The bigger you can make the touch area for a control, the better it is. In this example, the touch area of the fire button could be as large as the right half of the screen.

We are going to include this layout inside the game fragment, so we can see how it overlays. Since we already updated the top layout to be a FrameLayout, we just need to use an include tag.

<FrameLayout 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:paddingTop="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  tools:context="com.plattysoft.yass.counter.GameFragment">

  <TextView
    android:layout_gravity="top|left"
    android:id="@+id/txt_score"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" />

  <Button
    android:layout_gravity="top|right"
    android:id="@+id/btn_play_pause"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/pause" />
  <include layout="@layout/view_keypad" />
</FrameLayout>

If we just go on and run it, we can see how it looks all together.

Now let's write the code for the BasicInputController to handle the buttons. Starting with the constructor, the code is as follows:

public BasicInputController(View view) {
  view.findViewById(R.id.keypad_up).setOnTouchListener(this);
  view.findViewById(R.id.keypad_down).setOnTouchListener(this);
  view.findViewById(R.id.keypad_left).setOnTouchListener(this);
  view.findViewById(R.id.keypad_right).setOnTouchListener(this);
  view.findViewById(R.id.keypad_fire).setOnTouchListener(this);
}

We are setting the game controller as the touch listener for all the buttons: up, down, left, right, and fire. It is important to note that we have to use OnTouchListener and not OnClickListener.

The onClick callback is only triggered when the button is pressed and then released. In our case, we need to know when the button is pressed and when it is released. We need to move the spaceship while the button is being pressed. This is why we need the more detailed callback that OnTouchListener provides.

The implementation of the method from OnTouchListener in the BasicInputController is as follows:

@Override
public boolean onTouch(View v, MotionEvent event) {
  int action = event.getActionMasked();
  int id = v.getId();
  if (action == MotionEvent.ACTION_DOWN) {
    // User started pressing a key
    if (id == R.id.keypad_up) {
      mVerticalFactor -= 1;
    }
    else if (id == R.id.keypad_down) {
      mVerticalFactor += 1;
    }
    else if (id == R.id.keypad_left) {
      mHorizontalFactor -= 1;
    }
    else if (id == R.id.keypad_right) {
      mHorizontalFactor += 1;
    }
    else if (id == R.id.keypad_fire) {
      mIsFiring = false;
    }
  }
  else if (action == MotionEvent.ACTION_UP) {
    if (id == R.id.keypad_up) {
      mVerticalFactor += 1;
    }
    else if (id == R.id.keypad_down) {
      mVerticalFactor -= 1;
    }
    else if (id == R.id.keypad_left) {
      mHorizontalFactor += 1;
    }
    else if (id == R.id.keypad_right) {
      mHorizontalFactor -= 1;
    }
    else if (id == R.id.keypad_fire) {
      mIsFiring = false;
    }
  }
  return false;
}

It is important to note that we are calling getActionMasked instead of getAction. In the case of multiple touch pointers, getAction includes pointer information while that information is removed when requested as a masked action. This is why the recommended way to handle multitouch is to to use getActionMasked and getActionPointer. Otherwise, you need to use the OR operation to check for the action instead of the equal or it won't work when the pointers above the first one are being read.

Note

Using getActionMasked and getPointerIndex is the recommended way to deal with multitouch.

We have two cases. When the action is MotionEvent.ACTION_DOWN, it means that the user has pressed a button, so we check for the ID of the view that was touched and act accordingly.

If the view is up or down, we subtract or add 1 to the vertical factor. Similarly, we subtract or add 1 to the horizontal factor if the touched button was left or right.

The second part, where we handle the MotionEvent.ACTION_UP action, reverses the addition or subtraction to the corresponding factor.

We are adding and subtracting instead of setting the value to 1 or -1 for multitouch. For example, if you first tap on right and then on left, the spaceship should stop, since you are pressing on both buttons at the same time. Once you release one of them, the movement is restored.

For the fire button, we set mIsFiring to true when it is down and to false when it is up. Simple.

Finally, we return false. This is important, because it tells the system that the event was not consumed by our listener and, therefore, the chain of listeners can continue. This chain of listeners includes the button's own click listener, which is responsible for changing the background image to one consistent with the button's state. If we return true, updating the background will not happen.

Note

OnTouch implementation returns whether the event was consumed by this listener or not.

As simple as this—we can run the game now. We will see that the spaceship moves around the screen and also fires some bullets. At last, YASS starts to look like a game.

Limitations and problems

There are several limitations and problems with such a simple keypad. Apart from the fact that the buttons are quite small and hard to handle, the rest of the issues come from when users move the touch pointer.

If the user moves outside the button, Android versions before API level 17 will trigger an event of the MotionEvent.ACTION_DOWN type, but from this API level onwards they will not. If you want to handle this situation properly, you need to check on every move or action and validate whether it gets out of the rectangle for the original view to do a manual cancel. But this is not the only problem with move. If you tap on one button and move towards the opposite one, a new tap on the other button will not be detected, since it is an ACTION_MOVE and not an ACTION_DOWN.

The solution for this is to check the position of each pointer in each event, see whether it is inside the rectangle of a button, and act accordingly.

There is also the problem of not being able to handle diagonal movements.

We could try and solve these problems for this keypad. But since it is not a very elegant input controller anyway, we will just move forward and make an InputController that is a proper virtual joystick instead.

主站蜘蛛池模板: 察隅县| 沛县| 株洲市| 祁门县| 阿图什市| 彭山县| 阿巴嘎旗| 灵台县| 垣曲县| 长泰县| 台东县| 江西省| 江油市| 朔州市| 永丰县| 无棣县| 茶陵县| 九龙坡区| 灯塔市| 宁都县| 鄢陵县| 全南县| 赣榆县| 瑞丽市| 博湖县| 青海省| 白沙| 民丰县| 新田县| 萍乡市| 德惠市| 吴旗县| 东阳市| 阜康市| 于都县| 新和县| 阳新县| 大兴区| 高州市| 清徐县| 明溪县|