0

I have FrameLayout encapsulating two ImageViews. Here is what I want:

Move the entire layout as and when touched and dragged by the user but when there is a click, it should be handled by the individual ImageViews(through their respective View.OnClickListener())

Current behavior: Currently, when I try to drag the view group, it jumps randomly once(per drag event) and then starts to move with the finger. So, it's like you are moving your finger outside the view group and it's moving.

Secondly, there are no click events being routed to the child ImageViews

What I have tried:

I have tried extending the FrameLayout that is encapsulating the ImageViews:

public class ChatHeadFrameLayout extends FrameLayout {

    /**
     * Store the initial touch down x coordinate
     */
    private float initialTouchX;
    /**
     * Store the initial touch down y coordinate
     */
    private float initialTouchY;

    public ChatHeadFrameLayout(@NonNull Context context) {
        super(context);
    }

    public ChatHeadFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public ChatHeadFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(21)
    public ChatHeadFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // If the event is a move, the ViewGroup needs to handle it and return
        // the indication here
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                //remember the initial position
                initialTouchX = event.getRawX();
                initialTouchY = event.getRawY();
                return true;
        }
        return false;

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Will be called in case we decide to intercept the event
        // Handle the view move-with-touch behavior here

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                //remember the initial position
                initialTouchX = event.getRawX();
                initialTouchY = event.getRawY();
                return true;

            case MotionEvent.ACTION_UP:
                int Xdiff = (int) (event.getRawX() - initialTouchX);
                int Ydiff = (int) (event.getRawY() - initialTouchY);


                //The check for Xdiff <10 && YDiff< 10 because sometime elements moves a little while clicking.
                //So that is click event.
                        if (Xdiff < 10 && Ydiff < 10) {
                            return false;
                        }
                return true;

            case MotionEvent.ACTION_MOVE:
                //Calculate the X and Y coordinates of the view.
                setX(event.getRawX() - initialTouchX);
                setY(event.getRawY() - initialTouchY);


                //Update the layout with new X & Y coordinate
                invalidate();

                return true;
        }
        return false;
    }
}

So, what is wrong with this code?

Manish Kumar Sharma
  • 12,982
  • 9
  • 58
  • 105

2 Answers2

1

First, the result of getRawX() is translated to the screen 0,0, getX() is relative to its own view.

Also, the cumulative of initialX and getCurrentX will be only the value from the drag, you need to store the screen x and use it with the drag OR calculate every difference of ACTION_MOVE and use it

            //remember the initial position
            initialTouchX = event.getX();
            initialTouchY = event.getY();
            screenX = getX();
            screenY = getY();

Then ON_MOVE

            setX(screenX + event.getX() - initialTouchX);
            setY(screenY + event.getY() - initialTouchY);

Second, you must call super.onTouchEvent(MotionEvent.obtain(event)) or super.dispatchTouchEvent maybe as the first line from onTouchEvent cause your view are consuming all events.

Marcos Vasconcelos
  • 18,136
  • 30
  • 106
  • 167
  • I figured it out. While your getX() (i.e, the relative distance strategy) suggestion worked for right calculation of offsets, I had to work out a different strategy for the overall requirement. See my solution. – Manish Kumar Sharma Oct 18 '17 at 08:36
  • That was the solution I proposed. If this answer helps please upvote, if it solves please accept it as correct – Marcos Vasconcelos Oct 18 '17 at 12:37
  • Your proposed solution lacks the following: 1) Proposal for calling `super.onInterceptTouchEvent(event);` which frankly is the key piece of the solution 2) Describing which method should be implemented, `onTouchEvent()` or `onInterceptTouchEvent()` -- I ended up spending much time in getting the flow right. Have an upvote for the calculations. – Manish Kumar Sharma Oct 18 '17 at 15:23
  • Oh right, you did asked two questions, sorry not awsnering the other one – Marcos Vasconcelos Oct 18 '17 at 15:39
0

It turned out to be much simple than I had expected(see the explanation in the comment below):

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // The plan is to intercept the events here and if,
        // 1) The move is an ACTION_DOWN, return false indicating we want more events
        // to be able to ascertain if we want this ViewGroup to move or the children
        // views to handle the click event.
        // 2) On receiving the ACTION_MOVE, of course, move the ViewGroup but the sure short
        // indication of click would be to check it for in ACTION_DOWN with threshold of 5px.
        // In that case, we would return super.onInterceptTouchEvent(event); meaning, let the
        // event handling happen normally without interception
        // (Remember - the ACTION_DOWN had been recorded by the children previously when we
        // returned false and on receiving the ACTION_UP, it would constitute the complete event)
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                // remember the initial position
                initialTouchX = event.getX();
                //Log.d(TAG, "onInterceptTouchEvent().ACTION_DOWN.initialTouchX: " + initialTouchX);
                initialTouchY = event.getY();
                //Log.d(TAG, "onInterceptTouchEvent().ACTION_DOWN.initialTouchY: " + initialTouchY);
                return false;

            case MotionEvent.ACTION_UP:
                int xDiff = (int) (event.getX() - initialTouchX);
                int yDiff = (int) (event.getY() - initialTouchY);


                //The check for Xdiff <5 && YDiff< 5 because sometime elements moves a little while clicking.
                //So that is click event.
                if (xDiff < 5 && yDiff < 5) {
                    return super.onInterceptTouchEvent(event);
                }
                return false;

            case MotionEvent.ACTION_MOVE:

                int moveX = (int) (event.getX() - initialTouchX + getX());
                int moveY = (int) (event.getY() - initialTouchY + getY());

                setX(moveX);
                setY(moveY);

                //Update the layout with new X & Y coordinate
                invalidate();
        }
        return false;

    }

There is a Caveat with this solution: The click event is solely based on the distance moved. Normally, to differentiate between the click and long-click, you would also do something like this:

private long lastTouchDown;
private static int CLICK_ACTION_THRESHHOLD = 200;

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastTouchDown = System.currentTimeMillis();
            break;
        case MotionEvent.ACTION_UP:
            if (System.currentTimeMillis() - lastTouchDown < CLICK_ACTION_THRESHHOLD) {
                Log.w("App", "You clicked!");
            }
            break;
    }
    return true;
}

P.S: There is no requirement of overriding the onTouchEvent() method in the presented solution.

Manish Kumar Sharma
  • 12,982
  • 9
  • 58
  • 105