//package com.MobileAnarchy.Android.Widgets.Joystick;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
interface JoystickClickedListener {
public void OnClicked();
public void OnReleased();
}
public class JoystickView extends View {
public static final int INVALID_POINTER_ID = -1;
private final boolean D = false;
String TAG = "JoystickView";
private Paint dbgPaint1;
private Paint dbgPaint2;
private Paint bgPaint;
private Paint handlePaint;
private int innerPadding;
private int bgRadius;
private int handleRadius;
private int movementRadius;
private int handleInnerBoundaries;
private JoystickMovedListener moveListener;
private JoystickClickedListener clickListener;
//# of pixels movement required between reporting to the listener
private float moveResolution;
private boolean yAxisInverted;
private boolean autoReturnToCenter;
//Max range of movement in user coordinate system
public final static int CONSTRAIN_BOX = 0;
public final static int CONSTRAIN_CIRCLE = 1;
private int movementConstraint;
private float movementRange;
public final static int COORDINATE_CARTESIAN = 0; //Regular cartesian coordinates
public final static int COORDINATE_DIFFERENTIAL = 1; //Uses polar rotation of 45 degrees to calc differential drive paramaters
private int userCoordinateSystem;
//Records touch pressure for click handling
private float touchPressure;
private boolean clicked;
private float clickThreshold;
//Last touch point in view coordinates
private int pointerId = INVALID_POINTER_ID;
private float touchX, touchY;
//Last reported position in view coordinates (allows different reporting sensitivities)
private float reportX, reportY;
//Handle center in view coordinates
private float handleX, handleY;
//Center of the view in view coordinates
private int cX, cY;
//Size of the view in view coordinates
private int dimX, dimY;
//Cartesian coordinates of last touch point - joystick center is (0,0)
private int cartX, cartY;
//Polar coordinates of the touch point from joystick center
private double radial;
private double angle;
//User coordinates of last touch point
private int userX, userY;
//Offset co-ordinates (used when touch events are received from parent's coordinate origin)
private int offsetX;
private int offsetY;
public JoystickView(Context context) {
super(context);
initJoystickView();
}
public JoystickView(Context context, AttributeSet attrs) {
super(context, attrs);
initJoystickView();
}
public JoystickView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initJoystickView();
}
private void initJoystickView() {
setFocusable(true);
dbgPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
dbgPaint1.setColor(Color.RED);
dbgPaint1.setStrokeWidth(1);
dbgPaint1.setStyle(Paint.Style.STROKE);
dbgPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
dbgPaint2.setColor(Color.GREEN);
dbgPaint2.setStrokeWidth(1);
dbgPaint2.setStyle(Paint.Style.STROKE);
bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setStyle(Paint.Style.FILL);
handlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
handlePaint.setColor(Color.rgb(8, 8, 8));
handlePaint.setStrokeWidth(1);
handlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
innerPadding = 10;
setMovementRange(150);
setMoveResolution(0.1f);
setClickThreshold(0.4f);
setYAxisInverted(false);
setUserCoordinateSystem(COORDINATE_CARTESIAN);
setAutoReturnToCenter(true);
}
public void setAutoReturnToCenter(boolean autoReturnToCenter) {
this.autoReturnToCenter = autoReturnToCenter;
}
public boolean isAutoReturnToCenter() {
return autoReturnToCenter;
}
public void setUserCoordinateSystem(int userCoordinateSystem) {
if (userCoordinateSystem < COORDINATE_CARTESIAN || movementConstraint > COORDINATE_DIFFERENTIAL)
Log.e(TAG, "invalid value for userCoordinateSystem");
else
this.userCoordinateSystem = userCoordinateSystem;
}
public int getUserCoordinateSystem() {
return userCoordinateSystem;
}
public void setMovementConstraint(int movementConstraint) {
if (movementConstraint < CONSTRAIN_BOX || movementConstraint > CONSTRAIN_CIRCLE)
Log.e(TAG, "invalid value for movementConstraint");
else
this.movementConstraint = movementConstraint;
}
public int getMovementConstraint() {
return movementConstraint;
}
public boolean isYAxisInverted() {
return yAxisInverted;
}
public void setYAxisInverted(boolean yAxisInverted) {
this.yAxisInverted = yAxisInverted;
}
/**
* Set the pressure sensitivity for registering a click
* @param clickThreshold threshold 0...1.0f inclusive. 0 will cause clicks to never be reported, 1.0 is a very hard click
*/
public void setClickThreshold(float clickThreshold) {
if (clickThreshold < 0 || clickThreshold > 1.0f)
Log.e(TAG, "clickThreshold must range from 0...1.0f inclusive");
else
this.clickThreshold = clickThreshold;
}
public float getClickThreshold() {
return clickThreshold;
}
public void setMovementRange(float movementRange) {
this.movementRange = movementRange;
}
public float getMovementRange() {
return movementRange;
}
public void setMoveResolution(float moveResolution) {
this.moveResolution = moveResolution;
}
public float getMoveResolution() {
return moveResolution;
}
public void setOnJostickMovedListener(JoystickMovedListener listener) {
this.moveListener = listener;
}
public void setOnJostickClickedListener(JoystickClickedListener listener) {
this.clickListener = listener;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Here we make sure that we have a perfect circle
int measuredWidth = measure(widthMeasureSpec);
int measuredHeight = measure(heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int d = Math.min(getMeasuredWidth(), getMeasuredHeight());
dimX = d;
dimY = d;
cX = d / 2;
cY = d / 2;
bgRadius = dimX/2 - innerPadding;
// Texturize the back of the joysticks
Bitmap backTexture = null;//BitmapFactory.decodeResource(getResources(), R.drawable.joy_back);
backTexture = Bitmap.createScaledBitmap(backTexture, dimX, dimY, true);
BitmapShader backShader = new BitmapShader(backTexture, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
bgPaint.setShader(backShader);
handleRadius = (int)(d * 0.25);
handleInnerBoundaries = handleRadius;
movementRadius = Math.min(cX, cY) - handleInnerBoundaries;
}
private int measure(int measureSpec) {
int result = 0;
// Decode the measurement specifications.
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.UNSPECIFIED) {
// Return a default size of 200 if no bounds are specified.
result = 200;
} else {
// As you want to fill the available space
// always return the full available bounds.
result = specSize;
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
// Draw the background
canvas.drawCircle(cX, cY, bgRadius, bgPaint);
// Draw the handle
handleX = touchX + cX;
handleY = touchY + cY;
canvas.drawCircle(handleX, handleY, handleRadius, handlePaint);
if (D) {
canvas.drawRect(1, 1, getMeasuredWidth()-1, getMeasuredHeight()-1, dbgPaint1);
canvas.drawCircle(handleX, handleY, 3, dbgPaint1);
if ( movementConstraint == CONSTRAIN_CIRCLE ) {
canvas.drawCircle(cX, cY, this.movementRadius, dbgPaint1);
}
else {
canvas.drawRect(cX-movementRadius, cY-movementRadius, cX+movementRadius, cY+movementRadius, dbgPaint1);
}
//Origin to touch point
canvas.drawLine(cX, cY, handleX, handleY, dbgPaint2);
int baseY = (int) (touchY < 0 ? cY + handleRadius : cY - handleRadius);
canvas.drawText(String.format("%s (%.0f,%.0f)", TAG, touchX, touchY), handleX-20, baseY-7, dbgPaint2);
canvas.drawText("("+ String.format("%.0f, %.1f", radial, angle * 57.2957795) + (char) 0x00B0 + ")", handleX-20, baseY+15, dbgPaint2);
}
// Log.d(TAG, String.format("touch(%f,%f)", touchX, touchY));
// Log.d(TAG, String.format("onDraw(%.1f,%.1f)\n\n", handleX, handleY));
canvas.restore();
}
// Constrain touch within a box
private void constrainBox() {
touchX = Math.max(Math.min(touchX, movementRadius), -movementRadius);
touchY = Math.max(Math.min(touchY, movementRadius), -movementRadius);
}
// Constrain touch within a circle
private void constrainCircle() {
float diffX = touchX;
float diffY = touchY;
double radial = Math.sqrt((diffX*diffX) + (diffY*diffY));
if ( radial > movementRadius ) {
touchX = (int)((diffX / radial) * movementRadius);
touchY = (int)((diffY / radial) * movementRadius);
}
}
public void setPointerId(int id) {
this.pointerId = id;
}
public int getPointerId() {
return pointerId;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
return processMoveEvent(ev);
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
if ( pointerId != INVALID_POINTER_ID ) {
// Log.d(TAG, "ACTION_UP");
returnHandleToCenter();
setPointerId(INVALID_POINTER_ID);
}
break;
}
case MotionEvent.ACTION_POINTER_UP: {
if ( pointerId != INVALID_POINTER_ID ) {
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if ( pointerId == this.pointerId ) {
// Log.d(TAG, "ACTION_POINTER_UP: " + pointerId);
returnHandleToCenter();
setPointerId(INVALID_POINTER_ID);
return true;
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
if ( pointerId == INVALID_POINTER_ID ) {
int x = (int) ev.getX();
if ( x >= offsetX && x < offsetX + dimX ) {
setPointerId(ev.getPointerId(0));
// Log.d(TAG, "ACTION_DOWN: " + getPointerId());
return true;
}
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
if ( pointerId == INVALID_POINTER_ID ) {
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
int x = (int) ev.getX(pointerId);
if ( x >= offsetX && x < offsetX + dimX ) {
// Log.d(TAG, "ACTION_POINTER_DOWN: " + pointerId);
setPointerId(pointerId);
return true;
}
}
break;
}
}
return false;
}
private boolean processMoveEvent(MotionEvent ev) {
if ( pointerId != INVALID_POINTER_ID ) {
final int pointerIndex = ev.findPointerIndex(pointerId);
// Translate touch position to center of view
float x = ev.getX(pointerIndex);
touchX = x - cX - offsetX;
float y = ev.getY(pointerIndex);
touchY = y - cY - offsetY;
// Log.d(TAG, String.format("ACTION_MOVE: (%03.0f, %03.0f) => (%03.0f, %03.0f)", x, y, touchX, touchY));
reportOnMoved();
invalidate();
touchPressure = ev.getPressure(pointerIndex);
reportOnPressure();
return true;
}
return false;
}
private void reportOnMoved() {
if ( movementConstraint == CONSTRAIN_CIRCLE )
constrainCircle();
else
constrainBox();
calcUserCoordinates();
if (moveListener != null) {
boolean rx = Math.abs(touchX - reportX) >= moveResolution;
boolean ry = Math.abs(touchY - reportY) >= moveResolution;
if (rx || ry) {
this.reportX = touchX;
this.reportY = touchY;
// Log.d(TAG, String.format("moveListener.OnMoved(%d,%d)", (int)userX, (int)userY));
moveListener.OnMoved(userX, userY);
}
}
}
private void calcUserCoordinates() {
//First convert to cartesian coordinates
cartX = (int)(touchX / movementRadius * movementRange);
cartY = (int)(touchY / movementRadius * movementRange);
radial = Math.sqrt((cartX*cartX) + (cartY*cartY));
angle = Math.atan2(cartY, cartX);
//Invert Y axis if requested
if ( !yAxisInverted )
cartY *= -1;
if ( userCoordinateSystem == COORDINATE_CARTESIAN ) {
userX = cartX;
userY = cartY;
}
else if ( userCoordinateSystem == COORDINATE_DIFFERENTIAL ) {
userX = cartY + cartX / 4;
userY = cartY - cartX / 4;
if ( userX < -movementRange )
userX = (int)-movementRange;
if ( userX > movementRange )
userX = (int)movementRange;
if ( userY < -movementRange )
userY = (int)-movementRange;
if ( userY > movementRange )
userY = (int)movementRange;
}
}
//Simple pressure click
private void reportOnPressure() {
// Log.d(TAG, String.format("touchPressure=%.2f", this.touchPressure));
if ( clickListener != null ) {
if ( clicked && touchPressure < clickThreshold ) {
clickListener.OnReleased();
this.clicked = false;
// Log.d(TAG, "reset click");
invalidate();
}
else if ( !clicked && touchPressure >= clickThreshold ) {
clicked = true;
clickListener.OnClicked();
// Log.d(TAG, "click");
invalidate();
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
}
}
private void returnHandleToCenter() {
if ( autoReturnToCenter ) {
final int numberOfFrames = 5;
final double intervalsX = (0 - touchX) / numberOfFrames;
final double intervalsY = (0 - touchY) / numberOfFrames;
for (int i = 0; i < numberOfFrames; i++) {
final int j = i;
postDelayed(new Runnable() {
@Override
public void run() {
touchX += intervalsX;
touchY += intervalsY;
reportOnMoved();
invalidate();
if (moveListener != null && j == numberOfFrames - 1) {
moveListener.OnReturnedToCenter();
}
}
}, i * 40);
}
if (moveListener != null) {
moveListener.OnReleased();
}
}
}
public void setTouchOffset(int x, int y) {
offsetX = x;
offsetY = y;
}
}
interface JoystickMovedListener {
public void OnMoved(int pan, int tilt);
public void OnReleased();
public void OnReturnedToCenter();
}