Scope of the project
When a user touches the Android screen with two fingers, draw a "Frame" at each touch location with a "cursor" for each frame. Each frame is a custom slider that the cursor will move up and down. All the way up will be 100%, middle will be 0% and all the way down will be -100%. This will be used to control small motors, similar to tank turning, each touch controls a separate motor (sending signals over bluetooth). After a two touch and everything is drawn, I want to be able to lift off either finger, BUT keep the cursor at what ever location it was last at, while the other finger is free to move its cursor. When the last finger is lifted off, everything "hides" and resets to 0%.
Functionality Wanted
- On two finger touch, draw separate .pngs under the touch location
- After the frames and cursors are drawn, keep track of where they are relative to the frame to determine the percentage.
- If a finger is lifted off, keep that fingers cursor at last known location, but the other finger can move it's cursor. Also if the finger is put back down it should be able to move its cursor again.
- If both fingers are lifted off of the screen, hide everything and reset percentages to 0%
Functionality Obtained
- I can draw the frames and cursors on multitouch
- Positions and percentages work fine
- Cursors do move properly
What doesn't work
- I am unsure if I should have one custom class that handles both touch event or if i should have 2 instances of the custom class each handling their own touch events (I have tried both, the only way i get any "real" functionality is with 1 custom class handling both touch events, the other way doesn't work as intended)
- When I only have 1 custom class, It works great, but I have it "hide" everything if both fingers are not on the screen, and sometimes android registers that I have lifted a finger off the screen and this causes me a lot of issues when the frames hide then re appear in a different location
- When I use 2 custom classes I touch each custom class would have its own touch event, and i wouldn't have to worry about multitouch if i split the classes evenly between the screen. This was not the case, still need to deal with multitouch
Can someone explain to me how android handles their touch events. from what I have done, it seems if i lay down finger 1, the finger 2, the first finger will register a "ACTION_DOWN" and the second will register a "ACTION_POINTER_2_DOWN", BUT if i life off my first finger, my second finger is "demoted" and now all of the events my second finger registers does not related to "ACTION_POINTER_2" and instead will be "ACTION_DOWN, ACTION_UP, etc". Is this correct?
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));
}
}
};
}
Any other classes not listed I didn't believe needed to be, but if they are needed please let me know.
Any help is much appreciated