I have the following situation:
ZoomAndPanLayout
|
+---> ImageView
|
+---> FrameLayout (DragLayer)
|
+---> One or more controls. A view with a circle drawn on it.
With some minor issues because I don't care for now about screen bound the ZoomAndPanLayout works. I implemented ZoomAndPan like this:
public class ZoomAndPanLayout extends FrameLayout {
//region Constants
public static final float DEFAULT_MIN_SCALE_FACTOR = 1.0f;
public static final float DEFAULT_MAX_SCALE_FACTOR = 5.0f;
// endregion Constants
// region Fields
private float translationX = 0;
private float translationY = 0;
private float pivotX = 0;
private float pivotY = 0;
private float oldX;
private float oldY;
private float scaleFactor = 1.0f;
private float minScaleFactor = ZoomAndPanLayout.DEFAULT_MIN_SCALE_FACTOR;
private float maxScaleFactor = ZoomAndPanLayout.DEFAULT_MAX_SCALE_FACTOR;
private ScaleGestureDetector scaleGestureDetector = null;
// endregion Fields
// region Constructor
public ZoomAndPanLayout(Context context) {
super(context);
this.initialize(context);
}
public ZoomAndPanLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.initialize(context);
}
public ZoomAndPanLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.initialize(context);
}
private void initialize(Context context) {
this.scaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureListener());
}
// endregion Constructor
#Override
public boolean onTouchEvent(MotionEvent event) {
this.scaleGestureDetector.onTouchEvent(event);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
{
this.oldX = event.getX();
this.oldY = event.getY();
break;
}
case MotionEvent.ACTION_MOVE:
{
if (!this.scaleGestureDetector.isInProgress())
{
float x = event.getX();
float y = event.getY();
float deltaX = x - this.oldX;
float deltaY = y - this.oldY;
this.translationX += deltaX;
this.translationY += deltaY;
this.applyTransformations();
this.oldX = x;
this.oldY = y;
}
}
}
return true;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.scaleGestureDetector.onTouchEvent(event);
}
private void applyTransformations() {
final View child = this.getChildAt(0);
if (child != null)
{
child.setPivotX(this.pivotX);
child.setPivotY(this.pivotY);
child.setScaleX(this.scaleFactor);
child.setScaleY(this.scaleFactor);
// TODO: bound child to screen limits
child.setTranslationX(this.translationX);
child.setTranslationY(this.translationY);
}
}
public Rect getChildRect() {
View child = this.getChildAt(0);
if (child != null)
{
Rect outRect = new Rect();
outRect.right = (int) (child.getWidth() * child.getScaleX());
outRect.bottom = (int) (child.getHeight() * child.getScaleY());
int[] location = new int[2];
child.getLocationOnScreen(location);
outRect.offset(location[0], location[1]);
return outRect;
}
else
{
return new Rect(0, 0, 0, 0);
}
}
// region Private Inner Enums, Interfaces and Classes
private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
private ZoomAndPanLayout upper = ZoomAndPanLayout.this;
#Override
public boolean onScale(ScaleGestureDetector detector) {
float newScaleFactor = detector.getScaleFactor();
float originalScaleFactor = upper.scaleFactor;
upper.scaleFactor *= newScaleFactor;
// Bound the scaleFactor to the min and max limits
if (upper.scaleFactor >= upper.maxScaleFactor)
{
upper.scaleFactor = upper.maxScaleFactor;
newScaleFactor = upper.maxScaleFactor / originalScaleFactor;
}
else if (upper.scaleFactor * newScaleFactor <= upper.minScaleFactor)
{
upper.scaleFactor = upper.minScaleFactor;
newScaleFactor = upper.minScaleFactor / originalScaleFactor;
}
// set pivot
View child = upper.getChildAt(0);
if (child != null)
{
if (newScaleFactor * child.getWidth() * upper.scaleFactor <= originalScaleFactor * child.getWidth()
|| newScaleFactor * child.getHeight() * upper.scaleFactor <= originalScaleFactor * child.getWidth())
{
upper.pivotX = newScaleFactor * child.getWidth() / 2;
upper.pivotY = newScaleFactor * child.getHeight() / 2;
}
else
{
upper.pivotX = detector.getFocusX();
upper.pivotY = detector.getFocusY();
}
}
upper.applyTransformations();
return true;
}
}
// endregion Private Inner Enums, Interfaces and Classes
}
When I create each child of DragLayer I assign to them a OnLongClickListener, but the god damn thing dose not fire. when I long click on any child of DragLayer.
Any idea how can I implement this using both my idea for ZoomAndPanLayout or any idea. If you ask yourself why I need ZoomAndPanLayout, it is because I must be able to zoom and pan any layout not just an ImageView.
Any idea?
Probably because you return true in onTouchEvent method.
When true is returned it consumed an event and children don't receive their own onTouchEvent.
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.scaleGestureDetector.onTouchEvent(event);
}
always return true and it also blocks children from receiving an event.
Related
I'm trying to create an application in which the user can drag and zoom an ImageView. But I'm experiencing problems with the following code.
When the scaleFactor is not 1 and the second finger gets down it is translated a little to somewhere else. I don't know where this translate comes from...
Here's the complete class:
package me.miutaltbati.ramaview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;
import static android.view.MotionEvent.INVALID_POINTER_ID;
public class RamaView extends ImageView {
private Context context;
private Matrix matrix = new Matrix();
private Matrix translateMatrix = new Matrix();
private Matrix scaleMatrix = new Matrix();
// Properties coming from outside:
private int drawableLayoutId;
private int width;
private int height;
private static float MIN_ZOOM = 0.33333F;
private static float MAX_ZOOM = 5F;
private PointF mLastTouch = new PointF(0, 0);
private PointF mLastFocus = new PointF(0, 0);
private PointF mLastPivot = new PointF(0, 0);
private float mPosX = 0F;
private float mPosY = 0F;
public float scaleFactor = 1F;
private int mActivePointerId = INVALID_POINTER_ID;
private Paint paint;
private Bitmap bitmapLayout;
private OnFactorChangedListener mListener;
private ScaleGestureDetector mScaleDetector;
public RamaView(Context context) {
super(context);
initializeInConstructor(context);
}
public RamaView(Context context, #android.support.annotation.Nullable AttributeSet attrs) {
super(context, attrs);
initializeInConstructor(context);
}
public RamaView(Context context, #android.support.annotation.Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeInConstructor(context);
}
public void initializeInConstructor(Context context) {
this.context = context;
paint = new Paint();
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
mScaleDetector.setQuickScaleEnabled(false);
setScaleType(ScaleType.MATRIX);
}
public Bitmap decodeSampledBitmap() {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);
// Calculate inSampleSize
options.inSampleSize = Util.calculateInSampleSize(options, width, height); // e.g.: 4, 8
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);
}
public void setDrawable(int drawableId) {
drawableLayoutId = drawableId;
}
public void setSize(int width, int height) {
this.width = width;
this.height = height;
bitmapLayout = decodeSampledBitmap();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mScaleDetector.onTouchEvent(event);
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
int pointerIndex = event.getActionIndex();
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
// Remember where we started (for dragging)
mLastTouch = new PointF(x, y);
// Save the ID of this pointer (for dragging)
mActivePointerId = event.getPointerId(0);
}
case MotionEvent.ACTION_POINTER_DOWN: {
if (event.getPointerCount() == 2) {
mLastFocus = new PointF(mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
}
}
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
int pointerIndex = event.findPointerIndex(mActivePointerId);
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
// Calculate the distance moved
float dx = 0;
float dy = 0;
if (event.getPointerCount() == 1) {
// Calculate the distance moved
dx = x - mLastTouch.x;
dy = y - mLastTouch.y;
matrix.setScale(scaleFactor, scaleFactor, mLastPivot.x, mLastPivot.y);
// Remember this touch position for the next move event
mLastTouch = new PointF(x, y);
} else if (event.getPointerCount() == 2) {
// Calculate the distance moved
dx = mScaleDetector.getFocusX() - mLastFocus.x;
dy = mScaleDetector.getFocusY() - mLastFocus.y;
matrix.setScale(scaleFactor, scaleFactor, -mPosX + mScaleDetector.getFocusX(), -mPosY + mScaleDetector.getFocusY());
mLastPivot = new PointF(-mPosX + mScaleDetector.getFocusX(), -mPosY + mScaleDetector.getFocusY());
mLastFocus = new PointF(mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
}
mPosX += dx;
mPosY += dy;
matrix.postTranslate(mPosX, mPosY);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouch = new PointF(event.getX(newPointerIndex), event.getY(newPointerIndex));
mActivePointerId = event.getPointerId(newPointerIndex);
} else {
final int tempPointerIndex = event.findPointerIndex(mActivePointerId);
mLastTouch = new PointF(event.getX(tempPointerIndex), event.getY(tempPointerIndex));
}
break;
}
}
invalidate();
return true;
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.setMatrix(matrix);
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(bitmapLayout, 0, 0, paint);
canvas.restore();
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
}
I think the problem is on this line:
matrix.setScale(scaleFactor, scaleFactor, -mPosX + mScaleDetector.getFocusX(), -mPosY + mScaleDetector.getFocusY());
I tried a lot of things but I couldn't get it to work properly.
UPDATE:
Here's how you can init a RamaView instance:
Main activity's onCreate:
rvRamaView = findViewById(R.id.rvRamaView);
final int[] rvSize = new int[2];
ViewTreeObserver vto = rvRamaView.getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
rvRamaView.getViewTreeObserver().removeOnPreDrawListener(this);
rvSize[0] = rvRamaView.getMeasuredWidth();
rvSize[1] = rvRamaView.getMeasuredHeight();
rvRamaView.setSize(rvSize[0], rvSize[1]);
return true;
}
});
rvRamaView.setDrawable(R.drawable.original_jpg);
It would be better to use the matrix to accumulate changes rather than try to recalculate the transformations yourself. You can do this with the matrix post... and pre... methods and stay away from the set... methods which reset the matrix.
Here is a rework of the RamaView class which was largely on target except for the specific handling of the matrix as noted above. The mods are to the onTouchEvent() method. The video is output of the code working in a sample app.
RamaView.java
public class RamaView extends ImageView {
private final Matrix matrix = new Matrix();
// Properties coming from outside:
private int drawableLayoutId;
private int width;
private int height;
private static final float MIN_ZOOM = 0.33333F;
private static final float MAX_ZOOM = 5F;
private PointF mLastTouch = new PointF(0, 0);
private PointF mLastFocus = new PointF(0, 0);
public float scaleFactor = 1F;
private int mActivePointerId = INVALID_POINTER_ID;
private Paint paint;
private Bitmap bitmapLayout;
// private OnFactorChangedListener mListener;
private ScaleGestureDetector mScaleDetector;
public RamaView(Context context) {
super(context);
initializeInConstructor(context);
}
public RamaView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
initializeInConstructor(context);
}
public RamaView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeInConstructor(context);
}
public void initializeInConstructor(Context context) {
paint = new Paint();
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
mScaleDetector.setQuickScaleEnabled(false);
setScaleType(ScaleType.MATRIX);
}
public Bitmap decodeSampledBitmap() {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);
options.inSampleSize = Util.calculateInSampleSize(options, width, height); // e.g.: 4, 8
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);
}
public void setDrawable(int drawableId) {
drawableLayoutId = drawableId;
}
public void setSize(int width, int height) {
this.width = width;
this.height = height;
bitmapLayout = decodeSampledBitmap();
}
private float mLastScaleFactor = 1.0f;
#Override
public boolean onTouchEvent(MotionEvent event) {
mScaleDetector.onTouchEvent(event);
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
int pointerIndex = event.getActionIndex();
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
// Remember where we started (for dragging)
mLastTouch = new PointF(x, y);
// Save the ID of this pointer (for dragging)
mActivePointerId = event.getPointerId(0);
}
case MotionEvent.ACTION_POINTER_DOWN: {
if (event.getPointerCount() == 2) {
mLastFocus = new PointF(mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
}
}
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
int pointerIndex = event.findPointerIndex(mActivePointerId);
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
// Calculate the distance moved
float dx = 0;
float dy = 0;
if (event.getPointerCount() == 1) {
// Calculate the distance moved
dx = x - mLastTouch.x;
dy = y - mLastTouch.y;
// Remember this touch position for the next move event
mLastTouch = new PointF(x, y);
} else if (event.getPointerCount() == 2) {
// Calculate the distance moved
float focusX = mScaleDetector.getFocusX();
float focusY = mScaleDetector.getFocusY();
dx = focusX - mLastFocus.x;
dy = focusY - mLastFocus.y;
// Since we are accumating translation/scaling, we are just adding to
// the previous scale.
matrix.postScale(scaleFactor/mLastScaleFactor, scaleFactor/mLastScaleFactor, focusX, focusY);
mLastScaleFactor = scaleFactor;
mLastFocus = new PointF(focusX, focusY);
}
// Translation is cumulative.
matrix.postTranslate(dx, dy);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouch = new PointF(event.getX(newPointerIndex), event.getY(newPointerIndex));
mActivePointerId = event.getPointerId(newPointerIndex);
} else {
final int tempPointerIndex = event.findPointerIndex(mActivePointerId);
mLastTouch = new PointF(event.getX(tempPointerIndex), event.getY(tempPointerIndex));
}
break;
}
}
invalidate();
return true;
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.setMatrix(matrix);
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(bitmapLayout, 0, 0, paint);
canvas.restore();
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
}
It seems the code is not complete (e. g. I cannot see how matrix is used and where scaleFactor is assigned), but I think the reason why the translation is inconsistent is because in case of 2 pointers you get the [x, y] position from the mScaleDetector.getFocus. As the documentation for ScaleGestureDetector.getFocusX() states:
Get the X coordinate of the current gesture's focal point. If a
gesture is in progress, the focal point is between each of the
pointers forming the gesture.
You should use the mScaleDetector only for getting the current scale, but translation should be always calculated as the difference between the mLastTouch and event.getXY(pointerIndex) so that only one pointer is considered for translation. In case the user adds a second finger and releases the first one, make sure to reassign the pointerIndex and do not perform any translation to avoid jumping.
I'm developing a game, and I have a custom view Joystick which haves a onTouch event. I also have a ImageView which also haves onTouch for shooting.
If I add the views programatically using java code to a main relative layout, the onTouch of the two views works perfectly and at the same time.
The problem is that if I add the views using a XML layout file, then, if you touch the custom view Joystick, the onTouch of the other view doesn't works. It only happens if I use XML layout file for creating the layout.
How can I solve the problem?
The layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/shootImageView"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:src="#drawable/shoot"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
<com.myapp.uiviews.Joystick
android:id="#+id/joystick"
android:layout_width="wrap_content"
android:layout_height="150dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
</RelativeLayout>
The class:
mainLayout = (RelativeLayout) findViewById(R.id.mainLayout);
shootImageView= (ImageView) findViewById(R.id.shootImageView);
shootImageView.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
touchX=sw/2;
touchY=sh/2;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
LaserManager.getInstance().startLaserThread();
break;
case MotionEvent.ACTION_UP:
LaserManager.getInstance().stopLaserThread();
break;
}
return true;
}
});
shootImageView.invalidate();
shootImageView.refreshDrawableState();
shootImageView.forceLayout();
joystickOnScreen = (Joystick) findViewById(R.id.joystick);
int factor = GameState.getInstance().getSpaceship().getSpeedFactorCorrespondence();
joystickOnScreen.setMovementRange(sh/factor);
joystickOnScreen.setInnerPadding(sh/30);
joystickOnScreen.setOnJostickMovedListener(joystickListener);
joystickOnScreen.invalidate();
joystickOnScreen.refreshDrawableState();
joystickOnScreen.forceLayout();
Using that code doesn't works, but if I use this next code it works perfectly... why?
shootImageView = new ImageView(this);
shootImageView.setId(102);
RelativeLayout.LayoutParams shootImageViewParams = new RelativeLayout.LayoutParams(sw/8, sw/8);
shootImageViewParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
shootImageViewParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
shootImageViewParams.setMargins(0, 0, sh/15, sh/15);
shootImageView.setLayoutParams(shootImageViewParams);
shootImageView.setImageDrawable(getResources().getDrawable(R.drawable.shoot));
shootImageView.setAlpha(0.4f);
shootImageView.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
touchX=sw/2;
touchY=sh/2;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
LaserManager.getInstance().startLaserThread();
break;
case MotionEvent.ACTION_UP:
LaserManager.getInstance().stopLaserThread();
break;
}
return true;
}
});
main.addView(shootImageView);
joystickOnScreen = new Joystick(this);
int factor = GameState.getInstance().getSpaceship().getSpeedFactorCorrespondence();
joystickOnScreen.setMovementRange(sh/factor);
joystickOnScreen.setInnerPadding(sh/30);
joystickOnScreen.setOnJostickMovedListener(joystickListener);
RelativeLayout.LayoutParams joystickParams = new RelativeLayout.LayoutParams(sh/3, sh/3);
joystickParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
joystickParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
joystickParams.setMargins(sh/100, 0, 0, sh/100);
joystickOnScreen.setLayoutParams(joystickParams);
joystickOnScreen.setAlpha(0.3f);
main.addView(joystickOnScreen);
This is the Joystick class:
public class Joystick extends View {
public static final int INVALID_POINTER = -1;
private JoystickMovedListener moveListener;
//# of pixels moveWithAcceleration required between reporting to the listener
private float moveResolution;
//Max range of moveWithAcceleration in user coordinate system
private float movementRange;
//Last touch point in view coordinates
private int pointerId = INVALID_POINTER;
private float touchX;
private float touchY;
private float touchXDelayedMovement;
private float touchYDelayedMovement;
//Handle center in view coordinates
private float handleX;
private float handleY;
//Last reported position in view coordinates (allows different reporting sensitivities)
private float reportX;
private float reportY;
//Center of the view in view coordinates
private int cX;
private int cY;
//Size of the view in view coordinates
private int dimX;
private int dimY;
private int innerPadding;
private int bgRadius;
private int handleRadius;
private int movementRadius;
private int handleInnerBoundaries;
//Cartesian coordinates of last touch point - joystick center is (0,0)
private int cartX;
private int cartY;
//User coordinates of last touch point
private int userX;
private int userY;
//Offset co-ordinates (used when touch events are received from parent's coordinate origin)
private int offsetX;
private int offsetY;
private Paint bgPaint;
private Paint handlePaint;
boolean disabled;
Handler handler;
Handler handlerDelayedMovement;
public Joystick(Context context, AttributeSet attrs) {
super(context, attrs);
initJoystickView();
}
public Joystick(Context context) {
super(context);
initJoystickView();
}
private void initJoystickView() {
setFocusable(true);
handlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
handlePaint.setColor(Color.RED);
handlePaint.setStrokeWidth(1);
handlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setColor(Color.DKGRAY);
bgPaint.setStrokeWidth(1);
bgPaint.setStyle(Paint.Style.FILL_AND_STROKE);
this.moveResolution = 1.0f;
handler = new Handler();
handlerDelayedMovement = new Handler();
}
public void setMovementRange(float movementRange) {
this.movementRange = movementRange;
}
public void setOnJostickMovedListener(JoystickMovedListener listener) {
this.moveListener = listener;
}
#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.2);
handleInnerBoundaries = handleRadius;
movementRadius = Math.min(cX, cY) - handleInnerBoundaries;
}
#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);
}
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) {
result = 200; // Return a default size of 200 if no bounds are specified.
} else {
result = specSize; // As you want to fill the available space always return the full available bounds.
}
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);
canvas.restore();
}
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: {
if (disabled==true)
break;
return processMoveEvent(ev);
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
if ( pointerId != INVALID_POINTER ) {
returnHandleToCenterVisually();
returnHandleToCenterAcceleratedMovement();
setPointerId(INVALID_POINTER);
}
break;
}
case MotionEvent.ACTION_POINTER_UP: {
if ( pointerId != INVALID_POINTER ) {
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if ( pointerId == this.pointerId ) {
returnHandleToCenterVisually();
returnHandleToCenterAcceleratedMovement();
setPointerId(INVALID_POINTER);
return true;
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
handlerDelayedMovement.removeCallbacksAndMessages(null);
if ( pointerId == INVALID_POINTER ) {
int x = (int) ev.getX();
if ( x >= offsetX && x < offsetX + dimX ) {
setPointerId(ev.getPointerId(0));
if (disabled==true){
return true;
}
return processMoveEvent(ev);
}
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
if ( pointerId == INVALID_POINTER ) {
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 ) {
setPointerId(pointerId);
return true;
}
}
break;
}
}
return false;
}
private boolean processMoveEvent(MotionEvent ev) {
if ( pointerId != INVALID_POINTER ) {
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;
moveWithAcceleration();
invalidate();
return true;
}
return false;
}
private void moveWithAcceleration() {
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);
}
final int numberOfFrames = 5;
final double intervalsX = (touchX - touchXDelayedMovement) / numberOfFrames;
final double intervalsY = (touchY - touchYDelayedMovement) / numberOfFrames;
handlerDelayedMovement.removeCallbacksAndMessages(null);
for (int i = 0; i <= numberOfFrames; i++) {
handlerDelayedMovement.postDelayed(new Runnable() {
#Override
public void run() {
touchXDelayedMovement += intervalsX;
touchYDelayedMovement += intervalsY;
reportOnMoved();
}
}, i * 50);
}
}
private void reportOnMoved() {
//We calc user coordinates
//First convert to cartesian coordinates
cartX = (int)(touchXDelayedMovement / movementRadius * movementRange);
cartY = (int)(touchYDelayedMovement / movementRadius * movementRange);
//Cartesian Coordinates
userX = cartX;
userY = cartY;
if (moveListener != null) {
boolean rx = Math.abs(touchXDelayedMovement - reportX) >= moveResolution;
boolean ry = Math.abs(touchYDelayedMovement - reportY) >= moveResolution;
if (rx || ry) {
this.reportX = touchXDelayedMovement;
this.reportY = touchYDelayedMovement;
moveListener.OnMoved(userX, userY);
}
}
}
private void returnHandleToCenterVisually() {
final int numberOfFrames = 2;
final double intervalsX = (0 - touchX) / numberOfFrames;
final double intervalsY = (0 - touchY) / numberOfFrames;
handler.removeCallbacksAndMessages(null);
for (int i = 0; i < numberOfFrames; i++) {
final int j = i;
handler.postDelayed(new Runnable() {
#Override
public void run() {
touchX += intervalsX;
touchY += intervalsY;
invalidate();
if (moveListener != null && j == numberOfFrames - 1) {
moveListener.OnReturnedToCenter();
}
}
}, i * 15);
}
if (moveListener != null) {
moveListener.OnReleased();
}
}
private void returnHandleToCenterAcceleratedMovement() {
final int numberOfFrames = 10;
final double intervalsX = (0 - touchXDelayedMovement) / numberOfFrames;
final double intervalsY = (0 - touchYDelayedMovement) / numberOfFrames;
handlerDelayedMovement.removeCallbacksAndMessages(null);
for (int i = 0; i < numberOfFrames; i++) {
handlerDelayedMovement.postDelayed(new Runnable() {
#Override
public void run() {
touchXDelayedMovement += intervalsX;
touchYDelayedMovement += intervalsY;
reportOnMoved();
}
}, i * 50);
}
}
public void setInnerPadding(int innerPadding){
this.innerPadding=innerPadding;
}
public void disable(){
disabled=true;
}
public void enable(){
disabled=false;
}
public interface JoystickMovedListener {
public void OnMoved(int pan, int tilt);
public void OnReleased();
public void OnReturnedToCenter();
}
}
Incredible but true, the problem was with
android:layout_height="wrap_content" in shoot button and android:layout_width="wrap_content" in joystick, after setting a fixed size the problem dissapeared. Didn't understand why..
I'm in my android app I need a viewpager which slide vertically (up
down manner). For this I have made a custom viewpager & in which I'm
using the traditional viewpager & applied PageTransfirmer to make it
swipe vertically not horizontly. Everything is working fine in other
devices except One Plus 5t (andriod version 9)
My code is below:
public class VerticalViewPager extends ViewPager {
private float initialXValue;
private float initialYValue;
private float minXDifference = 200;
private float minYDifference = 100;
public static SwapListener swapListener;
public static String SwipeLeft = "left";
public static String SwipeRight = "right";
public static boolean swipeTriggered = false;
public static boolean verticalSwipeTriggered = false;
private FixedSpeedScroller mScroller = null;
private boolean enabled;
public VerticalViewPager(Context context) {
super(context);
init();
}
public VerticalViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// The majority of the magic happens here
setPageTransformer(true, new VerticalPageTransformer());
// The easiest way to get rid of the overscroll drawing that
happens on
the left and right
setOverScrollMode(OVER_SCROLL_NEVER);
try {
Class<?> viewpager = ViewPager.class;
Field scroller = viewpager.getDeclaredField("mScroller");
scroller.setAccessible(true);
mScroller = new FixedSpeedScroller(getContext(),
new DecelerateInterpolator());
scroller.set(this, mScroller);
} catch (Exception ignored) {
}
}
/*
* Set the factor by which the duration will change
*/
public void setScrollDuration(int duration) {
mScroller.setScrollDuration(duration);
}
private class FixedSpeedScroller extends Scroller {
private int mDuration = 1000;
public FixedSpeedScroller(Context context) {super(context);}
public FixedSpeedScroller(Context context, Interpolator interpolator) {
super(context, interpolator);
}
public FixedSpeedScroller(Context context, Interpolator interpolator,
boolean flywheel) {super(context, interpolator, flywheel);}
#Override
public void startScroll(int startX, int startY, int dx, int dy, int
duration) {
// Ignore received duration, use fixed one instead
super.startScroll(startX, startY, dx, dy, mDuration);
}
#Override
public void startScroll(int startX, int startY, int dx, int dy) {
// Ignore received duration, use fixed one instead
super.startScroll(startX, startY, dx, dy, mDuration);
}
public void setScrollDuration(int duration) {mDuration = duration;}
}
private class VerticalPageTransformer implements
ViewPager.PageTransformer {
#Override
public void transformPage(View view, float position) {
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0);
}
else if (position <= 1) {
view.setAlpha(1);
// Counteract the default slide transition
view.setTranslationX(view.getWidth() * -position);
//set Y position to swipe in from top
float yPosition = position * view.getHeight();
view.setTranslationY(yPosition);
}
else {
// This page is way off-screen to the right.
view.setAlpha(0);
}
}
}
/**
* Swaps the X and Y coordinates of your touch event.
*/
private MotionEvent swapXY(MotionEvent ev) {
float width = getWidth();
float height = getHeight();
float newX = (ev.getY() / height) * width;
float newY = (ev.getX() / width) * height;
ev.setLocation(newX, newY);
return ev;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev){
boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
swapXY(ev); // return touch coordinates to original reference frame
for any child views
//IsSwipeAllowed(ev);
return intercepted;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
IsSwipeAllowed(ev);
return super.onTouchEvent(swapXY(ev));
}
private void IsSwipeAllowed(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN) {
initialXValue = event.getX();
initialYValue = event.getY();
}
if(event.getAction()==MotionEvent.ACTION_MOVE) {
try {
float diffX = Math.abs(event.getX() - initialXValue);
float diffy = Math.abs(event.getY() - initialYValue);
if (diffX > diffy && diffX > minXDifference) {
// swipe horizotal
if (!swipeTriggered && event.getX() > initialXValue) {
swapListener.listenSwapEvent(SwipeRight);
swipeTriggered = true;
}
else if (event.getX() < initialXValue) {
if (!HomeScreen.projectName.equals("ABMCPL") &&
CustomViewPager.IsSwipeAllowed(event) && !swipeTriggered) {
swapListener.listenSwapEvent(SwipeLeft); // to webview page
swipeTriggered = true;
}
}
}
else if (diffX < diffy && diffy > minYDifference) {
if (!verticalSwipeTriggered && event.getY() > initialYValue) {
viewPager.setCurrentItem(LandingPage.viewPager.getCurrentItem() - 1);
verticalSwipeTriggered = true;
}
else if (!verticalSwipeTriggered && event.getY() < initialYValue){
verticalSwipeTriggered = true;
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
public interface SwapListener {
void listenSwapEvent (String direction);
}
}
Hi I'm trying to perform Translate, Scale and Rotate on View (FrameLayout) in android.
In brief, I've a Fresco's SimpleDraweeView inside FrameLayout, as Fresco is not supporting Matrix transformations, so as an alternative I put that in FrameLayout and doing Translation, Rotation and Scaling.
I've extended FrameLayout here..
public class InteractiveFrameLayout extends FrameLayout {
private ViewTransformer mViewTransformer;
public InteractiveFrameLayout(Context context) {
super(context);
init(context);
}
public InteractiveFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public InteractiveFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
// Determine dimensions of 'earth' image
int baseViewWidth = (int) getResources().getDimension(R.dimen.animation_image_size);
int baseViewHeight = (int) getResources().getDimension(R.dimen.animation_image_size);
// Setup Gesture Detectors
mViewTransformer = new ViewTransformer(this, baseViewWidth, baseViewHeight);
}
public boolean onTouchEvent(MotionEvent event) {
return mViewTransformer.onTouchEvent(event) || super.onTouchEvent(event);
}
}
Below class takes view and does everything related to ViewTransformations.
public class ViewTransformer {
private View mView;
private Vector2D position;
private float scale = 1;
private float angle = 0;
private TouchManager touchManager = new TouchManager(2);
public ViewTransformer(View view, int viewWidth, int viewHeight) {
mView = view;
position = new Vector2D();
position.set(viewWidth / 2, viewHeight / 2);
}
public boolean onTouchEvent(MotionEvent event) {
try {
touchManager.update(event);
if (touchManager.getPressCount() == 1) {
position.add(touchManager.moveDelta(0));
ViewAffineOperation.moveViewTo(mView, position.getX(), position.getY());
}
else {
if (touchManager.getPressCount() == 2) {
Vector2D current = touchManager.getVector(0, 1);
Vector2D previous = touchManager.getPreviousVector(0, 1);
float currentDistance = current.getLength();
float previousDistance = previous.getLength();
if (previousDistance != 0) {
scale *= currentDistance / previousDistance;
ViewAffineOperation.scaleViewBy(mView, scale);
}
angle -= Vector2D.getSignedAngleBetween(current, previous);
ViewAffineOperation.rotateViewBy(mView, getDegreesFromRadians(angle));
}
}
mView.invalidate();
}
catch(Throwable t) {
// So lazy...
}
return true;
}
private static float getDegreesFromRadians(float angle) {
return (float)(angle * 180.0 / Math.PI);
}
public float getScale() {
return scale;
}
public float getRotationDegrees() {
return angle;
}
}
and this one
public class ViewAffineOperation {
public static void moveViewTo(View view, float focusX, float focusY) {
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
int midPointX = layoutParams.width >> 1;
int midPointY = layoutParams.height >> 1;
float dx = (focusX - midPointX);
float dy = (focusY - midPointY);
view.setTranslationX(view.getTranslationX() + dx);
view.setTranslationY(view.getTranslationY() + dy);
}
public static void scaleViewBy(View view, float scaleFactor) {
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
}
public static void rotateViewBy(View view, float degrees) {
view.setRotation(degrees);
}
}
The Key problem is in
view.setTranslationX(view.getTranslationX() + dx);
view.setTranslationY(view.getTranslationY() + dy);
This one is most promising thing what I've found in stack over flow.. Rotate and scale a view based on one handle in Android
Thanks a ton in Advance.
#Sasha Salauyou I'm trying to reach you to help me in finding solution.
I was working with the Custom RelativeLayout because I want to zoom scale and rotate whatever views present in that layout.
These features are working correctly but the problem is when I insert the button in layout and move the layout on screen the button doesn't respond but the position where it was originally or initially located responds.
Can someone tell me where I am doing wrong.
Please help.
Here is Custom RelativeLayout Code:
public class ZoomableRelativeLayout extends RelativeLayout implements
OnTouchListener {
// float mScaleFactor = 1;
float mPivotX;
float mPivotY;
/*
* New Declarations
*/
private float mScaleFactor = 0.6f;
private float mRotationDegrees = 0.f;
private float changeInRotationDegrees;
private float mFocusX;
private float mFocusY;
private float layoutCenterX, layoutCenterY;
private ScaleGestureDetector mScaleDetector = new ScaleGestureDetector(
getContext(), new ScaleListener());
private RotateGestureDetector mRotateDetector = new RotateGestureDetector(
getContext(), new RotateListener());
private MoveGestureDetector mMoveDetector = new MoveGestureDetector(
getContext(), new MoveListener());
private boolean firstTym = true;
// private ShoveGestureDetector mShoveDetector;
/*
* End of New Declarations
*/
public ZoomableRelativeLayout(Context context) {
super(context);
// TODO Auto-generated constructor stub
// setOnTouchListener(this);
}
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
#SuppressWarnings("deprecation")
public ZoomableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
setOnTouchListener(this);
MyMainActivity mContext = (MyMainActivity) context;
Display defaultDisplay = mContext.getWindowManager()
.getDefaultDisplay();
mFocusX = defaultDisplay.getWidth() / 2f;
mFocusY = defaultDisplay.getHeight() / 2f;
}
public ZoomableRelativeLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
// setOnTouchListener(this);
}
protected void dispatchDraw(Canvas canvas) {
if (firstTym) {
layoutCenterX = (getWidth() * mScaleFactor) / 2;
layoutCenterY = (getHeight() * mScaleFactor) / 2;
// Toast.makeText(getContext(), ""+layoutCenterX+"***"+layoutCenterY, 2000).show();
firstTym = false;
}
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.scale(mScaleFactor, mScaleFactor);
canvas.rotate(mRotationDegrees, layoutCenterX,layoutCenterY);
canvas.translate((mFocusX - layoutCenterX), (mFocusY - layoutCenterY));
super.dispatchDraw(canvas);
canvas.restore();
}
#SuppressWarnings("deprecation")
#Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
layoutCenterX = (v.getWidth() * mScaleFactor) / 2;
layoutCenterY = (v.getHeight() * mScaleFactor) / 2;
mScaleDetector.onTouchEvent(event);
mRotateDetector.onTouchEvent(event);
mMoveDetector.onTouchEvent(event);
return true;
}
#TargetApi(Build.VERSION_CODES.FROYO)
private class ScaleListener extends
ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor(); // scale change since
// previous event
ZoomableRelativeLayout.this.invalidate();
// Don't let the object get too small or too large.
// mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));
return true;
}
}
private class RotateListener extends
RotateGestureDetector.SimpleOnRotateGestureListener {
#Override
public boolean onRotate(RotateGestureDetector detector) {
mRotationDegrees -= detector.getRotationDegreesDelta();
ZoomableRelativeLayout.this.invalidate();
return true;
}
}
private class MoveListener extends
MoveGestureDetector.SimpleOnMoveGestureListener {
#Override
public boolean onMove(MoveGestureDetector detector) {
PointF d = detector.getFocusDelta();
mFocusX += d.x;
mFocusY += d.y;
ZoomableRelativeLayout.this.invalidate();
return true;
}
}
}
As you are returning true on the overriden onTouch method, it means that the event won't go deeper in your view hierarchy, and therefore it will never hit the button. Try to give the MotionEvent directly to the button:
(you must reference it in your ZoomableRelativeLayout first)
#Override
public boolean onTouch(View v, MotionEvent event) {
layoutCenterX = (v.getWidth() * mScaleFactor) / 2;
layoutCenterY = (v.getHeight() * mScaleFactor) / 2;
mScaleDetector.onTouchEvent(event);
mRotateDetector.onTouchEvent(event);
mMoveDetector.onTouchEvent(event);
myButton.onTouch(event);
return true;
}
Button myButton;
public void setMyButton(Button b) {
this.myButton = b;
}