From bf9343532241536197d93031cff7d38af18d93bd Mon Sep 17 00:00:00 2001 From: James Cotton Date: Sat, 4 Aug 2012 21:00:54 -0500 Subject: [PATCH] Check in a joystick gadget class --- .../Android/Widgets/DockPanel/DockPanel.java | 319 +++++++++++ .../Widgets/DockPanel/DockPosition.java | 5 + .../DragAndDrop/DragAndDropManager.java | 124 +++++ .../Widgets/DragAndDrop/DragSurface.java | 94 ++++ .../Widgets/DragAndDrop/DraggableItem.java | 44 ++ .../DragAndDrop/DraggableViewsFactory.java | 19 + .../Android/Widgets/DragAndDrop/DropZone.java | 67 +++ .../DragAndDrop/DropZoneEventsListener.java | 9 + .../Widgets/Joystick/DualJoystickView.java | 147 +++++ .../Joystick/JoystickClickedListener.java | 6 + .../Joystick/JoystickMovedListener.java | 7 + .../Widgets/Joystick/JoystickView.java | 521 ++++++++++++++++++ .../ThresholdEditText/ThresholdEditText.java | 169 ++++++ .../ThresholdTextChanged.java | 7 + .../Widgets/TilesLayout/SingleTileLayout.java | 68 +++ .../Widgets/TilesLayout/TilePosition.java | 63 +++ .../Widgets/TilesLayout/TilesLayout.java | 299 ++++++++++ .../TilesLayout/TilesLayoutPreset.java | 157 ++++++ 18 files changed, 2125 insertions(+) create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPanel.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPosition.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragAndDropManager.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragSurface.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableItem.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableViewsFactory.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZone.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZoneEventsListener.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/DualJoystickView.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickClickedListener.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickMovedListener.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickView.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdEditText.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdTextChanged.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/SingleTileLayout.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilePosition.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayout.java create mode 100644 androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayoutPreset.java diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPanel.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPanel.java new file mode 100644 index 000000000..e2397394e --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPanel.java @@ -0,0 +1,319 @@ +package com.MobileAnarchy.Android.Widgets.DockPanel; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.TranslateAnimation; +import android.view.animation.Animation.AnimationListener; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +public class DockPanel extends LinearLayout { + + // ========================================= + // Private members + // ========================================= + + private static final String TAG = "DockPanel"; + private DockPosition position; + private int contentLayoutId; + private int handleButtonDrawableId; + private Boolean isOpen; + private Boolean animationRunning; + private FrameLayout contentPlaceHolder; + private ImageButton toggleButton; + private int animationDuration; + + // ========================================= + // Constructors + // ========================================= + + public DockPanel(Context context, int contentLayoutId, + int handleButtonDrawableId, Boolean isOpen) { + super(context); + + this.contentLayoutId = contentLayoutId; + this.handleButtonDrawableId = handleButtonDrawableId; + this.isOpen = isOpen; + + Init(null); + } + + public DockPanel(Context context, AttributeSet attrs) { + super(context, attrs); + + // to prevent from crashing the designer + try { + Init(attrs); + } catch (Exception ex) { + } + } + + // ========================================= + // Initialization + // ========================================= + + private void Init(AttributeSet attrs) { + setDefaultValues(attrs); + + createHandleToggleButton(); + + // create the handle container + FrameLayout handleContainer = new FrameLayout(getContext()); + handleContainer.addView(toggleButton); + + // create and populate the panel's container, and inflate it + contentPlaceHolder = new FrameLayout(getContext()); + String infService = Context.LAYOUT_INFLATER_SERVICE; + LayoutInflater li = (LayoutInflater) getContext().getSystemService( + infService); + li.inflate(contentLayoutId, contentPlaceHolder, true); + + // setting the layout of the panel parameters according to the docking + // position + if (position == DockPosition.LEFT || position == DockPosition.RIGHT) { + handleContainer.setLayoutParams(new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.FILL_PARENT, 1)); + contentPlaceHolder.setLayoutParams(new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.FILL_PARENT, 1)); + } else { + handleContainer.setLayoutParams(new LayoutParams( + android.view.ViewGroup.LayoutParams.FILL_PARENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, 1)); + contentPlaceHolder.setLayoutParams(new LayoutParams( + android.view.ViewGroup.LayoutParams.FILL_PARENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, 1)); + } + + // adding the view to the parent layout according to docking position + if (position == DockPosition.RIGHT || position == DockPosition.BOTTOM) { + this.addView(handleContainer); + this.addView(contentPlaceHolder); + } else { + this.addView(contentPlaceHolder); + this.addView(handleContainer); + } + + if (!isOpen) { + contentPlaceHolder.setVisibility(GONE); + } + } + + private void setDefaultValues(AttributeSet attrs) { + // set default values + isOpen = true; + animationRunning = false; + animationDuration = 500; + setPosition(DockPosition.RIGHT); + + // Try to load values set by xml markup + if (attrs != null) { + String namespace = "http://com.MobileAnarchy.Android.Widgets"; + + animationDuration = attrs.getAttributeIntValue(namespace, + "animationDuration", 500); + contentLayoutId = attrs.getAttributeResourceValue(namespace, + "contentLayoutId", 0); + handleButtonDrawableId = attrs.getAttributeResourceValue( + namespace, "handleButtonDrawableResourceId", 0); + isOpen = attrs.getAttributeBooleanValue(namespace, "isOpen", true); + + // Enums are a bit trickier (needs to be parsed) + try { + position = DockPosition.valueOf(attrs.getAttributeValue( + namespace, "dockPosition").toUpperCase()); + setPosition(position); + } catch (Exception ex) { + // Docking to the left is the default behavior + setPosition(DockPosition.LEFT); + } + } + } + + private void createHandleToggleButton() { + toggleButton = new ImageButton(getContext()); + toggleButton.setPadding(0, 0, 0, 0); + toggleButton.setLayoutParams(new FrameLayout.LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER)); + toggleButton.setBackgroundColor(Color.TRANSPARENT); + toggleButton.setImageResource(handleButtonDrawableId); + toggleButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + toggle(); + } + }); + } + + private void setPosition(DockPosition position) { + this.position = position; + switch (position) { + case TOP: + setOrientation(LinearLayout.VERTICAL); + setGravity(Gravity.TOP); + break; + case RIGHT: + setOrientation(LinearLayout.HORIZONTAL); + setGravity(Gravity.RIGHT); + break; + case BOTTOM: + setOrientation(LinearLayout.VERTICAL); + setGravity(Gravity.BOTTOM); + break; + case LEFT: + setOrientation(LinearLayout.HORIZONTAL); + setGravity(Gravity.LEFT); + break; + } + } + + // ========================================= + // Public methods + // ========================================= + + public int getAnimationDuration() { + return animationDuration; + } + + public void setAnimationDuration(int milliseconds) { + animationDuration = milliseconds; + } + + public Boolean getIsRunning() { + return animationRunning; + } + + public void open() { + if (!animationRunning) { + Log.d(TAG, "Opening..."); + + Animation animation = createShowAnimation(); + this.setAnimation(animation); + animation.start(); + + isOpen = true; + } + } + + public void close() { + if (!animationRunning) { + Log.d(TAG, "Closing..."); + + Animation animation = createHideAnimation(); + this.setAnimation(animation); + animation.start(); + isOpen = false; + } + } + + public void toggle() { + if (isOpen) { + close(); + } else { + open(); + } + } + + // ========================================= + // Private methods + // ========================================= + + private Animation createHideAnimation() { + Animation animation = null; + switch (position) { + case TOP: + animation = new TranslateAnimation(0, 0, 0, -contentPlaceHolder + .getHeight()); + break; + case RIGHT: + animation = new TranslateAnimation(0, contentPlaceHolder + .getWidth(), 0, 0); + break; + case BOTTOM: + animation = new TranslateAnimation(0, 0, 0, contentPlaceHolder + .getHeight()); + break; + case LEFT: + animation = new TranslateAnimation(0, -contentPlaceHolder + .getWidth(), 0, 0); + break; + } + + animation.setDuration(animationDuration); + animation.setInterpolator(new AccelerateInterpolator()); + animation.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + animationRunning = true; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + contentPlaceHolder.setVisibility(View.GONE); + animationRunning = false; + } + }); + return animation; + } + + private Animation createShowAnimation() { + Animation animation = null; + switch (position) { + case TOP: + animation = new TranslateAnimation(0, 0, -contentPlaceHolder + .getHeight(), 0); + break; + case RIGHT: + animation = new TranslateAnimation(contentPlaceHolder.getWidth(), + 0, 0, 0); + break; + case BOTTOM: + animation = new TranslateAnimation(0, 0, contentPlaceHolder + .getHeight(), 0); + break; + case LEFT: + animation = new TranslateAnimation(-contentPlaceHolder.getWidth(), + 0, 0, 0); + break; + } + Log.d(TAG, "Animation duration: " + animationDuration); + animation.setDuration(animationDuration); + animation.setInterpolator(new DecelerateInterpolator()); + animation.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + animationRunning = true; + contentPlaceHolder.setVisibility(View.VISIBLE); + Log.d(TAG, "\"Show\" Animation started"); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + animationRunning = false; + Log.d(TAG, "\"Show\" Animation ended"); + } + }); + return animation; + } + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPosition.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPosition.java new file mode 100644 index 000000000..59643dfc6 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DockPanel/DockPosition.java @@ -0,0 +1,5 @@ +package com.MobileAnarchy.Android.Widgets.DockPanel; + +public enum DockPosition { + TOP, BOTTOM, LEFT, RIGHT +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragAndDropManager.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragAndDropManager.java new file mode 100644 index 000000000..d7e0c5e44 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragAndDropManager.java @@ -0,0 +1,124 @@ +package com.MobileAnarchy.Android.Widgets.DragAndDrop; + +import java.util.ArrayList; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; + +public class DragAndDropManager { + + // ========================================= + // Private members + // ========================================= + + protected static final String TAG = "DragAndDropManager"; + private static DragAndDropManager instance = null; + private ArrayList dropZones; + private OnTouchListener originalTouchListener; + private DragSurface dragSurface; + private DraggableItem draggedItem; + private DropZone activeDropZone; + + // ========================================= + // Protected Constructor + // ========================================= + + protected DragAndDropManager() { + // Exists only to defeat instantiation. + dropZones = new ArrayList(); + } + + // ========================================= + // Public Properties + // ========================================= + + public static DragAndDropManager getInstance() { + if (instance == null) { + instance = new DragAndDropManager(); + } + return instance; + } + + public Context getContext() { + if (dragSurface == null) + return null; + + return dragSurface.getContext(); + } + + // ========================================= + // Public Methods + // ========================================= + + public void init(DragSurface surface) { + dragSurface = surface; + clearDropZones(); + } + + public void clearDropZones() { + dropZones.clear(); + } + + public void addDropZone(DropZone dropZone) { + dropZones.add(dropZone); + } + + + public void startDragging(OnTouchListener originalListener, DraggableItem draggedItem) { + originalTouchListener = originalListener; + this.draggedItem = draggedItem; + draggedItem.getSource().setOnTouchListener(new OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + int[] location = new int[2]; + v.getLocationOnScreen(location); + event.offsetLocation(location[0], location[1]); + invalidateDropZones((int)event.getX(), (int)event.getY()); + return dragSurface.onTouchEvent(event); + } + }); + + dragSurface.startDragging(draggedItem); + } + + + // ========================================= + // Protected Methods + // ========================================= + + protected void invalidateDropZones(int x, int y) { + if (activeDropZone != null) { + if (!activeDropZone.isOver(x, y)) { + activeDropZone.getListener().OnDragZoneLeft(activeDropZone, draggedItem); + activeDropZone = null; + } + else { + // we are still over the same drop zone, no need to check other drop zones + return; + } + } + + for (DropZone dropZone : dropZones) { + if (dropZone.isOver(x, y)) { + activeDropZone = dropZone; + dropZone.getListener().OnDragZoneEntered(activeDropZone, draggedItem); + break; + } + } + } + + protected void stoppedDragging() { + if (activeDropZone != null) { + activeDropZone.getListener().OnDropped(activeDropZone, draggedItem); + } + + // Registering the "old" listener to the view that initiated this drag session + draggedItem.getSource().setOnTouchListener(originalTouchListener); + draggedItem = null; + activeDropZone = null; + } + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragSurface.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragSurface.java new file mode 100644 index 000000000..aebb9d481 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DragSurface.java @@ -0,0 +1,94 @@ +package com.MobileAnarchy.Android.Widgets.DragAndDrop; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.widget.FrameLayout; + +public class DragSurface extends FrameLayout { + + private float draggedViewHalfHeight; + private float draggedViewHalfWidth; + private int framesCount; + + private Boolean isDragging; + private DraggableItem draggedItem; + + public DragSurface(Context context, AttributeSet attrs) { + super(context, attrs); + isDragging = false; + } + + // ========================================= + // Touch Events Listener + // ========================================= + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (isDragging && event.getAction() == MotionEvent.ACTION_UP) { + // Dragging ended + removeAllViews(); + isDragging = false; + + DragAndDropManager.getInstance().stoppedDragging(); + } + + if (isDragging && event.getAction() == MotionEvent.ACTION_MOVE) { + // Move the dragged view to it's new position + repositionView(event.getX(), event.getY()); + + // Mark this event as handled (so that other UI elements will not intercept it) + return true; + } + + return false; + } + + public void startDragging(DraggableItem draggableItem) { + this.draggedItem = draggableItem; + this.draggedItem.getDraggedView().setVisibility(INVISIBLE); + isDragging = true; + addView(this.draggedItem.getDraggedView()); + //repositionView(x, y); + framesCount = 0; + } + + private void repositionView(float x, float y) { + draggedViewHalfHeight = draggedItem.getDraggedView().getHeight() / 2f; + draggedViewHalfWidth = draggedItem.getDraggedView().getWidth() / 2f; + + // If the dragged view was not drawn yet, skip this phase + if (draggedViewHalfHeight == 0 || draggedViewHalfWidth == 0) + return; + + framesCount++; + + //Log.d(TAG, "Original = (x=" + x + ", y=" + y + ")"); + //Log.d(TAG, "Size (W=" + draggedViewHalfWidth + ", H=" + draggedViewHalfHeight + ")"); + + x = x - draggedViewHalfWidth; + y = y - draggedViewHalfHeight; + + x = Math.max(x, 0); + x = Math.min(x, getWidth() - draggedViewHalfWidth * 2); + + y = Math.max(y, 0); + y = Math.min(y, getHeight() - draggedViewHalfHeight * 2); + + //Log.d(TAG, "Moving view to (x=" + x + ", y=" + y + ")"); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, Gravity.TOP + Gravity.LEFT); + + lp.setMargins((int)x, (int)y, 0, 0); + draggedItem.getDraggedView().setLayoutParams(lp); + + // hte first couple of dragged frame's positions are not calculated correctly, + // so we have a threshold before making the dragged view visible again + if (framesCount < 2) + return; + + draggedItem.getDraggedView().setVisibility(VISIBLE); + } + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableItem.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableItem.java new file mode 100644 index 000000000..d0c8b7e25 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableItem.java @@ -0,0 +1,44 @@ +package com.MobileAnarchy.Android.Widgets.DragAndDrop; + +import android.view.View; + +public class DraggableItem { + + // ========================================= + // Private members + // ========================================= + + private View source; + private View draggedView; + private Object tag; + + // ========================================= + // Constructor + // ========================================= + + public DraggableItem(View source, View draggedItem) { + this.source = source; + this.draggedView = draggedItem; + } + + // ========================================= + // Public properties + // ========================================= + + public Object getTag() { + return tag; + } + + public void setTag(Object tag) { + this.tag = tag; + } + + public View getSource() { + return source; + } + + public View getDraggedView() { + return draggedView; + } + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableViewsFactory.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableViewsFactory.java new file mode 100644 index 000000000..246b2392f --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DraggableViewsFactory.java @@ -0,0 +1,19 @@ +package com.MobileAnarchy.Android.Widgets.DragAndDrop; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; +import android.widget.TableLayout.LayoutParams; + +public class DraggableViewsFactory { + + public static View getLabel(String text) { + Context context = DragAndDropManager.getInstance().getContext(); + TextView textView = new TextView(context); + textView.setText(text); + textView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + //textView.setGravity(Gravity.TOP + Gravity.LEFT); + return textView; + } + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZone.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZone.java new file mode 100644 index 000000000..06a8caa17 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZone.java @@ -0,0 +1,67 @@ +package com.MobileAnarchy.Android.Widgets.DragAndDrop; + +import android.view.View; + +public class DropZone { + + // ========================================= + // Private members + // ========================================= + + private View view; + private DropZoneEventsListener listener; + private int left, top, width, height; + private Boolean dimansionsCalculated; + + // ========================================= + // Constructor + // ========================================= + + public DropZone(View view, DropZoneEventsListener listener) { + this.view = view; + this.listener = listener; + dimansionsCalculated = false; + } + + // ========================================= + // Public properties + // ========================================= + + public View getView() { + return view; + } + + // ========================================= + // Public methods + // ========================================= + + public Boolean isOver(int x, int y) { + if (!dimansionsCalculated) + calculateDimensions(); + + Boolean isOver = (x >= left && x <= (left + width)) && + (y >= top && y <= (top + height)); + + //Log.d("DragZone", "x=" +x + ", left=" + left + ", y=" + y + ", top=" + top + " width=" + width + ", height=" + height + ", isover=" + isOver); + + return isOver; + } + + // ========================================= + // Protected & Private methods + // ========================================= + + protected DropZoneEventsListener getListener() { + return listener; + } + + private void calculateDimensions() { + int[] location = new int[2]; + view.getLocationOnScreen(location); + left = location[0]; + top = location[1]; + width = view.getWidth(); + height = view.getHeight(); + dimansionsCalculated = true; + } +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZoneEventsListener.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZoneEventsListener.java new file mode 100644 index 000000000..828a1cbf2 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/DragAndDrop/DropZoneEventsListener.java @@ -0,0 +1,9 @@ +package com.MobileAnarchy.Android.Widgets.DragAndDrop; + +public interface DropZoneEventsListener { + + void OnDragZoneEntered(DropZone zone, DraggableItem item); + void OnDragZoneLeft(DropZone zone, DraggableItem item); + void OnDropped(DropZone zone, DraggableItem item); + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/DualJoystickView.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/DualJoystickView.java new file mode 100644 index 000000000..bb419d404 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/DualJoystickView.java @@ -0,0 +1,147 @@ +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; + } +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickClickedListener.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickClickedListener.java new file mode 100644 index 000000000..128828980 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickClickedListener.java @@ -0,0 +1,6 @@ +package com.MobileAnarchy.Android.Widgets.Joystick; + +public interface JoystickClickedListener { + public void OnClicked(); + public void OnReleased(); +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickMovedListener.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickMovedListener.java new file mode 100644 index 000000000..346f2efed --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickMovedListener.java @@ -0,0 +1,7 @@ +package com.MobileAnarchy.Android.Widgets.Joystick; + +public interface JoystickMovedListener { + public void OnMoved(int pan, int tilt); + public void OnReleased(); + public void OnReturnedToCenter(); +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickView.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickView.java new file mode 100644 index 000000000..bab74e5a7 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/Joystick/JoystickView.java @@ -0,0 +1,521 @@ +package com.MobileAnarchy.Android.Widgets.Joystick; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Handler; +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, 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; + + // ========================================= + // 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.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; + } +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdEditText.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdEditText.java new file mode 100644 index 000000000..5fbd76f46 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdEditText.java @@ -0,0 +1,169 @@ +package com.MobileAnarchy.Android.Widgets.ThresholdEditText; + +import android.content.Context; +import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.widget.EditText; + +public class ThresholdEditText extends EditText { + + // ========================================= + // Private members + // ========================================= + + private int threshold; + private ThresholdTextChanged thresholdTextChanged; + private Handler handler; + private Runnable invoker; + private boolean thresholdDisabledOnEmptyInput; + + + // ========================================= + // Constructors + // ========================================= + + public ThresholdEditText(Context context) { + super(context); + initAttributes(null); + init(); + } + + public ThresholdEditText(Context context, AttributeSet attrs) { + super(context, attrs); + initAttributes(attrs); + init(); + } + + + // ========================================= + // Public properties + // ========================================= + + /** + * Get the current threshold value + */ + public int getThreshold() { + return threshold; + } + + /** + * Set the threshold value (in milliseconds) + * + * @param threshold + * Threshold value + */ + public void setThreshold(int threshold) { + this.threshold = threshold; + } + + /** + * @return True = the callback will fire immediately when the content of the + * EditText is emptied False = The threshold will be used even on + * empty input + */ + public boolean getThresholdDisabledOnEmptyInput() { + return thresholdDisabledOnEmptyInput; + } + + /** + * @param thresholdDisabledOnEmptyInput + * Set to true if you want the callback to fire immediately when + * the content of the EditText is emptied + */ + public void setThresholdDisabledOnEmptyInput( + boolean thresholdDisabledOnEmptyInput) { + this.thresholdDisabledOnEmptyInput = thresholdDisabledOnEmptyInput; + } + + /** + * Set the callback to the OnThresholdTextChanged event + * + * @param listener + */ + public void setOnThresholdTextChanged(ThresholdTextChanged listener) { + this.thresholdTextChanged = listener; + } + + // ========================================= + // Private / Protected methods + // ========================================= + + /** + * Load properties values from xml layout + */ + private void initAttributes(AttributeSet attrs) { + if (attrs != null) { + String namespace = "http://com.MobileAnarchy.Android.Widgets"; + + // Load values to local members + this.threshold = attrs.getAttributeIntValue(namespace, "threshold", + 500); + this.thresholdDisabledOnEmptyInput = attrs.getAttributeBooleanValue( + namespace, "disableThresholdOnEmptyInput", true); + } else { + // Default threshold value is 0.5 seconds + threshold = 500; + + // Default behaviour on emptied text - no threshold + thresholdDisabledOnEmptyInput = true; + } + } + + /** + * Initialize the private members with default values + */ + private void init() { + + handler = new Handler(); + + invoker = new Runnable() { + + @Override + public void run() { + invokeCallback(); + } + + }; + + this.addTextChangedListener(new TextWatcher() { + + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + + // Remove any existing pending callbacks + handler.removeCallbacks(invoker); + + if (s.length() == 0 && thresholdDisabledOnEmptyInput) { + // The text is empty, so invoke the callback immediately + invoker.run(); + } else { + // Post a new delayed callback + handler.postDelayed(invoker, threshold); + } + } + + }); + } + + /** + * Invoking the callback on the listener provided (if provided) + */ + private void invokeCallback() { + if (thresholdTextChanged != null) { + thresholdTextChanged.onThersholdTextChanged(this.getText()); + } + } + +} \ No newline at end of file diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdTextChanged.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdTextChanged.java new file mode 100644 index 000000000..145f6547b --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/ThresholdEditText/ThresholdTextChanged.java @@ -0,0 +1,7 @@ +package com.MobileAnarchy.Android.Widgets.ThresholdEditText; + +import android.text.Editable; + +public interface ThresholdTextChanged { + void onThersholdTextChanged(Editable text); +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/SingleTileLayout.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/SingleTileLayout.java new file mode 100644 index 000000000..d2f3be2d3 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/SingleTileLayout.java @@ -0,0 +1,68 @@ +package com.MobileAnarchy.Android.Widgets.TilesLayout; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +public class SingleTileLayout extends FrameLayout { + + // ========================================= + // Private members + // ========================================= + + private TilePosition position; + private long timestamp; + + // ========================================= + // Constructors + // ========================================= + + public SingleTileLayout(Context context) { + super(context); + } + + public SingleTileLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + + // ========================================= + // Public Methods + // ========================================= + + public TilePosition getPosition() { + return position; + } + + public void setPosition(TilePosition position) { + this.position = position; + } + + public long getTimestamp() { + return this.timestamp; + } + + // ========================================= + // Overrides + // ========================================= + + @Override + public void addView(View child) { + super.addView(child); + timestamp = java.lang.System.currentTimeMillis(); + } + + @Override + public void removeAllViews() { + super.removeAllViews(); + timestamp = 0; + } + + @Override + public void removeView(View view) { + super.removeView(view); + timestamp = 0; + } + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilePosition.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilePosition.java new file mode 100644 index 000000000..04bf0c1d4 --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilePosition.java @@ -0,0 +1,63 @@ +package com.MobileAnarchy.Android.Widgets.TilesLayout; + + +public class TilePosition { + + // ========================================= + // Private Members + // ========================================= + + private float x, y, height, width; + + // ========================================= + // Constructors + // ========================================= + + public TilePosition(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.height = height; + this.width = width; + } + + // ========================================= + // Public Properties + // ========================================= + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public float getHeight() { + return height; + } + + public float getWidth() { + return width; + } + + + // ========================================= + // Public Methods + // ========================================= + + public Boolean equals(TilePosition position) { + if (position == null) + return false; + + return this.x == position.x && + this.y == position.y && + this.height == position.height && + this.width == position.width; + } + + @Override + public String toString() { + return "TilePosition = [X: " + x + ", Y: " + y + ", Height: " + height + ", Width: " + width + "]"; + } + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayout.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayout.java new file mode 100644 index 000000000..c1728faff --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayout.java @@ -0,0 +1,299 @@ +package com.MobileAnarchy.Android.Widgets.TilesLayout; + +import java.util.ArrayList; +import java.util.List; + +import android.R; +import android.content.Context; +import android.graphics.Color; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.TranslateAnimation; +import android.view.animation.Animation.AnimationListener; +import android.widget.FrameLayout; + +public class TilesLayout extends FrameLayout { + + // ========================================= + // Private members + // ========================================= + + private static final String TAG = "TilesLayout"; + private int animatedTransitionDuration; + private List tiles; + private TilesLayoutPreset preset; + private int tileBackgroundResourceId; + + // ========================================= + // Constructors + // ========================================= + + public TilesLayout(Context context) { + super(context); + Init(null); + } + + public TilesLayout(Context context, AttributeSet attrs) { + super(context, attrs); + Init(attrs); + } + + + // ========================================= + // Public Properties + // ========================================= + + public void setPreset(TilesLayoutPreset preset) { + try { + rebuildLayout(preset); + this.preset = preset; + } + catch (Exception ex) { + Log.e(TAG, "Failed to set layout preset", ex); + } + } + + public TilesLayoutPreset getPreset() { + return this.preset; + } + + public int getAnimatedTransitionDuration() { + return animatedTransitionDuration; + } + + public void setAnimatedTransitionDuration(int animatedTransitionDuration) { + this.animatedTransitionDuration = animatedTransitionDuration; + } + + public int getTileBackgroundResourceId() { + return tileBackgroundResourceId; + } + + public void setTileBackgroundResourceId(int tileBackgroundResourceId) { + this.tileBackgroundResourceId = tileBackgroundResourceId; + } + + // ========================================= + // Public Methods + // ========================================= + + public void addContent(View view) { + for (int i = 0; i < tiles.size(); i++) { + if (tiles.get(i).getChildCount() == 0) { + tiles.get(i).addView(view); + return; + } + } + // No available space for the new view... + // TODO: Take the tile with the smallest time stamp and place the new view in it + } + + public void clearView(int tileId) { + if (tiles.size() < tileId) { + tiles.get(tileId).removeAllViews(); + } + } + + // ========================================= + // Private Methods + // ========================================= + + private void Init(AttributeSet attrs) { + animatedTransitionDuration = 750; + tileBackgroundResourceId = R.drawable.edit_text; + tiles = new ArrayList(); + } + + + private void rebuildLayout(TilesLayoutPreset preset) { + ArrayList positions = buildViewsPositions(preset); + + // We need to transform the current layout, to the new layout + int extraViews = tiles.size() - positions.size(); + if (extraViews > 0) { + // Remove the extra views + while(tiles.size() - positions.size() > 0) { + int lastViewPosition = tiles.size() - 1; + removeView(tiles.get(lastViewPosition)); + tiles.remove(lastViewPosition); + } + } else { + // Add the extra views + for (int i = tiles.size(); i< positions.size(); i++) { + TilePosition newTilePosition = positions.get(i); + SingleTileLayout tile = new SingleTileLayout(getContext()); + tile.setBackgroundResource(tileBackgroundResourceId); + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + (int)newTilePosition.getWidth(), + (int)newTilePosition.getHeight(), + Gravity.TOP + Gravity.LEFT); + lp.setMargins((int)newTilePosition.getX(), + (int)newTilePosition.getY(), 0, 0); + + tile.setLayoutParams(lp); + + tiles.add(tile); + addView(tile); + } + } + // There is a bug in the animation-set, so we'll not animate + animateChange(positions); + + // Regular repositioning (no animation) + //processChange(positions); + } + + + + private ArrayList buildViewsPositions(TilesLayoutPreset preset) { + int width = getWidth(); + int height = getHeight(); + + Log.d(TAG, "Container's Dimensions = Width: " + width + ", Height: " + height); + ArrayList actualPositions = new ArrayList(); + for (TilePosition position : preset.getTilePositions()) { + + int tileX = (int) Math.round(width * ((float)position.getX() / 100.0)); + int tileY = (int) Math.round(height * ((float)position.getY() / 100.0)); + int tileWidth = (int) Math.round(width * ((float)position.getWidth() / 100.0)); + int tileHeight = (int) Math.round(height * ((float)position.getHeight() / 100.0)); + + TilePosition actualPosition = new TilePosition(tileX, tileY, tileWidth, tileHeight); + actualPositions.add(actualPosition); + + Log.d(TAG, "New tile created - X: " + tileX + ", Y: " + tileY + ", Width: " + tileWidth + ", Height: " + tileHeight); + } + return actualPositions; + } + + + @SuppressWarnings("unused") + private void processChange(ArrayList positions) { + for (int i = 0; i < tiles.size(); i++) { + final SingleTileLayout currentTile = tiles.get(i); + final TilePosition targetPosition = positions.get(i); + currentTile.setPosition(targetPosition); + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + (int)targetPosition.getWidth(), + (int)targetPosition.getHeight(), + Gravity.TOP + Gravity.LEFT); + lp.setMargins((int)targetPosition.getX(), + (int)targetPosition.getY(), 0, 0); + + currentTile.setLayoutParams(lp); + } + } + + + private void animateChange(ArrayList positions) { + AnimationSet animationSet = new AnimationSet(true); + DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(); + + for (int i = 0; i < tiles.size(); i++) { + AnimationSet scaleAndMove = new AnimationSet(true); + scaleAndMove.setFillAfter(true); + + final SingleTileLayout currentTile = tiles.get(i); + TilePosition currentPosition = currentTile.getPosition(); + final TilePosition targetPosition = positions.get(i); + + if (currentPosition == null) { + AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1); + alphaAnimation.setDuration(animatedTransitionDuration); + alphaAnimation.setStartOffset(0); + currentTile.setAnimation(alphaAnimation); + + scaleAndMove.addAnimation(alphaAnimation); + } + + currentTile.setPosition(targetPosition); + + if (!targetPosition.equals(currentPosition)) { + float toXDelta = 0, toYDelta = 0; + if (currentPosition != null) { + // Calculate new position + toXDelta = targetPosition.getX() - currentPosition.getX(); + toYDelta = targetPosition.getY() - currentPosition.getY(); + + // Factor in the scaling animation + toXDelta = toXDelta / (targetPosition.getWidth() / currentPosition.getWidth()); + toYDelta = toYDelta / (targetPosition.getHeight() / currentPosition.getHeight()); + } + + // Move + TranslateAnimation moveAnimation = new TranslateAnimation(0, toXDelta, 0, toYDelta); + moveAnimation.setDuration(animatedTransitionDuration); + moveAnimation.setStartOffset(0); + moveAnimation.setFillAfter(true); + moveAnimation.setInterpolator(decelerateInterpolator); + scaleAndMove.addAnimation(moveAnimation); + + // Physically move the tile when the animation ends + scaleAndMove.setAnimationListener(new AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { } + + @Override + public void onAnimationRepeat(Animation animation) { } + + @Override + public void onAnimationEnd(Animation animation) { + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + (int)targetPosition.getWidth(), + (int)targetPosition.getHeight(), + Gravity.TOP + Gravity.LEFT); + lp.setMargins((int)targetPosition.getX(), + (int)targetPosition.getY(), 0, 0); + + currentTile.setLayoutParams(lp); + + // The following null animation just gets rid of screen flicker + animation = new TranslateAnimation(0.0f, 0.0f, 0.0f, 0.0f); + animation.setDuration(1); + currentTile.startAnimation(animation); + } + }); + + // Scale + if (currentPosition != null) { + ScaleAnimation scaleAnimation = + new ScaleAnimation(1, + targetPosition.getWidth() / currentPosition.getWidth(), + 1, + targetPosition.getHeight() / currentPosition.getHeight(), + Animation.ABSOLUTE, 0, + Animation.ABSOLUTE, 0); + + scaleAnimation.setDuration(animatedTransitionDuration); + scaleAnimation.setStartOffset(0); + scaleAnimation.setFillAfter(true); + scaleAndMove.addAnimation(scaleAnimation); + } + + // Set animation to the tile + currentTile.setAnimation(scaleAndMove); + } + // Add to the total animation set + animationSet.addAnimation(scaleAndMove); + } + + if (animationSet.getAnimations().size() > 0) { + Log.d(TAG, "Starting animation"); + animationSet.setFillAfter(true); + animationSet.start(); + } + } + + +} diff --git a/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayoutPreset.java b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayoutPreset.java new file mode 100644 index 000000000..76a13d73b --- /dev/null +++ b/androidgcs/src/com/MobileAnarchy/Android/Widgets/TilesLayout/TilesLayoutPreset.java @@ -0,0 +1,157 @@ +package com.MobileAnarchy.Android.Widgets.TilesLayout; + +import java.util.LinkedList; +import java.util.List; + +/** + * Describes the positioning of a tiles in a 100x100 environment + */ +public class TilesLayoutPreset { + + // ========================================= + // Private Members + // ========================================= + + private List _positions; + private String _presetName; + + // ========================================= + // Constructors + // ========================================= + + public TilesLayoutPreset(String name) { + _presetName = name; + _positions = new LinkedList(); + } + + // ========================================= + // Public Methods + // ========================================= + + public void add(float x, float y, float width, float height) { + TilePosition tilePosition = new TilePosition(x, y, height, width); + add(tilePosition); + } + + public void add(TilePosition tilePosition) { + _positions.add(tilePosition); + } + + public Iterable getTilePositions() { + return _positions; + } + + public int getCount() { + return _positions.size(); + } + + // ========================================= + // Static Presets Factories + // ========================================= + + public static TilesLayoutPreset get1x1() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 1X1"); + preset.add(0, 0, 100, 100); + return preset; + } + + + public static TilesLayoutPreset get1x2() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 1X2"); + preset.add(0, 0, 50, 100); + preset.add(0, 50, 50, 100); + return preset; + } + + + public static TilesLayoutPreset get2x1() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 2X1"); + preset.add(0, 0, 100, 50); + preset.add(50, 0, 100, 50); + return preset; + } + + + public static TilesLayoutPreset get2x2() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 2X2"); + preset.add(0, 0, 50, 50); + preset.add(50, 0, 50, 50); + preset.add(0, 50, 50, 50); + preset.add(50, 50, 50, 50); + return preset; + } + + public static TilesLayoutPreset get3x3() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 3X3"); + preset.add(0, 0, 100/3f, 100/3f); + preset.add(100/3f, 0, 100/3f, 100/3f); + preset.add(200/3f, 0, 100/3f, 100/3f); + preset.add(0, 100/3f, 100/3f, 100/3f); + preset.add(100/3f, 100/3f, 100/3f, 100/3f); + preset.add(200/3f, 100/3f, 100/3f, 100/3f); + preset.add(0, 200/3f, 100/3f, 100/3f); + preset.add(100/3f, 200/3f, 100/3f, 100/3f); + preset.add(200/3f, 200/3f, 100/3f, 100/3f); + return preset; + } + + public static TilesLayoutPreset get3x2() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 3X2"); + preset.add(0, 0, 50, 100/3f); + preset.add(100/3f, 0, 50, 100/3f); + preset.add(200/3f, 0, 50, 100/3f); + preset.add(0, 50, 50, 100/3f); + preset.add(100/3f, 50, 50, 100/3f); + preset.add(200/3f, 50, 50, 100/3f); + return preset; + } + + public static TilesLayoutPreset get2x3() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 2X3"); + preset.add(0, 0, 100/3f, 50); + preset.add(50, 0, 100/3f, 50); + preset.add(0, 100/3f, 100/3f, 50); + preset.add(50, 100/3f, 100/3f, 50); + preset.add(0, 200/3f, 100/3f, 50); + preset.add(50, 200/3f, 100/3f, 50); + return preset; + } + + + public static TilesLayoutPreset get4x4() { + TilesLayoutPreset preset = new TilesLayoutPreset("Default 2X2"); + preset.add(0, 0, 25, 25); + preset.add(25, 0, 25, 25); + preset.add(50, 0, 25, 25); + preset.add(75, 0, 25, 25); + preset.add(0, 25, 25, 25); + preset.add(25, 25, 25, 25); + preset.add(50, 25, 25, 25); + preset.add(75, 25, 25, 25); + preset.add(0, 50, 25, 25); + preset.add(25, 50, 25, 25); + preset.add(50, 50, 25, 25); + preset.add(75, 50, 25, 25); + preset.add(0, 75, 25, 25); + preset.add(25, 75, 25, 25); + preset.add(50, 75, 25, 25); + preset.add(75, 75, 25, 25); + return preset; + } + + public static TilesLayoutPreset get2x3x3() { + TilesLayoutPreset preset = new TilesLayoutPreset("Custom 2X4X4"); + preset.add(0, 0, 50, 50); + preset.add(50, 0, 50, 50); + preset.add(0, 50, 50, 50); + + preset.add(50, 50, 25, 25); + preset.add(75, 50, 25, 25); + preset.add(50, 75, 25, 25); + preset.add(75, 75, 25, 25); + + return preset; + } + + +}