Added Joy stick classes and resources

Signed-off-by: Ricky Barrette <rickbarrette@gmail.com>
This commit is contained in:
2012-12-11 11:47:12 -05:00
parent 2c30e39eaa
commit b10f5d1c85
10 changed files with 817 additions and 3 deletions

View File

@@ -0,0 +1,156 @@
package com.MobileAnarchy.Android.Widgets.Joystick;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
public class DualJoystickView extends LinearLayout {
@SuppressWarnings("unused")
private static final String TAG = DualJoystickView.class.getSimpleName();
private final boolean D = false;
private Paint dbgPaint1;
private JoystickView stickL;
private JoystickView stickR;
private View pad;
public DualJoystickView(Context context) {
super(context);
stickL = new JoystickView(context);
stickR = new JoystickView(context);
initDualJoystickView();
}
public DualJoystickView(Context context, AttributeSet attrs) {
super(context, attrs);
stickL = new JoystickView(context, attrs);
stickR = new JoystickView(context, attrs);
initDualJoystickView();
}
private void initDualJoystickView() {
setOrientation(LinearLayout.HORIZONTAL);
if (D) {
dbgPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
dbgPaint1.setColor(Color.CYAN);
dbgPaint1.setStrokeWidth(1);
dbgPaint1.setStyle(Paint.Style.STROKE);
}
pad = new View(getContext());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
removeView(stickL);
removeView(stickR);
float padW = getMeasuredWidth() - (getMeasuredHeight() * 2);
int joyWidth = (int) ((getMeasuredWidth() - padW) / 2);
LayoutParams joyLParams = new LayoutParams(joyWidth,
getMeasuredHeight());
stickL.setLayoutParams(joyLParams);
stickR.setLayoutParams(joyLParams);
stickL.TAG = "L";
stickR.TAG = "R";
stickL.setPointerId(JoystickView.INVALID_POINTER_ID);
stickR.setPointerId(JoystickView.INVALID_POINTER_ID);
addView(stickL);
ViewGroup.LayoutParams padLParams = new ViewGroup.LayoutParams(
(int) padW, getMeasuredHeight());
removeView(pad);
pad.setLayoutParams(padLParams);
addView(pad);
addView(stickR);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
stickR.setTouchOffset(stickR.getLeft(), stickR.getTop());
}
public void setAutoReturnToCenter(boolean left, boolean right) {
stickL.setAutoReturnToCenter(left);
stickR.setAutoReturnToCenter(right);
}
public void setOnJostickMovedListener(JoystickMovedListener left,
JoystickMovedListener right) {
stickL.setOnJostickMovedListener(left);
stickR.setOnJostickMovedListener(right);
}
public void setOnJostickClickedListener(JoystickClickedListener left,
JoystickClickedListener right) {
stickL.setOnJostickClickedListener(left);
stickR.setOnJostickClickedListener(right);
}
public void setYAxisInverted(boolean leftYAxisInverted,
boolean rightYAxisInverted) {
stickL.setYAxisInverted(leftYAxisInverted);
stickL.setYAxisInverted(rightYAxisInverted);
}
public void setMovementConstraint(int movementConstraint) {
stickL.setMovementConstraint(movementConstraint);
stickR.setMovementConstraint(movementConstraint);
}
public void setMovementRange(float movementRangeLeft,
float movementRangeRight) {
stickL.setMovementRange(movementRangeLeft);
stickR.setMovementRange(movementRangeRight);
}
public void setMoveResolution(float leftMoveResolution,
float rightMoveResolution) {
stickL.setMoveResolution(leftMoveResolution);
stickR.setMoveResolution(rightMoveResolution);
}
public void setUserCoordinateSystem(int leftCoordinateSystem,
int rightCoordinateSystem) {
stickL.setUserCoordinateSystem(leftCoordinateSystem);
stickR.setUserCoordinateSystem(rightCoordinateSystem);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (D) {
canvas.drawRect(1, 1, getMeasuredWidth() - 1,
getMeasuredHeight() - 1, dbgPaint1);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean l = stickL.dispatchTouchEvent(ev);
boolean r = stickR.dispatchTouchEvent(ev);
return l || r;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean l = stickL.onTouchEvent(ev);
boolean r = stickR.onTouchEvent(ev);
return l || r;
}
}

View File

@@ -0,0 +1,7 @@
package com.MobileAnarchy.Android.Widgets.Joystick;
public interface JoystickClickedListener {
public void OnClicked();
public void OnReleased();
}

View File

@@ -0,0 +1,9 @@
package com.MobileAnarchy.Android.Widgets.Joystick;
public interface JoystickMovedListener {
public void OnMoved(int pan, int tilt);
public void OnReleased();
public void OnReturnedToCenter();
}

View File

@@ -0,0 +1,542 @@
package com.MobileAnarchy.Android.Widgets.Joystick;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
public class JoystickView extends View {
public static final int INVALID_POINTER_ID = -1;
// =========================================
// Private Members
// =========================================
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;
// 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;
// =========================================
// Constructors
// =========================================
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();
}
// =========================================
// Initialization
// =========================================
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.setColor(Color.GRAY);
bgPaint.setStrokeWidth(1);
bgPaint.setAlpha(80);
bgPaint.setStyle(Paint.Style.FILL_AND_STROKE);
handlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
handlePaint.setColor(Color.DKGRAY);
handlePaint.setStrokeWidth(1);
handlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
innerPadding = 10;
setMovementRange(10);
setMoveResolution(1.0f);
setClickThreshold(0.4f);
setYAxisInverted(true);
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 Methods
// =========================================
public void setOnJostickMovedListener(JoystickMovedListener listener) {
this.moveListener = listener;
}
public void setOnJostickClickedListener(JoystickClickedListener listener) {
this.clickListener = listener;
}
// =========================================
// Drawing Functionality
// =========================================
@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;
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;
}
}