Как правильно обрабатывать события касания в Android?

Объем проекта

Когда пользователь касается экрана Android двумя пальцами, нарисуйте «Рамку» в каждом месте касания с «курсором» для каждого кадра. Каждый кадр представляет собой настраиваемый ползунок, по которому курсор будет перемещаться вверх и вниз. Весь путь вверх будет 100%, средний будет 0% и полностью вниз будет -100%. Это будет использоваться для управления небольшими двигателями, аналогично повороту танка, каждое касание управляет отдельным двигателем (отправляя сигналы по bluetooth). После двух касаний, и все будет нарисовано, я хочу иметь возможность оторвать любой палец, НО удерживать курсор в том месте, где он был последним, в то время как другой палец может перемещать его курсор. Когда последний палец отрывается, все «прячется» и сбрасывается до 0%.

Требуются функциональные возможности

  1. При касании двумя пальцами нарисуйте отдельные файлы .png под местом касания.
  2. После того, как кадры и курсоры нарисованы, отслеживайте их положение относительно кадра, чтобы определить процентное соотношение.
  3. Если палец оторван, удерживайте курсор этого пальца в последнем известном месте, но другой палец может перемещать его. Также, если палец опущен, он снова сможет перемещать курсор.
  4. Если оба пальца оторваны от экрана, скройте все и сбросьте проценты на 0%.

Функциональность получена

  • Я умею рисовать рамки и курсоры на мультитач
  • Позиции и проценты работают нормально
  • Курсоры движутся правильно

Что не работает

  • Я не уверен, что у меня должен быть один настраиваемый класс, который обрабатывает оба события касания, или у меня должно быть 2 экземпляра настраиваемого класса, каждый из которых обрабатывает свои собственные события касания (я пробовал оба, единственный способ получить любую «настоящую» функциональность - это 1 настраиваемый класс, обрабатывающий оба события касания, другой способ не работает должным образом)
  • Когда у меня есть только 1 настраиваемый класс, он отлично работает, но у меня он «скрывает» все, если оба пальца не на экране, а иногда Android регистрирует, что я убрал палец с экрана, и это вызывает у меня много проблем. когда кадры скрываются, затем снова появляются в другом месте
  • Когда я использую 2 настраиваемых класса, я касаюсь каждого настраиваемого класса, у него будет свое собственное событие касания, и мне не придется беспокоиться о мультитач, если я равномерно разделю классы между экранами. Такого не было, еще разобраться с мультитачем нужно

Может кто-нибудь объяснить мне, как Android обрабатывает события касания. из того, что я сделал, кажется, что если я положу палец 1, палец 2, первый палец зарегистрирует «ACTION_DOWN», а второй зарегистрирует «ACTION_POINTER

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parentLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <com.robota.android.TouchUI xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/leftTouchUI"
        android:background="#0000"
        android:layout_height="match_parent"
        android:layout_width="wrap_content"
        android:layout_weight="1">
    </com.robota.android.TouchUI>
    <com.robota.android.TouchUI xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rightTouchUI"
        android:background="#0000"
        android:layout_height="match_parent"
        android:layout_width="wrap_content"
        android:layout_weight="1">
    </com.robota.android.TouchUI>
</LinearLayout>
DOWN», НО если я живу на своем первом пальце, моем втором пальце "понижен", и теперь все события, регистрируемые моим вторым пальцем, не связаны с "ACTION_POINTER_2", а вместо этого будут "ACTION_DOWN, ACTION_UP и т. д.". Это правильно?

TouchUI.java

    package com.robota.android;

    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.Canvas;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.widget.ImageView;

public class TouchUI extends ImageView {

public static final String LEFT_TOUCHUI = "com.robota.android:id/leftTouchUI";
public static final String RIGHT_TOUCHUI = "com.robota.android:id/rightTouchUI";
private String whoAmI = new String();
private MyPoints framePts = new MyPoints();
private MyPoints cursorPts = new MyPoints();
private Bitmap frame;
private Bitmap cursor;
private int frameWidth;
private int frameHeight;
private int cursorHeight;
private boolean pointerDown = false;
private int dy;

public TouchUI(final Context context, final AttributeSet as){
    super(context, as);
    Log.d("TouchUI", getResources().getResourceName(this.getId()));
    whoAmI = new String(getResources().getResourceName(this.getId()));
    if(whoAmI.equals(LEFT_TOUCHUI)){
        frame = BitmapFactory.decodeResource(getResources(), R.drawable.tank_left);
    }else if(whoAmI.equals(RIGHT_TOUCHUI)){
        frame = BitmapFactory.decodeResource(getResources(), R.drawable.tank_right);
    }
    cursor = BitmapFactory.decodeResource(getResources(), R.drawable.cursor);
    frameWidth = frame.getWidth();
    frameHeight = frame.getHeight();
    cursorHeight = cursor.getHeight();
}

public void determinePointers(int x, int y){
        framePts.setOrigin(x-frameWidth/2, y-frameHeight/2);
        cursorPts.setOrigin(x-frameWidth/2, y-frameHeight/2);
}

@Override
public boolean onTouchEvent(MotionEvent e){
    int x = 0;
    int y = 0;
    Log.d("TouchUI", ">>>>> " + whoAmI);
    if(e.getAction() == MotionEvent.ACTION_DOWN){
        determinePointers(x,y);
        pointerDown = true;
    }else if(e.getAction() == MotionEvent.ACTION_UP){
        pointerDown = false;
    }else if(e.getAction() == MotionEvent.ACTION_MOVE){
        dy = (int)e.getY()-framePts.getY();
        if(dy <= 0){
            dy=0;
        }else if(dy+cursorHeight/2 >= frameHeight){
            dy=frameHeight;
        }
        sendMotorSpeed(dy);
    }
    return true;
}

public void sendMotorSpeed(int dy){
    float motor = dy;
    motor-=frameHeight;
    motor*=-1;

    motor = (motor/frameHeight)*255;

    PacketController.updateMotorSpeeds(whoAmI, (int)motor);
}

public void onDraw(Canvas canvas){
    if(pointerDown){//twoDown){
        canvas.drawBitmap(frame, framePts.getX(), framePts.getY(), null);
        canvas.drawBitmap(cursor, cursorPts.getX(), (cursorPts.getY()+dy), null);
    }
    invalidate();
}

private class MyPoints{

    private int x = -100;
    private int y = -100;
    private int deltaY = 0;;

    public MyPoints(){
        this.x = 0;
        this.y = 0;
    }

    public int getX(){
        return this.x;
    }

    public int getY(){
        return this.y;
    }

    public void setOrigin(int x, int y){
        this.x = x;
        this.y = y;
    }

    public int getDeltaY(){
        return deltaY;
    }

    public void setDeltaY(int newY){
        deltaY = (newY-y);
        Log.d("TouchUI", "DY: " + deltaY);
    }
}
}

Main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parentLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <com.robota.android.TouchUI xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/leftTouchUI"
        android:background="#0000"
        android:layout_height="match_parent"
        android:layout_width="wrap_content"
        android:layout_weight="1">
    </com.robota.android.TouchUI>
    <com.robota.android.TouchUI xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rightTouchUI"
        android:background="#0000"
        android:layout_height="match_parent"
        android:layout_width="wrap_content"
        android:layout_weight="1">
    </com.robota.android.TouchUI>
</LinearLayout>

RobotController.java (Main Activity Class)

    package com.robota.android;

    import android.app.Activity;
    import android.bluetooth.BluetoothAdapter;
    import android.bluetooth.BluetoothDevice;
    import android.content.ActivityNotFoundException;
    import android.content.Intent;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.util.Log;
    import android.view.Menu;
    import android.view.MenuInflater;
    import android.view.MenuItem;
    import android.view.View;
    import android.view.Window;
    import android.widget.ScrollView;
    import android.widget.TextView;
    import android.widget.Toast;

public class RobotController extends Activity {
// Tag used to keep track of class in the Log
private static final String TAG = "robotController_new";
// Boolean to debugging
private static final boolean D = true;

// Intent request codes
private static final int DISCONNECT_DEVICE = 1;
private static final int CONNECT_DEVICE = 2;
private static final int REQUEST_ENABLE_BT = 3;

// Handler Codes
public static final int MESSAGE_READ = 1;
public static final int MESSAGE_WRITE = 2;

// Local Bluetooth Adapter
private BluetoothAdapter bluetoothAdapter = null;
// Bluetooth Discovery and Datahandler
private BluetoothComm btComm = null;

// Debug's TextView, this is where strings will be written to display
private TextView tv;
private ScrollView sv;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    if(D) Log.d(TAG, "++ON CREATE++");
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.main);

    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

    if(bluetoothAdapter == null){
        if(D) Log.d(TAG, "NO BLUETOOTH DEVICE");
        Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_SHORT).show();
        finish();
        return;
    }

    PacketController.controller = this;
}


public void onStart(){
    super.onStart();
    if(D) Log.d(TAG, "++ON START++");

    if(!bluetoothAdapter.isEnabled()){
        Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
    }else{
        // Start BluetoothComm
        if(btComm == null){
            setupComm();
        }
    }
}

/**
 * Creates new Bluetooth Communication
 */
private void setupComm(){
    if(D) Log.d(TAG, "+++setupComm+++");
    btComm = new BluetoothComm(this, handler);
}

private void connectDevice(Intent data){
    if(D) Log.d(TAG, "+++connectDevice+++");
    String addr = data.getExtras()
        .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
    BluetoothDevice device = bluetoothAdapter.getRemoteDevice(addr);
    if(D) Log.d(TAG,"REMOTE ADDR: "+ addr);
    btComm.connect(device);
}

private void disconnectDevice(){
    if(D) Log.d(TAG, "---disconnectDevice---");
    if(btComm.getState() == btComm.STATE_CONNECTED){
        btComm.disconnect();
    }
}

@Override
public boolean onCreateOptionsMenu(Menu menu)
{
    //super.onCreateOptionsMenu(menu);
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    Intent serverIntent = null;
    switch(item.getItemId()){       
    case R.id.insecure_connect_scan:
        // Launch the DeviceListActivity to see devices and do scan
        serverIntent = new Intent(this, DeviceListActivity.class);
        try{
            startActivityForResult(serverIntent, CONNECT_DEVICE);
        }catch(ActivityNotFoundException activityNotFound){
            Log.e(TAG, "Could not start DeviceListActivity(Insecure)");
        }
        return true;
    }
    return false;
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
    switch(requestCode){
    case CONNECT_DEVICE:
        if(resultCode == Activity.RESULT_OK){
            connectDevice(data);
        }
        break;
    case DISCONNECT_DEVICE:
        if(resultCode == Activity.RESULT_OK){
            disconnectDevice();
        }
        break;
    }
}

public Handler getHandler(){
    return this.handler;
}

public BluetoothComm getBtComm(){
    return this.btComm;
}

// The Handler that gets information back from the BluetoothChatService
private final Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if(D) Log.d(TAG, "check message");
        switch (msg.what) {
        case MESSAGE_READ:
            if(D) Log.d(TAG, "trying to read message");
            byte[] readBuf = (byte[]) msg.obj;
            // construct a string from the valid bytes in the buffer
            String readMessage = new String(readBuf, 0, msg.arg1);
            if(D) Log.d(TAG, "bytes: " + readBuf + " arg1: " + msg.arg1 + " Message: " + readMessage);
            tv.append(readMessage);
            break;
        case MESSAGE_WRITE:
            if(D) Log.d(TAG, "trying to send message");
            String sendMessage = new String(String.valueOf(msg.obj));
        }
    }
};
}

Любые другие классы, не указанные в списке, я не считал необходимыми, но если они необходимы, дайте мне знать.

Любая помощь высоко ценится


person heater    schedule 31.05.2011    source источник


Ответы (1)


Вам нужно будет сохранить идентификаторы указателей каждой точки и сравнить их с новыми идентификаторами, указанными для каждого события MotionEvent. Это немного сложно объяснить, поэтому я укажу вам на это Сообщение ADB, которое объясняет это намного лучше, чем я мог. Короче говоря? Мультитач может показаться хитрым, но это не так плохо, как кажется на первый взгляд.

person Geobits    schedule 31.05.2011
comment
Но возвращает ли motionEvent.getPointerIndex (PointerNumber) правильное касание, если я касаюсь первым пальцем, касаюсь вторым пальцем, а затем поднимаю первый палец. Указательный ли у меня второй палец - 0 или 1? И останется ли он этим индексом на всем протяжении ??? - person heater; 31.05.2011
comment
Что ж, индекс может измениться. Идентификатор останется постоянным, поэтому используйте его для ссылки. Чтобы узнать, какой индекс для данного указателя имеет только -current- MotionEvent, используйте findPointerIndex (id). Обязательно перечитайте раздел об индексе и идентификаторе в приведенной мной ссылке. - person Geobits; 01.06.2011
comment
Хорошо, эта статья помогает мне постепенно понять, но я не понимаю, должен ли я произвольно выбирать идентификатор указателя для каждого касания? Эта строка mActivePointerId = ev.getPointerId (0); смутила меня, потому что 0 относится к первому пальцу вниз или это число, которое кодировщик выбрал для обозначения первого пальца вниз? Означает ли это, что мой второй палец вниз должен быть равен 1, а, возможно, мой третий палец должен быть 2? Заранее спасибо. - person heater; 01.06.2011
comment
Нет, это не произвольно. Указанная вами строка находится внутри случая ACTION_DOWN. ACTION_DOWN срабатывает только при нажатии первого пальца. Во-вторых, вместо этого будет генерироваться ACTION_POINTER_DOWN. Итак, он использует здесь 0, потому что знает, что в событии есть только один индекс, и ему нужен первый (и единственный) один. Что вам следует сделать, так это сохранить идентификатор для каждого пальца каждый раз, когда вы выполняете действие типа ВНИЗ. Когда вы получаете действие типа MOVE или UP, запустите findPointerIndex () для каждого сохраненного идентификатора. Тогда у вас есть индекс, который вы можете использовать для вызова getX / Y (index). Не забудьте также удалить идентификатор на событиях UP. - person Geobits; 02.06.2011
comment
Хорошо, мне очень жаль, но у меня все еще есть проблемы с пониманием, большое спасибо за ваше терпение. FindPointerIndex () требует идентификатора для правильного определения индекса? Но тогда для метода getPointerId () требуется индекс? Мне это кажется очень круглым ...: P. Так что мне просто нужно назначить идентификатор для моего первого пальца (например: 0) и назначить идентификатор для моего второго пальца (например: 1)? Или какой метод использовать для получения начального идентификатора касания без индексации? Еще раз спасибо за терпение. - person heater; 02.06.2011
comment
Взгляните на следующую строку из этого сообщения: final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) ›› MotionEvent.ACTION_POINTER_INDEX_SHIFT; Здесь вы можете использовать операцию маски и сдвига, чтобы получить индекс для указателя current, который взаимодействует с POINTER_UP / DOWN. Для простого ACTION_UP / DOWN просто используйте индекс 0, как указано выше, потому что в этом событии только один. Затем получите идентификатор с помощью getPointerId () - person Geobits; 03.06.2011
comment
В этом есть смысл. В порядке. Таким образом, индекс ACTION_DOWN равен 0, потому что опущен только 1 палец. Итак, я буду использовать getPointerId (index) для получения первого идентификатора. Затем в ACTION_POINTER_DOWN я буду использовать упомянутый метод для получения индекса второго пальца и использовать getPointerId (index) для получения второго идентификатора. В этом есть смысл. Теперь эти идентификаторы изменятся, правда? Или после получения ID они не меняются? - person heater; 03.06.2011
comment
Идентификаторы будут оставаться неизменными на протяжении всего жеста (от первого вниз до последнего вверх), но не после этого. Тем не менее, индекс может меняться при каждом событии MotionEvent, поэтому вам нужно получать индексы для каждого события и сравнивать их с сохраненными идентификаторами. - person Geobits; 03.06.2011
comment
Замечательно, большое спасибо за помощь и за ваше терпение. Я собираюсь все это попробовать и посмотреть, не смогу ли я получить функциональность, которую ищу сегодня вечером. - person heater; 03.06.2011