diff --git a/app/app.iml b/app/app.iml index 1b595178..f7cfabed 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,5 +1,5 @@ - + @@ -103,18 +103,18 @@ - + - - - + + + diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 9e1ecbd4..661d60d6 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -8,6 +8,7 @@ import com.limelight.binding.input.KeyboardTranslator; import com.limelight.binding.input.TouchContext; import com.limelight.binding.input.evdev.EvdevListener; import com.limelight.binding.input.evdev.EvdevWatcher; +import com.limelight.binding.input.virtual_controller.VirtualController; import com.limelight.binding.video.ConfigurableDecoderRenderer; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnectionListener; @@ -41,6 +42,7 @@ import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; +import android.widget.FrameLayout; import android.widget.Toast; @@ -56,6 +58,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, private TouchContext[] touchContextMap = new TouchContext[2]; private ControllerHandler controllerHandler; + private VirtualController virtualController; private KeyboardTranslator keybTranslator; private PreferenceConfiguration prefConfig; @@ -197,7 +200,14 @@ public class Game extends Activity implements SurfaceHolder.Callback, evdevWatcher = new EvdevWatcher(this); evdevWatcher.start(); } - + + if (prefConfig.virtualController_enable) + { + FrameLayout frameLayout = (FrameLayout) findViewById(R.id.frameLayout); + + virtualController = new VirtualController(conn, frameLayout, getApplicationContext(), getWindowManager()); + } + // The connection will be started when the surface gets created sh.addCallback(this); } diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/AnalogStick.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/AnalogStick.java new file mode 100644 index 00000000..4a3ac283 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/AnalogStick.java @@ -0,0 +1,298 @@ +package com.limelight.binding.input.virtual_controller; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.view.MotionEvent; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Karim Mreisi on 30.11.2014. + */ +public class AnalogStick extends View +{ + private enum _STICK_STATE + { + NO_MOVEMENT, + MOVED + } + + private static final boolean _PRINT_DEBUG_INFORMATION = false; + + public interface AnalogStickListener + { + void onMovement(float x, float y); + } + + public void addAnalogStickListener (AnalogStickListener listener) + { + listeners.add(listener); + } + + private static final void _DBG(String text) + { + if (_PRINT_DEBUG_INFORMATION) + { + System.out.println("AnalogStick: " + text); + } + } + + float radius_complete = 0; + float radius_dead_zone = 0; + float radius_analog_stick = 0; + + float position_stick_x = 0; + float position_stick_y = 0; + + boolean pressed = false; + _STICK_STATE stick_state = _STICK_STATE.NO_MOVEMENT; + + List listeners = new ArrayList(); + + public AnalogStick(Context context) + { + super(context); + + position_stick_x = getWidth() / 2; + position_stick_y = getHeight() / 2; + + stick_state = _STICK_STATE.NO_MOVEMENT; + pressed = false; + + } + + private float getPercent(float value, int percent) + { + return value / 100 * percent; + } + + private int getCorrectWidth() + { + return getWidth() > getHeight() ? getHeight() : getWidth(); + } + + private double getMovementRadius(float x, float y) + { + if (x == 0) + { + return y > 0 ? y : -y; + } + + if (y == 0) + { + return x > 0 ? x : -x; + } + + return Math.sqrt(x * x + y * y); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) + { + radius_complete = getPercent(getCorrectWidth() / 2, 90); + radius_dead_zone = getPercent(getCorrectWidth() / 2, 10); + radius_analog_stick = getPercent(getCorrectWidth() / 2, 20); + + super.onSizeChanged(w, h, oldw, oldh); + } + + @Override + protected void onDraw(Canvas canvas) + { + Paint paint = new Paint(); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(getPercent(getCorrectWidth() / 2, 2)); + + paint.setColor(Color.YELLOW); + + // draw outer circle + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint); + + // draw dead zone + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint); + + // draw stick depending on state (no movement, moved, active(out of dead zone)) + if (pressed) + { + switch (stick_state) + { + case NO_MOVEMENT: + { + paint.setColor(Color.BLUE); + canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint); + + break; + } + case MOVED: + { + paint.setColor(Color.CYAN); + canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint); + + break; + } + } + } + else + { + paint.setColor(Color.RED); + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint); + } + // set transparent background + canvas.drawColor(Color.TRANSPARENT); + + super.onDraw(canvas); + } + + private double getAngle(float way_x, float way_y) + { + double angle = 0; + + // prevent divisions by zero + if (way_x == 0) + { + if (way_y > 0) + { + angle = 0; + } + else if (way_y < 0) + { + angle = Math.PI; + } + } + else if (way_y == 0) + { + if (way_x > 0) + { + angle = Math.PI * 3/2; + } + else if (way_x < 0) + { + angle = Math.PI * 1/2; + } + } + else + { + if (way_x > 0) + { + if (way_y < 0) + { // first quadrant + angle = 3 * Math.PI / 2 + Math.atan((double)(-way_y / way_x)); + } + else + { // second quadrant + angle = Math.PI + Math.atan((double)(way_x / way_y)); + } + } + else + { + if (way_y > 0) + { // third quadrant + angle = Math.PI / 2 + Math.atan((double)(way_y / -way_x)); + } + else + { // fourth quadrant + angle = 0 + Math.atan((double) (-way_x / -way_y)); + } + } + } + + _DBG("angle: " + angle + " way y: "+ way_y + " way x: " + way_x); + + return angle; + } + + private void moveActionCallback(float x, float y) + { + _DBG("movement x: " + x + " movement y: " + y); + + // notify listeners + for (AnalogStickListener listener : listeners) + { + listener.onMovement(x, y); + } + } + + private void updatePosition(float x, float y) + { + float way_x = -(getWidth() / 2 - x); + float way_y = -(getHeight() / 2 - y); + + float movement_x = 0; + float movement_y = 0; + + double movement_radius = getMovementRadius(way_x, way_y); + double movement_angle = getAngle(way_x, way_y); + + // chop radius if out of outer circle + if (movement_radius > (radius_complete - radius_analog_stick)) + { + movement_radius = radius_complete - radius_analog_stick; + } + + float correlated_y = (float)(Math.sin(Math.PI / 2 - movement_angle) * (movement_radius)); + float correlated_x = (float)(Math.cos(Math.PI / 2 - movement_angle) * (movement_radius)); + + float complete = (radius_complete - radius_analog_stick); + + movement_x = -(1 / complete) * correlated_x; + movement_y = (1 / complete) * correlated_y; + + position_stick_x = getWidth() / 2 - correlated_x; + position_stick_y = getHeight() / 2 - correlated_y; + + // check if analog stick is inside of dead zone + if (movement_radius > radius_dead_zone) + { + moveActionCallback(movement_x, movement_y); + + stick_state = _STICK_STATE.MOVED; + } + else + { + stick_state = _STICK_STATE.NO_MOVEMENT; + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) + { + // get masked (not specific to a pointer) action + int action = event.getActionMasked(); + + switch (action) + { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_MOVE: + { + pressed = true; + + break; + } + default: + { + pressed = false; + + break; + } + } + + if (pressed) + { // when is pressed calculate new positions (will trigger movement if necessary) + updatePosition(event.getX(), event.getY()); + } + else + { // no longer pressed reset movement + moveActionCallback(0, 0); + } + + // to get view refreshed + invalidate(); + + return true; + } +} diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualController.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualController.java new file mode 100644 index 00000000..08057566 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualController.java @@ -0,0 +1,427 @@ +package com.limelight.binding.input.virtual_controller; + +import android.animation.LayoutTransition; +import android.app.ActionBar; +import android.content.Context; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import com.limelight.nvstream.NvConnection; +import com.limelight.nvstream.input.ControllerPacket; + +/** + * Created by Karim Mreisi on 30.11.2014. + */ +public class VirtualController +{ + private static final boolean _PRINT_DEBUG_INFORMATION = false; + + private static final void _DBG(String text) + { + if (_PRINT_DEBUG_INFORMATION) + { + System.out.println("VirtualController: " + text); + } + } + private short inputMap = 0x0000; + private byte leftTrigger = 0x00; + private byte rightTrigger = 0x00; + private short rightStickX = 0x0000; + private short rightStickY = 0x0000; + private short leftStickX = 0x0000; + private short leftStickY = 0x0000; + + private FrameLayout frame_layout = null; + private RelativeLayout relative_layout = null; + + private RelativeLayout.LayoutParams layoutParamsButtonDPadLeft = null; + private RelativeLayout.LayoutParams layoutParamsButtonDPadRight = null; + private RelativeLayout.LayoutParams layoutParamsButtonDPadUp = null; + private RelativeLayout.LayoutParams layoutParamsButtonDPadDown = null; + + private RelativeLayout.LayoutParams layoutParamsButtonA = null; + private RelativeLayout.LayoutParams layoutParamsButtonB = null; + private RelativeLayout.LayoutParams layoutParamsButtonX = null; + private RelativeLayout.LayoutParams layoutParamsButtonY = null; + private RelativeLayout.LayoutParams layoutParamsButtonR1 = null; + private RelativeLayout.LayoutParams layoutParamsButtonR2 = null; + + private RelativeLayout.LayoutParams layoutParamsParamsStick = null; + private RelativeLayout.LayoutParams layoutParamsParamsStick2 = null; + + private Button buttonStart = null; + private Button buttonSelect = null; + private Button buttonESC = null; + + private Button buttonDPadLeft = null; + private Button buttonDPadRight = null; + private Button buttonDPadUp = null; + private Button buttonDPadDown = null; + + private Button buttonA = null; + private Button buttonB = null; + private Button buttonX = null; + private Button buttonY = null; + private Button buttonR1 = null; + private Button buttonR2 = null; + + private AnalogStick stick = null; + private AnalogStick stick2 = null; + + NvConnection connection = null; + + private int getPercentageV(int percent) + { + return (int)(((float)frame_layout.getHeight() / (float)100) * (float)percent); + } + + private int getPercentageH(int percent) + { + return (int)(((float)frame_layout.getWidth() / (float)100) * (float)percent); + } + + private void setPercentilePosition(RelativeLayout.LayoutParams parm, int pos_x, int pos_y) + { + parm.setMargins( + (int)(((float)frame_layout.getWidth() / (float)100 * (float)pos_x) - ((float)parm.width / (float)2)), + (int)(((float)frame_layout.getHeight() / (float)100 * (float)pos_y) - ((float)parm.height / (float)2)), + 0, + 0 + ); + } + + private void onButtonTouchEvent(View v, MotionEvent event, short key) + { + // get masked (not specific to a pointer) action + int action = event.getActionMasked(); + + switch (action) + { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + { + inputMap |= key; + + sendControllerInputPacket(); + + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + { + inputMap &= ~key; + + sendControllerInputPacket(); + + break; + } + } + } + + void refreshLayout() + { + relative_layout.removeAllViews(); + + layoutParamsButtonDPadLeft = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonDPadRight = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonDPadUp = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonDPadDown = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + + layoutParamsParamsStick = new RelativeLayout.LayoutParams(getPercentageV(40), getPercentageV(40)); + layoutParamsParamsStick2 = new RelativeLayout.LayoutParams(getPercentageV(40), getPercentageV(40)); + + layoutParamsButtonA = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonB = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonX = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonY = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonR1 = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + layoutParamsButtonR2 = new RelativeLayout.LayoutParams(getPercentageV(10), getPercentageV(10)); + + setPercentilePosition(layoutParamsButtonDPadLeft, 5, 45); + setPercentilePosition(layoutParamsButtonDPadRight, 15, 45); + setPercentilePosition(layoutParamsButtonDPadUp, 10, 35); + setPercentilePosition(layoutParamsButtonDPadDown, 10, 55); + + setPercentilePosition(layoutParamsParamsStick, 22, 78); + setPercentilePosition(layoutParamsParamsStick2, 78, 78); + + setPercentilePosition(layoutParamsButtonA, 85, 52); + setPercentilePosition(layoutParamsButtonB, 92, 47); + setPercentilePosition(layoutParamsButtonX, 85, 40); + setPercentilePosition(layoutParamsButtonY, 92, 35); + + setPercentilePosition(layoutParamsButtonR1, 95, 68); + setPercentilePosition(layoutParamsButtonR2, 95, 80); + + relative_layout.addView(buttonDPadLeft, layoutParamsButtonDPadLeft); + relative_layout.addView(buttonDPadRight, layoutParamsButtonDPadRight); + relative_layout.addView(buttonDPadUp, layoutParamsButtonDPadUp); + relative_layout.addView(buttonDPadDown, layoutParamsButtonDPadDown); + + relative_layout.addView(stick, layoutParamsParamsStick); + relative_layout.addView(stick2, layoutParamsParamsStick2); + relative_layout.addView(buttonA, layoutParamsButtonA); + relative_layout.addView(buttonB, layoutParamsButtonB); + relative_layout.addView(buttonX, layoutParamsButtonX); + relative_layout.addView(buttonY, layoutParamsButtonY); + relative_layout.addView(buttonR1, layoutParamsButtonR1); + relative_layout.addView(buttonR2, layoutParamsButtonR2); + } + + public VirtualController(final NvConnection conn, FrameLayout layout, Context context, WindowManager window_manager) + { + this.connection = conn; + frame_layout = layout; + + relative_layout = new RelativeLayout(context); + + relative_layout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() + { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) + { + refreshLayout(); + } + }); + + frame_layout.addView(relative_layout); + + buttonDPadLeft = new Button(context); + buttonDPadLeft.setText("LF"); + buttonDPadLeft.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.LEFT_FLAG); + + return false; + } + }); + + buttonDPadRight = new Button(context); + buttonDPadRight.setText("RI"); + buttonDPadRight.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.RIGHT_FLAG); + + return false; + } + }); + + buttonDPadUp = new Button(context); + buttonDPadUp.setText("UP"); + buttonDPadUp.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.UP_FLAG); + + return false; + } + }); + + buttonDPadDown = new Button(context); + buttonDPadDown.setText("DW"); + buttonDPadDown.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.DOWN_FLAG); + + return false; + } + }); + + buttonX = new Button(context); + buttonX.setText("X"); + buttonX.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.X_FLAG); + + return false; + } + }); + + buttonY = new Button(context); + buttonY.setText("Y"); + buttonY.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.Y_FLAG); + + return false; + } + }); + + buttonA = new Button(context); + buttonA.setText("A"); + buttonA.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.A_FLAG); + + return false; + } + }); + + buttonB = new Button(context); + buttonB.setText("B"); + buttonB.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + onButtonTouchEvent(v, event, ControllerPacket.B_FLAG); + + return false; + } + }); + + buttonR1 = new Button(context); + buttonR1.setText("LT"); + buttonR1.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + // get masked (not specific to a pointer) action + int action = event.getActionMasked(); + + switch (action) + { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + { + leftTrigger = (byte)(1 * 0xFF); + + sendControllerInputPacket(); + + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + { + leftTrigger = (byte)(0 * 0xFF); + + sendControllerInputPacket(); + + break; + } + } + + return false; + } + }); + + buttonR2 = new Button(context); + buttonR2.setText("RT"); + buttonR2.setOnTouchListener(new View.OnTouchListener() + { + @Override + public boolean onTouch(View v, MotionEvent event) + { + // get masked (not specific to a pointer) action + int action = event.getActionMasked(); + + switch (action) + { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + { + rightTrigger = (byte)(1 * 0xFF); + + sendControllerInputPacket(); + + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + { + rightTrigger = (byte)(0 * 0xFF); + + sendControllerInputPacket(); + + break; + } + } + + return false; + } + }); + + stick = new AnalogStick(context); + + stick.addAnalogStickListener(new AnalogStick.AnalogStickListener() + { + @Override + public void onMovement(float x, float y) + { + leftStickX = (short) (x * 0x7FFE); + leftStickY = (short) (y * 0x7FFE); + + _DBG("LEFT STICK MOVEMENT X: "+ leftStickX + " Y: " + leftStickY); + sendControllerInputPacket(); + } + }); + + stick2 = new AnalogStick(context); + stick2.addAnalogStickListener(new AnalogStick.AnalogStickListener() + { + @Override + public void onMovement(float x, float y) + { + rightStickX = (short) (x * 0x7FFE); + rightStickY = (short) (y * 0x7FFE); + + _DBG("RIGHT STICK MOVEMENT X: "+ rightStickX + " Y: " + rightStickY); + sendControllerInputPacket(); + } + }); + + + refreshLayout(); + } + + private void sendControllerInputPacket() + { + try + { + _DBG("INPUT_MAP + " + inputMap); + _DBG("LEFT_TRIGGER " + leftTrigger); + _DBG("RIGHT_TRIGGER " + rightTrigger); + _DBG("LEFT STICK X: " + leftStickX + " Y: " + leftStickY); + _DBG("RIGHT STICK X: " + rightStickX + " Y: " + rightStickY); + _DBG("RIGHT STICK X: " + rightStickX + " Y: " + rightStickY); + + + connection.sendControllerInput(inputMap, leftTrigger, rightTrigger, + leftStickX, leftStickY, rightStickX, rightStickY); + } + catch (Exception e) + { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index 772c7032..50f0159a 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -14,6 +14,9 @@ public class PreferenceConfiguration { private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio"; private static final String DEADZONE_PREF_STRING = "seekbar_deadzone"; + private static final String VIRTUAL_CONTROLLER_ENABLE = "virtual_controller_checkbox_enable"; + private static final Boolean VIRTUAL_CONTROLLER_ENABLE_DEFAULT = true; + private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_1080_30 = 10; @@ -38,6 +41,8 @@ public class PreferenceConfiguration { public int deadzonePercentage; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; + public boolean virtualController_enable; + public static int getDefaultBitrate(String resFpsString) { if (resFpsString.equals("720p30")) { return BITRATE_DEFAULT_720_30; @@ -141,6 +146,8 @@ public class PreferenceConfiguration { config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); + config.virtualController_enable = prefs.getBoolean(VIRTUAL_CONTROLLER_ENABLE, VIRTUAL_CONTROLLER_ENABLE_DEFAULT); + return config; } } diff --git a/app/src/main/res/layout/activity_game.xml b/app/src/main/res/layout/activity_game.xml index 7f625494..c8e6006a 100644 --- a/app/src/main/res/layout/activity_game.xml +++ b/app/src/main/res/layout/activity_game.xml @@ -1,5 +1,6 @@ diff --git a/limelight_vc.iml b/limelight_vc.iml new file mode 100644 index 00000000..2a022014 --- /dev/null +++ b/limelight_vc.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + +