Smartphone als Mousepad benutzen

Wer ein Laptop besitzt, hat standardmäßig ein Mousepad. Meistens erweitert man sein Equipment durch eine Mause. Besitzt man allerdings einen Stand-PC inklusive Maus, rüstet man selten ein Touchpad nach. Warum auch?

Ich habe mir trotzdem mal die Mühe gemacht und ein kleine App für Android programmiert, mit der man sein Smartphone als Touchpad benutzen kann. Weniger des Nutzes wegen, sondern vielmehr der Programmierübung.

Android

Design

Zunächst einmal müssen wir uns überlegen, wie die Oberfläche der App überhaupt aussehen soll. Ich würde sagen, wir lassen den ganzen Screen weiß und setzen in die obere Hälfte zwei bunte Flächen, die unsere zwei Maustasten repräsentieren sollen. Für das Design habe ich eine extra Classe als View erstellt:

public class Draw extends View {

    private Paint paintL;
    private Paint paintR;

    public Draw(Context context){
        super(context);

        paintL = new Paint();
        paintL.setColor(Color.RED);
        paintR = new Paint();
        paintR.setColor(Color.BLUE);

    }

    @Override
    protected void onDraw(Canvas canvas){

        DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();

        canvas.drawRect(0,0,displayMetrics.widthPixels/2,displayMetrics.heightPixels/4, paintL);
        canvas.drawRect(displayMetrics.widthPixels/2,0,displayMetrics.widthPixels,displayMetrics.heightPixels/4, paintR);


    }
}

Zwei Rect-Objekte, denen jeweils ein Farbmuster zugewiesen wird und eine Größe, welche sich an den Verhältnissen der Bildschirmauflösung orientiert.

Natürlich haben wir noch das XML File unserer MainActivity. Dieses löschen wir nicht, sondern entfernen lediglich alle Objekte und fügen eine Layout ID hinzu.

android:id="@+id/relativeLayout">

Der Sender

Da wir unsere Daten ja irgendwie an den PC schicken wollen, fehlt uns noch eine Sendemittel. Hierzu nutzen wir Java’s Socket, die „lower-level network communication“, mit deren Hilfe man netzwerkintern Daten austauschen kann.

public class Send extends AsyncTask{

    private Socket client;
    private PrintWriter printWriter;
    private String message;
    private String text;
    private String id;

    public Send(String s, String id) {
        this.id = id;
        this.text = s;
    }

    @Override
    protected Object doInBackground(Object[] objects) {

        message = text;
        try {
            client = new Socket(id,4445);
            printWriter = new PrintWriter(client.getOutputStream(), true);
            printWriter.write(message);
            printWriter.flush();
            printWriter.close();
            client.close();


        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }
}

Die Klasse ist relativ schnell erklärt: Man gibt bei aufrufen eines Send()-Objektes den zu sendenen Text an, welcher dann als message an die zusätzlich angegebene ID des Zielcomputers gesendet wird. Neben der ID ist noch der Port, welcher dem Socket-Objekt (hier client) übergeben wird. Dieser sollte zwischen 4444 und 5555 liegen und mit dem dem übereinstimmen, welchen wir später auf dem PC Programm festlegen. Sollte es mal zu Verbindungsproblemen kommen, so könnte der gewählte Port der Fehler sein, da er schon vom System belegt sein könnte. In solchen Fällen einfach einen anderen ausprobieren!

Wir wollen hier nicht weiter auf Sockets eingehen, obwohl es wirklich ein großes und durchaus interessantes Themengebiet ist.

Manifest

Um die Verbindung zu ermöglichen, fehlt noch ein Einzeiler, der mir durch simples Vergessen schon viele Probleme bereitet hat!

Wir öffnen also das Manifest und kopieren folgende line hinzu:

<manifest package="com.vonpichowski.arne.mousecontrol"
          xmlns:android="http://schemas.android.com/apk/res/android">
          <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"

Nun hat unsere App auch die Erlaubnis, das Intener benutzen zu dürfen.

Main

Nun kommen wir auch endlich zur MainActivity.java. Zunächst einmal legen wir unsere Draw Classe als View fest.

public class MainActivity extends AppCompatActivity {

    private Draw draw;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        draw = new Draw(this);
        
        RelativeLayout layout= (RelativeLayout) findViewById(R.id.relativeLayout);
        layout.addView(draw);

Wie im Senderpart schon angesprochen, brauchen wir noch die ID des Zielcomputers. Da diese je nach Netzwerk und Computer natürlich wechselt, können wir sie nicht von Anfang an festlegen, sondern müssen einen Dialog erzeugen, der dem Nutzer die Möglichkeit gibt, die ID einzugeben.

private String ID;
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);

dialogBuilder.setMessage("Insert ID.");
dialogBuilder.setCancelable(true);
final EditText titleBox = new EditText(this);
dialogBuilder.setView(titleBox);

dialogBuilder.setPositiveButton(
        "OK",
        new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                ID = titleBox.getText().toString();
                dialog.cancel();
            }
        });

AlertDialog alert = dialogBuilder.create();
alert.show();

Wir haben nun also einen Dialog, der nach dem Starten der App erscheint, die ID abfragt und in das hierzu vorgesehene Stringobjekt speichert.

Im Folgenden wird nun die Eingabe verarbeitet. Vorbereitend hierzu holen wir uns die Displaygrößen und den Point() mouseLoc, auf den später noch eingegangen wird.

private Point mouseLoc = new Point();
final DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);

mouseLoc.set(displayMetrics.widthPixels/2, displayMetrics.heightPixels/2);

Nun können wir auf unser Layout einen TouchListener hinzufügen.

layout.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()){
            case MotionEvent.ACTION_MOVE:

            case MotionEvent.ACTION_DOWN:
                
            case MotionEvent.ACTION_UP:
               
        }
        return true;
    }
});

Wir unterscheiden in drei touch-Möglichkeiten: move für die Mausezeigerbewegung, down für die linke und rechte Taste und up wird uns bei move unterstützen.

Fangen wir der Einfachheit halber mit den Tasten an. Hierzu unterscheiden wir zuerst, ob der Druck im bunten Tastenbereich des Displays liegt, danach, ob links oder rechts. Dem Ergebnis entsprechend wird einem Send()-Objekt die zuvor eingegeben ID und die Klickseite übergeben.

case MotionEvent.ACTION_DOWN:
    if(motionEvent.getY()<=displayMetrics.heightPixels/4){
        if(motionEvent.getX()<=displayMetrics.widthPixels/2){
            new Send("l",ID).execute();
        }else{
            new Send("r",ID).execute();
        }
    }
    break;

Im nächsten Schritt gucken wir uns die move Bewegung an.

private boolean move = false;
case MotionEvent.ACTION_MOVE:

    if(!move){
        mouseLoc.set((int) motionEvent.getX(),(int) motionEvent.getY());
    }

    move = true;

    String message = "m"+String.valueOf((int) (motionEvent.getX()-mouseLoc.x))+"|"+String.valueOf((int) (motionEvent.getY()-mouseLoc.y));
    new Send(message,ID).execute();

    break;

Zunächst einmal fügen wir ein boolean Wert hinzu, der angibt, ob eine move Bewegung ausgeführt wird oder beendet ist.

Sollte der TouchListener nun eine Bewegung bemerken, move ist aber false, bedeutet dies, dass der Finger zum ersten mal auf den Display gesetzt wurde, bzw. die Bewegung nun begonnen hat. Die Fingerposition wird als mouseLoc gespeichert.

Die Bewegung hat also angefangen und move kann true gesetzt werden.

Jetzt bauen wir uns noch unseren String zusammen, der anschließend samt ID an das Send() Objekt übergeben wird. Dieser String setzt sich zusammen aus m (diese Angabe wird später beim PC Programm noch wichtig), aus der aktuellen x-Position des Fingers, abzüglich des Bewegunsstartwertes und natürlich aus auch aus dem y-Wert. Solange die Bewegung also nicht abgeschlossen wird, wird immer die Differenz zwischen dem ersten Druckpunkt und dem aktuellen Punkt gesendet.

Um das Ende der Bewegung festzustellen, wird der up case benutzt.

case MotionEvent.ACTION_UP:

    if(move){
        move = false;
        new Send("u", ID).execute();
    }

    break;

Nach der Unterscheidung, ob es sich um eine endende Bewegung oder um einen einfachen Druck handelt, wird move wieder auf false gesetzt und auch dem PC mitgeteilt, dass die Bewegung zuende ist.

Das war’s auch schon mit dem Androidteil des Projekts!

Java

Das Programm selbst wird im Hintergrund laufen, das heißt wir brauchen keine richtige Oberfläche. Eigentlich. Denn am Start des Programms wäre es natürlich schön, wenn es uns die IP Adresse des PCs anzeigt, damit wir sie in unser Smartphone eintragen können. Hierzu reicht ein simples JOptionPane.

public class TouchPad {

    public static void main(String[] args) throws UnknownHostException, AWTException {

        InetAddress ip = InetAddress.getLocalHost ();
        JOptionPane.showMessageDialog(null, ip.getHostAddress());
    }
}

Das Grundprinzip des Programms basiert auf dem Robo Tool von Java. Dieses ermöglicht eine Art kleine Manipulation der Maus, Tastendrücke und anderer Sachen. In unserem Fall werden wir es benutzen, um den Courser zu bewegen. Wir fügen also im nächsten Schritt ein Robo() Objekt hinzu und zusätzlich schon wie bei unserer App die Screengrößen und auch wieder einen mouseLoc Point(). Diesmal ergänzen wir allerdings auch den mouseActLoc Point(), welcher später die aktuelle Position des Coursers speichern wird.

private static Robot robo;
private static Point mouseLoc;
private static Point mouseActLoc;
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
double width = screenSize.getWidth();
double height = screenSize.getHeight();

mouseLoc = new Point();
mouseActLoc = new Point();
mouseLoc.setLocation(width/2,height/2);

robo = new Robot();

Nun fügen wir den Socket Gegenspieler hinzu. Dieser wird einfach in der main Klasse integriert, ohne gleich eine neue Klasse zu schreiben.

private static ServerSocket serverSocket;
private static Socket clientSocket;
private static InputStreamReader inputStreamReader;
private static BufferedReader bufferedReader;
private static String message;
try{
    serverSocket = new ServerSocket(4445);

} catch (IOException e) {
    e.printStackTrace();
}

while(true){
    try{
        clientSocket = serverSocket.accept();
        inputStreamReader = new InputStreamReader(clientSocket.getInputStream());
        bufferedReader = new BufferedReader(inputStreamReader);
        message = bufferedReader.readLine();

        switch(message.substring(0, 1)){
            case "r": 

            case "l": 

            case "m": 

            case "u": 

        }

        inputStreamReader.close();
        clientSocket.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Wie oben angesprochen, haben wir hier erneut unseren Port 4445, der frei wählbar ist, aber dem in der App entsprechen sollte. Aus der message lesen wir nun zunächst den ersten Buchstaben heraus, welcher uns die Art der Info (rechte Maustaste, linke Maustaste, Bewegung des Courser, Ende der Bewegung) angibt.

Gehen wir in die Fallunterscheidung: Fangen wir mit den Maustasten an.

case "r": robo.mousePress(InputEvent.BUTTON3_MASK);
    robo.mouseRelease(InputEvent.BUTTON3_MASK);
    break;

Relativ simple geben wir über unser robo Objekt die Anweisung, die rechte Taste zu drücken und sofort wieder loszulassen. Mit der linken Taste verhält es sich genauso, nur dass man statt BUTTON3_MASK, BUTTON1_MASK verwendet.

Kommen wir zur Bewegung. In dieser message sind nicht nur das m für move enthalten, sondern auch noch die Koordinaten für die x, als auch für die y-Position. Folglich müssen wir den String erst einmal auftrennen. Da der erste char eh schon für das m reserviert ist, durchsuchen wir noch den restlichen Teil nach der Position unseres Trennsymbols „|“. Dessen Position speichern wir in der Variable s. Jetzt noch den Stringteile in Integer parsen und den Courser dementsprechend bewegen. Als letzte Aktion wird die aktuelle Position des Coursers in mouseActLoc zwischengespeichert.

case "m": int s = 0;
    for(int i = 1; i<message.length(); i++){
        if(message.charAt(i) == '|'){ sec = i; break;}
    }
    int x = Integer.parseInt(message.substring(1, s));
    int y = Integer.parseInt(message.substring(s+1, message.length()));
    robo.mouseMove(mouseLoc.x+x, mouseLoc.y+y);
    mouseActLoc.setLocation(mouseLoc.x+x,mouseLoc.y+y);
    break;

Anmerkung:

Ich habe lange überlegt, wie ich den Courser anhand welcher Koordinaten bewegen könnte. Das Problem ist die ANzahl an Möglichkeiten. Die einfachste wäre es, die Koordinaten des Fingers auf dem Handy direkt zu übergeben. Durch den Auflösungsunterschied würde nun aber der Courser auf dem PC nur in dem Radius der Smartphonedisplaymaßen bewegt werden. Man könnte also statt mit den exakten Pixelwerten mit der Position im Maßstab zum Display arbeiten.

Nächstes Problem: Der Courser würde immer bei neuer Bewegung springen. Gemeint ist, dass die neuen Positionen in Abhängigkeit zum Finger stehen und nicht zu dem Punkt, an dem der Courser zuletzt war. Ziel ist es also, die Abstände, welche der Finger zu seiner Startposition hat, an die letzte Courserposition anzuknüpfen.

Lange Rede, kurzer Sinn: es gibt viele Möglichkeiten, den Courser zu bewegen aber nur die angegebene entspricht einem „richtigen“ TouchPad.

Last but not least steht uns noch das Ende der Bewegung bevor.

case "u": mouseLoc.x = mouseActLoc.x;
    mouseLoc.y = mouseActLoc.y;
    break;

Hier wird einfach die letzte aktuelle Position des Coursers an den anderen Point übergeben. Natürlich wäre es simpler, bei Beginn der Bewegung die aktuelle Courserposition über

Point location = info.getLocation();

abzufragen aber das hat bei mir nicht funktioniert. Warum auch immer.

Downloads

Wie immer stelle ich das gesamte Projekt zum Download und Nachvollziehen zur Verfügung.

Und ebenfalls wie immer würde ich mich über Rückmeldungen und Fragen freuen!

Schreibe einen Kommentar

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s