i'm trying to drag and drop a button the problem is that when i use getX() at motion event it works but the button starts to tremble . When i call the method getRawX() it does not tremble but it jumps at least 80px right before i start the drag and drop .
how can i managed that , i'll post my code here:
public class MyButton extends Button {
private final static int START_DRAGGING = 0;
private final static int STOP_DRAGGING = 1;
private int status;
private LinearLayout parentLayout;
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
// Log.i("teste", "Button width: " + btWidth + ", Height : "+ btHeight);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
status = START_DRAGGING;
Log.i("teste", "Coordenada on ACTION_DOWN: " + (int) event.getRawX());
break;
case MotionEvent.ACTION_UP:
status = STOP_DRAGGING;
Log.i("teste", "Coordenada on ACTION_UP: " + (int) event.getRawX());
break;
case MotionEvent.ACTION_MOVE:
if(status == START_DRAGGING){
parentLayout.setPadding((int)event.getRawX(), 0,0,0);
parentLayout.invalidate();
Log.i("teste", "Coordenada on ACTION_MOVE: " + (int) event.getRawX());
}
break;
}
return true;
}
}
event.getX() returns touch coordinates relative to your view (the button), event.getRawX() returns touch coordinates relative to the display, so I would think the first way is the correct one, if you set the padding of the button, instead of the layout. But you'll still have the "jump" problem because you're supposed to touch the button, not its edge, and the first move will put the edge under your finger.
I would try using a GestureDetector, its OnGestureListener has an onScroll() method that gives you the scrolling distance (it does the job of remembering last position and giving a relative motion), so that you can add that value to the padding, that is, you drag 10px => you add 10px of padding.
code example:
private GestureDetector gd =
new GestureDetector(getContext(), new SimpleOnGestureListener() {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
setPadding((int)(getPaddingLeft()+distanceX),0,0,0);
}
#Override
public boolean onDown(MotionEvent e) {
return true; // else no event will be handled
}
I would also add some checks to prevent negative or excessive paddings.
I have been working on touching and moving objects too. I started out making something move in a similar way: changing the top and left padding. Pretty quickly, as I added more moveable objects to the screen, things got a bit confusing. I would touch one spot on the screen and something else would start moving. What was going on was there were multiple overlapping views on the screen. Only the top view would receive the touch events.
I found a post that suggested taking a look at the Android Launcher code and seeing how they did drag and drop. I think their approach is really good. You do have to add a ViewGroup to hold your moveable objects, but that works out. The bounds of views match what you see on the screen and you end up with no surprises. Events go to the view you expect.
If it turns out you are going to have more than one moveable object, you might want to take a look at my blog post: "Moving Views In Android". More explanations about the Android Launcher and source code are there.
Related
Solved it by using emandt's suggestion. My personal Solution added below.
I'm using Android Studio for this.
I searched for solutions but couldn't find anything resembling this.
I want to know on which ImageView an UP action occurs while starting the DOWN action on a different ImageView (to eventually be able to drag one image over the other and make it snap to the same position by getting the position of the image I dragged over).
My example has two ImageViews with the id imageView (left) and imageView2(right).
In my example I'm not dragging anything yet, I just want to touch the left image, see "Action was down" in the log and lift the finger over the right image showing "Action was up2".
I don't know if this is easily possible.
As far as I can tell from testing, the MotionEvent.ACTION_UP only fires for an ImageView when you also pressed down on it beforehand. So when I release on top of imageView2 it only shows "Action was up" from the left image.
I wondered if it was possible by playing with return false, since the return value tells if an ActionEvent is consumed so I thought if the UP event of imageView returns false, maybe it does trigger the UP event of imageView2 but no. (Either complete misunderstanding on my part or it doesn't recognise UP on the second because it didn't start with a DOWN and MotionEvents probably always have to start with a DOWN).
public class MainActivity extends Activity {
ImageView imageView;
ImageView imageView2;
String DEBUG_TAG = "action";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageView);
imageView2 = findViewById(R.id.imageView2);
imageView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
//int action = MotionEventCompat.getActionMasked(event);
int action = event.getActionMasked();
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN"+v.toString());
return true;
case (MotionEvent.ACTION_MOVE) :
//Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP"+v.toString());
return false;
default :
//return true;
}
return true;
}
});
imageView2.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
//int action = MotionEventCompat.getActionMasked(event);
int action = event.getActionMasked();
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN2"+v.toString());
return true;
case (MotionEvent.ACTION_MOVE) :
//Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP2"+v.toString());
return true;
default :
//return true;
}
return true;
}
});
}
}
If there is no simple way to do this, I'm thinking about solving this mathematically, but maybe some of you can help.
So my question is, is there a way to recognise an UP action on a second ImageView while currently being in a MotionEvent of another ImageView?
SOLUTION (see emandt's answer)
I ditched the second OnClickListener because I realised that the 2nd image doesn't need any, I just need its position.
Added this method:
#Nullable
private View getDroppedView(View droppedView, int x, int y, List<View> arrayOfPossibilities) {
Rect cVisibleBoundsRect = new Rect();
for (View cView : arrayOfPossibilities) {
//if currently iterated view doesn't have values for getGlobalVisibleRect, skip the .contains part
//ignore the item which is your current active item (which would potentially be dropped)
//getGlobalVisibleRect sets cVisibleBoundsRect immediately to the Rect given as parameter
if (!cView.getGlobalVisibleRect(cVisibleBoundsRect)||(cView.equals(droppedView))) continue;
if (cVisibleBoundsRect.contains(x, y)) {
Log.d(DEBUG_TAG,"Found something");
//THIS "cView" IS THE VIEW WHERE YOU RELEASED THE FINGER
return cView;
}
}
Log.d(DEBUG_TAG,"Found nothing");
return null;
}
And added this in onUP:
case (MotionEvent.ACTION_UP) :
View dropTarget;
Log.d(DEBUG_TAG,"Action was UP"+v.toString());
dropTarget = getDroppedView(v, (int)event.getRawX(), (int)event.getRawY(), listOfViews);
if (dropTarget != null){
v.setX(dropTarget.getX());
v.setY(dropTarget.getY());
}
I think you want to know which is the View where you release the finger from the screen, am I right?
To do this you can use the same "View.OnTouchListener()" for all of your Views and in the ACTION_UP you have to call a new method similar to this (pseudo-code):
....
case (MotionEvent.ACTION_UP) :
View[] cArrayOfPossibileViews = new View[]{ findViewById(IMAGE_1), findViewById(IMAGE2) }
getDroppedView(v, event.getRawX(), event.getRawY(), cArrayOfPossibileViews);
break;
}
....
#Nullable
private View getDroppedView(View view, int x, int y, View[] arrayOfPossibilities) {
Rect cVisibleBoundsRect = new Rect();
for (View cView : arrayOfPossibilities) {
if (!cView.getGlobalVisibleRect(cVisibleBoundsRect)) continue;
if (cVisibleBoundsRect.contains(x, y)) {
//THIS "cView" IS THE VIEW WHERE YOU RELEASED THE FINGER
return cView;
}
}
return null;
}
This method get View bounds and compare them avains X and Y of your Touch Event. If X and Y are contained inside a View bounds it means that View is the one you need.
I thought this is a pretty relevant and common question, but I couldnt find an answer.
At the moment I have this method:
public boolean onTouchEvent (MotionEvent evt){
if (evt.getAction() == MotionEvent.ACTION_DOWN) {
do stuff ...
}
}
So if the user taps on the screen (wherever) the code is executed. Now I want the distinction between the right side of the display and the left side (left side means --> go back).
You can do this many ways. Here is one of them:
Attach onTouch listener to the view, which stretches to its edges. (For example your RelativeLayout which holds rest of views)
private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
float halfOfAScreen = mainLayout.getMaxWidth() / 2;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float fingerPosition = event.getX();
if(fingerPosition < halfOfAScreen) {
onBackPressed();
}
return true;
default:
return false;
}
}
};
Refer to this post on how to get touch position.
It seems in your case you will use
int x = (int)event.getX();
int y = (int)event.getY();
and work within the bounds of your layout that you want the app to react to.
I have a RelativeLayout that contains many child views with various touch events. I want to get notified when the user swipes anywhere on the parent RelativeLayout so I can update some UI while still letting the child views handle their own touch/drag events. What is the standard way of accomplishing this for Android?
I was thinking that I could put an overlay over all the views and have it detect swipe gestures and if it wasn't a swipe I could pass the touch event on to other views in the hierarchy. It doesn't seem like Android supports that sort of touch detection and once one view decides to see if a event is a certain gesture no other views will be able to see the events.
A swipe gesture consists of three touch events: ACTION_DOWN, ACTION_MOVE and ACTION_UP. You need to record all three events and then see if it was a swipe or not. If it was not a swipe then we would need to pass those events to other child views to see if it meets their criteria for the gesture they are looking for. If it is a swipe we would want to block the events from being sent to the child view. Just not sure if this is actually possible.
Update
Using the ideas of the users in the answers section I was able to write a layout that met my specification. This RelativeLayout just handles right and left swipes but could be added to to handle more directions. OnSwipeListener is just an interface with two methods void swipedLeft() and void swipedRight().
public class SwipeRelativeLayout extends RelativeLayout {
public OnSwipeListener mSwipeListener = null;
private static final int SWIPE_DISTANCE_THRESHOLD = 100;
private float mStartX = 0;
private float mStartY = 0;
private float mEndX = 0;
private float mEndY = 0;
public SwipeRelativeLayout(Context context) {
super(context);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean handled = onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) return handled;
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mStartX = event.getRawX();
mStartY = event.getRawY();
break;
}
case MotionEvent.ACTION_MOVE: {
float distanceX = event.getRawX() - mStartX;
float distanceY = event.getRawY() - mStartY;
if (Math.abs(distanceX) > Math.abs(distanceY) && Math.abs(distanceX) > SWIPE_DISTANCE_THRESHOLD) {
if (distanceX > 0) {
if (mSwipeListener != null) mSwipeListener.swipedRight();
} else {
if (mSwipeListener != null) mSwipeListener.swipedLeft();
}
return true;
}
return false;
}
}
return true;
}
}
When a touch event occurs it is first passed to the parent layout view and passed on to child view via onInterceptTouchEvent returning true or false. You want to override and intercept the touch events on the parent RelativeLayout and determine if you have seen a swipe gesture or not. If you have seen a swipe you want to return that you have handled it. In this case ACTION_UP is the end of a possible swipe and if your onTouchEvent handled the event then you can return true and the views below it will not get the finishing event and thus ignore their gestures.
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean handled = onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) return handled;
return false;
}
I posted a question yesterday [ Swipable Linear/Relative Layout ]. The question did not get any answers. Meanwhile, I tried to solve my own problem. What I did is that I used the OnTouchListener to detect swipe and thus toggle the scientific part of the calculator (Read the question I posted earlier to get a clear idea) I used the following code :-
RelativeLayout layout = (RelativeLayout)findViewById(R.id.advanced);
layout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
downX = event.getX();
return true;
}
case MotionEvent.ACTION_MOVE: {
upX = event.getX();
float deltaX = downX - upX;
swipe((int) deltaX);
return true;
}
case MotionEvent.ACTION_UP: {
downX = 0;
}
}
return true;
}
and the swipe method is :-
private void swipe(int distance) {
RelativeLayout layout = (RelativeLayout)findViewById(R.id.advanced);
LinearLayout.LayoutParams head_params = (LinearLayout.LayoutParams)layout.getLayoutParams();
head_params.setMargins(-distance, 0, 0, 0); //substitute parameters for left, top, right, bottom
layout.setLayoutParams(head_params);
}
The code works fine and achieves what it was meant to be till a certain extent. The problem is that, the swipe is buggy,i.e., it kind of flashes when I swipe it and also, it is very slow. Please tell me how to implement this correctly. Shall I use ViewPager or something else. Please enlighten me. Thanks
The reason this is buggy is because you've registered a touch listener on the view you're moving as well. This causes the touch listener to return 'twitchy' values. One approach you could take is putting the listener on a parent view that is stationary, and using that to update the child view. I would also recommend looking into using a GestureDetector instead, it does most of the touch logic for you and provides some very useful methods through a listener.
I have an ImageView inside of a view pager with an ActionBar at the top. I would like to be able to single tap to hide the action bar, and I would also like to be able to pinch zoom and pan on each ImageView.
To implement the single tap to hide the action bar I have a simple OnClickListener that hides it.
To implement the pinch zoom and pan on each ImageView I am using the PhotoView Library Project.
I am having issues because only one touch event listener can be associated with an ImageView, and the implementing the PhotoView Library project overwrites my OnClickListener to hide the ActionBar with,
parent.requestDisallowInterceptTouchEvent(true);
I am not sure how to go about getting both implemented at the same time. It seems like the only solution is to create my own Pinch Zoom ImageView in order to control touch events myself.
Found out that the PhotoView library actually allows me to set onViewTap for the PhotoViewAttacher object which is exactly what I wanted.
To create the PhotoViewAttacher in the current Fragment/Activity have it implement PhotoViewAttacher.OnViewTapListener, create the attacher,
PhotoViewAttacher mAttacher = new PhotoViewAttacher(imageView);
mAttacher.setOnViewTapListener(this);
and add the following function,
public void onViewTap(View view, float x, float y) {
// your code here
}
Source
You'll have to override the PhotoView library itself. If you look at the source code, the PhotoViewAttacher class is the one that handles the onTouch events.
You'll have to add the special funcionality you're looking for at this part of the code (specially, the ACTION_DOWN) event:
#Override
public final boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
if (mZoomEnabled && hasDrawable((ImageView) v)) {
ViewParent parent = v.getParent();
switch (ev.getAction()) {
case ACTION_DOWN:
// First, disable the Parent from intercepting the touch
// event
if (null != parent)
parent.requestDisallowInterceptTouchEvent(true);
else
Log.i(LOG_TAG, "onTouch getParent() returned null");
// If we're flinging, and the user presses down, cancel
// fling
cancelFling();
break;
case ACTION_CANCEL:
case ACTION_UP:
// If the user has zoomed less than min scale, zoom back
// to min scale
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();
if (null != rect) {
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
}
// Check to see if the user double tapped
if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
if (!handled && null != parent) {
parent.requestDisallowInterceptTouchEvent(false);
}
// Finally, try the Scale/Drag detector
if (null != mScaleDragDetector
&& mScaleDragDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}