android: move a view on touch move (ACTION_MOVE)
Asked Answered
E

13

216

I'd like to do a simple control: a container with a view inside. If I touch the container and I move the finger, I want to move the view to follow my finger.

What kind of container (layout) should I use? How to do this?

I don't need to use a surface, but a simple layout.

Endanger answered 22/2, 2012 at 15:45 Comment(1)
Here is an example thegeekyland.blogspot.com/2015/12/…Ex
L
252

Something like this:

public class MyActivity extends Activity implements View.OnTouchListener {

TextView _view;
ViewGroup _root;
private int _xDelta;
private int _yDelta;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    _root = (ViewGroup)findViewById(R.id.root);

    _view = new TextView(this);
    _view.setText("TextView!!!!!!!!");

    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(150, 50);
    layoutParams.leftMargin = 50;
    layoutParams.topMargin = 50;
    layoutParams.bottomMargin = -250;
    layoutParams.rightMargin = -250;
    _view.setLayoutParams(layoutParams);

    _view.setOnTouchListener(this);
    _root.addView(_view);
}

public boolean onTouch(View view, MotionEvent event) {
    final int X = (int) event.getRawX();
    final int Y = (int) event.getRawY();
    switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            RelativeLayout.LayoutParams lParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
            _xDelta = X - lParams.leftMargin;
            _yDelta = Y - lParams.topMargin;
            break;
        case MotionEvent.ACTION_UP:
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            break;
        case MotionEvent.ACTION_POINTER_UP:
            break;
        case MotionEvent.ACTION_MOVE:
            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
            layoutParams.leftMargin = X - _xDelta;
            layoutParams.topMargin = Y - _yDelta;
            layoutParams.rightMargin = -250;
            layoutParams.bottomMargin = -250;
            view.setLayoutParams(layoutParams);
            break;
    }
    _root.invalidate();
    return true;
}}

In main.xml just RelativeLayout with @+id/root

Layman answered 22/2, 2012 at 16:29 Comment(15)
@appserv: Nice Job!! But I wonder why you put layoutPrarms.rightMargin = -250 and the same with bottomMargin!! Can you explain it? Anyway, thank you very much!!Martinmartina
If my memory serves me, without these values, the view will be compressed when moving it right or down. You can try to change them and see what happen.Layman
If I implement _view in main.xml file, can I move the _view? And if not? Why? Thanks you!!Martinmartina
Why X and Y has to be final?Clearcole
They don't have to be final. I made them final just for avoid reassign these variables.Layman
Its working fine but Is there any way to restrict the move from outside the screen that means should move only inside screen boundary..Jameyjami
@VyacheslavShilkin The only problem I found in this code is I couldn't make layouts inflated from xml file to move. Is this really the issue of the code or am I missing because of my ignorance?Painterly
Perfect, but could you explain why the View will be compressed if I removed layoutPrarms.rightMargin = -250 and layoutPrarms.bootmMargin = -250Pyrotechnic
In some situations one will have to use FrameLayout. RelativeLayout works in simple situations but not with nested viewsLecompte
@VyacheslavShilkin I have posted a question and used this answer but not getting desired result can you help me with my question? https://mcmap.net/q/128368/-how-to-hold-and-drag-re-position-a-layout-along-with-its-associated-layouts-in-android/2311051Fanfare
@VyacheslavShilkin why not _view.layout(left, top, right, bottom) ?Amblygonite
if view is bigger it is compressing it anyway on bottom and right - calling onDraw()Sandpiper
@Pyrotechnic hey.. I think the reason is because we want to give the view more room to move. If right and bottom margin is set to zero, the view can only be moved near the border. Let's say your view width is 100px, when moved to the right border, it will start shrinking when there is less than 100px of space between your finger and the border. So setting a negative margin. it will move the margin outside of your screen, hence no shrinking or compressing occurs.Undenominational
Its working fine but Is there any way to restrict the move from outside the screen that means should move only inside screen boundary. ping me [email protected]Kinelski
The OnTouch returns true if this listener has consumed this event, false otherwise. In this question, obviously you should not return true for all events. you should return true only for those events that you has consumed, return false for others.Mourant
N
408

I've found an easy approach to do that with the ViewPropertyAnimator:

float dX, dY;

@Override
public boolean onTouch(View view, MotionEvent event) {

    switch (event.getAction()) {

        case MotionEvent.ACTION_DOWN:

            dX = view.getX() - event.getRawX();
            dY = view.getY() - event.getRawY();
            break;

        case MotionEvent.ACTION_MOVE:

            view.animate()
                    .x(event.getRawX() + dX)
                    .y(event.getRawY() + dY)
                    .setDuration(0)
                    .start();
            break;
        default:
            return false;
    }
    return true;
}
Naxos answered 27/6, 2015 at 22:49 Comment(15)
@ruan65 can I restrict the view to not get dragged out my screen?Rosannrosanna
Best answer so far, and I have found different approaches. @Dhiraj - Easily, just find screen width and height and put boundaries in if clauses. DisplayMetrics displaymetrics = new DisplayMetrics(); windowManager.getDefaultDisplay().getMetrics(displaymetrics); final int height = displaymetrics.heightPixels; final int width = displaymetrics.widthPixels;Write
However, if I add a child to the layout such as FrameLayout, that child is not movable. Also, this code catches coordinates, wherever you click, and not only on the view that should be moved..Write
@Write to avoid catches redundant coordinates use getAction() instead of using getActionMasked(). I'm using getActionMasked() to have possibility move two objects simultaneously.Naxos
how to move a view in 3 directions only . like upSwipe ,left and right and disable downside up ^ | left<---- | ---->rightFeeley
But this code doesn't let an onclick listener to be attached to the view. Returning true in the ontouch() invokes the onclick() on every move and returning false doesnt invoke the onclick at allAdvised
If someone was as confused as I to why this works then just know that getX() returns an X coordinate that is relative to the view, while getRawX() returns an absolute coordinate, relative to the device screen. https://mcmap.net/q/128367/-difference-between-motionevent-getrawx-and-motionevent-getxFamily
Genius, just added some bounds checking and it works great for horizontal scrolling of a slider buttonSeraglio
While this works just like the previous answer its better to use translationX and translationY methods in your move event. To make the position persistant set the layout properties of the view in the "up" event. Translation methods are using hardware layer of your phone. Layout properties not.Seer
Couldn't make this work for my case. Found that using the GestureDetector might be the easiest way: gist.github.com/slightfoot/a7de4ecdc6cf06c68640Qualified
While I try this, I only got a movable fab inside the red box space. red box is imaginary i.stack.imgur.com/a5o9j.pngMonotype
@Andrey have a look at my question too. #51889507Pintle
We can also use setX and setY directly, instead of applying an animation of duration 0.Adim
OK. So the funny thing is I copied and pasted it in my code that already done pinch zoom, and this just works... first time anything works the first time in android for me :DBurson
A simple yet powerful solution for me what I exactly want. Thank you so muchJudijudicable
L
252

Something like this:

public class MyActivity extends Activity implements View.OnTouchListener {

TextView _view;
ViewGroup _root;
private int _xDelta;
private int _yDelta;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    _root = (ViewGroup)findViewById(R.id.root);

    _view = new TextView(this);
    _view.setText("TextView!!!!!!!!");

    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(150, 50);
    layoutParams.leftMargin = 50;
    layoutParams.topMargin = 50;
    layoutParams.bottomMargin = -250;
    layoutParams.rightMargin = -250;
    _view.setLayoutParams(layoutParams);

    _view.setOnTouchListener(this);
    _root.addView(_view);
}

public boolean onTouch(View view, MotionEvent event) {
    final int X = (int) event.getRawX();
    final int Y = (int) event.getRawY();
    switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            RelativeLayout.LayoutParams lParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
            _xDelta = X - lParams.leftMargin;
            _yDelta = Y - lParams.topMargin;
            break;
        case MotionEvent.ACTION_UP:
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            break;
        case MotionEvent.ACTION_POINTER_UP:
            break;
        case MotionEvent.ACTION_MOVE:
            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
            layoutParams.leftMargin = X - _xDelta;
            layoutParams.topMargin = Y - _yDelta;
            layoutParams.rightMargin = -250;
            layoutParams.bottomMargin = -250;
            view.setLayoutParams(layoutParams);
            break;
    }
    _root.invalidate();
    return true;
}}

In main.xml just RelativeLayout with @+id/root

Layman answered 22/2, 2012 at 16:29 Comment(15)
@appserv: Nice Job!! But I wonder why you put layoutPrarms.rightMargin = -250 and the same with bottomMargin!! Can you explain it? Anyway, thank you very much!!Martinmartina
If my memory serves me, without these values, the view will be compressed when moving it right or down. You can try to change them and see what happen.Layman
If I implement _view in main.xml file, can I move the _view? And if not? Why? Thanks you!!Martinmartina
Why X and Y has to be final?Clearcole
They don't have to be final. I made them final just for avoid reassign these variables.Layman
Its working fine but Is there any way to restrict the move from outside the screen that means should move only inside screen boundary..Jameyjami
@VyacheslavShilkin The only problem I found in this code is I couldn't make layouts inflated from xml file to move. Is this really the issue of the code or am I missing because of my ignorance?Painterly
Perfect, but could you explain why the View will be compressed if I removed layoutPrarms.rightMargin = -250 and layoutPrarms.bootmMargin = -250Pyrotechnic
In some situations one will have to use FrameLayout. RelativeLayout works in simple situations but not with nested viewsLecompte
@VyacheslavShilkin I have posted a question and used this answer but not getting desired result can you help me with my question? https://mcmap.net/q/128368/-how-to-hold-and-drag-re-position-a-layout-along-with-its-associated-layouts-in-android/2311051Fanfare
@VyacheslavShilkin why not _view.layout(left, top, right, bottom) ?Amblygonite
if view is bigger it is compressing it anyway on bottom and right - calling onDraw()Sandpiper
@Pyrotechnic hey.. I think the reason is because we want to give the view more room to move. If right and bottom margin is set to zero, the view can only be moved near the border. Let's say your view width is 100px, when moved to the right border, it will start shrinking when there is less than 100px of space between your finger and the border. So setting a negative margin. it will move the margin outside of your screen, hence no shrinking or compressing occurs.Undenominational
Its working fine but Is there any way to restrict the move from outside the screen that means should move only inside screen boundary. ping me [email protected]Kinelski
The OnTouch returns true if this listener has consumed this event, false otherwise. In this question, obviously you should not return true for all events. you should return true only for those events that you has consumed, return false for others.Mourant
T
15

Touch the container and the view will follow your finger.

xml code

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/floating_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

    <ImageView
      android:id="@+id/btn_chat"
      android:layout_width="42dp"
      android:layout_height="42dp"
      />
    
<LinearLayout>

Java code

public class DashBoardActivity extends Activity implements View.OnClickListener, View.OnTouchListener {
    
    float dX;
    float dY;
    int lastAction;
    LinearLayout floatingLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_dashboard);

        floatingLayout = findViewById(R.id.floating_layout);
        floatingLayout.setOnTouchListener(this);    
    
    
    
     @Override
    public boolean onTouch(View view, MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                dX = view.getX() - event.getRawX();
                dY = view.getY() - event.getRawY();
                lastAction = MotionEvent.ACTION_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                view.setY(event.getRawY() + dY);
                view.setX(event.getRawX() + dX);
                lastAction = MotionEvent.ACTION_MOVE;
                break;

            case MotionEvent.ACTION_UP:
                if (lastAction == MotionEvent.ACTION_DOWN)
                    Toast.makeText(DashBoardActivity.this, "Clicked!", Toast.LENGTH_SHORT).show();
                break;

            default:
                return false;
        }
        return true;
    }
}
Towle answered 16/9, 2019 at 12:46 Comment(0)
P
13

Following the @Andrew approach, if you want to move the view from its center, you only have to substract the view's half height and half width to the movement.

float dX, dY;

@Override
public boolean onTouchEvent(View view, MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            dX = view.getX() - event.getRawX();
            dY = view.getY() - event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            view.animate()
                .x(event.getRawX() + dX - (view.getWidth() / 2f))
                .y(event.getRawY() + dY - (view.getHeight() / 2f))
                .setDuration(0)
                .start();
            break;
        default:
            return false;
    }
    return true;
}     
Preordain answered 16/5, 2017 at 7:43 Comment(0)
E
12

Create a custom touch listener class (in Kotlin):

(This code restrict your view from dragging out of its parent view)

class CustomTouchListener(
  val screenWidth: Int, 
  val screenHeight: Int
) : View.OnTouchListener {
    private var dX: Float = 0f
    private var dY: Float = 0f

    override fun onTouch(view: View, event: MotionEvent): Boolean {

        val newX: Float
        val newY: Float

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                dX = view.x - event.rawX
                dY = view.y - event.rawY
            }
            MotionEvent.ACTION_MOVE -> {

                newX = event.rawX + dX
                newY = event.rawY + dY

                if ((newX <= 0 || newX >= screenWidth - view.width) || (newY <= 0 || newY >= screenHeight - view.height)) {
                    return true
                }

                view.animate()
                    .x(newX)
                    .y(newY)
                    .setDuration(0)
                    .start()
            }
        }
        return true
    }
}

How to use it?

parentView.viewTreeObserver.addOnGlobalLayoutListener { view.setOnTouchListener(CustomTouchListener(parentView.width, parentView.height)) }

parentView is the parent of your view.

Edmonds answered 7/7, 2019 at 7:28 Comment(2)
Great!!, and simple solution for kotlin.Karolinekaroly
you are a saviourLazor
K
5

Same implementation in Kotlin

    rightPanel.setOnTouchListener(View.OnTouchListener { view, event ->
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {

                rightDX = view!!.x - event.rawX
                // rightDY = view!!.getY() - event.rawY;

            }
            MotionEvent.ACTION_MOVE -> {

                var displacement = event.rawX + rightDX

                view!!.animate()
                        .x(displacement)
                        // .y(event.getRawY() + rightDY)
                        .setDuration(0)
                        .start()
            }
            else -> { // Note the block
                return@OnTouchListener false
            }
        }
        true
 })
Kalgoorlie answered 23/1, 2019 at 13:34 Comment(1)
Funny thing about this answer is I wrote it and It helped me multiple time.Kalgoorlie
D
4

I recommend to use view.translationX and view.translationY to move your views.

Kotlin snippet:

yourView.translationX = xTouchCoordinate
yourView.translationY = yTouchCoordinate
Dustcloth answered 5/6, 2019 at 8:39 Comment(0)
A
2

In the code below, I've created something called the RegionView (git), which is a reusable container responsible for managing drag and zoom operations for each of its nested children.

Here, we manipulate the top and left coefficients of a child View's LayoutParams to simulate movement about the diagram. By decoupling the interpretation of handling what's understood as a drag operation, and what is determined to be a scale operation, we can provide reliable manipulation of a child View.

package com.zonal.regionview;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Vibrator;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.RelativeLayout;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by Alexander Thomas (@Cawfree) on 20/07/2017.
 */

/** Enables users to customize Regions Of Interest on a Canvas. */
public class RegionView extends RelativeLayout implements View.OnTouchListener, GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener {

    /* Member Variables. */
    private final GestureDetector      mGestureDetector;
    private final ScaleGestureDetector mScaleGestureDetector;
    private final Map<Integer, View>   mViewMap;
    private       boolean              mScaling;
    private       float                mScale;
    private       boolean              mWrapContent;
    private       boolean              mDropOnScale;

    public RegionView(Context context) {
        // Implement the Parent.
        super(context);
        // Initialize Member Variables.
        this.mGestureDetector      = new GestureDetector(context, this);
        this.mViewMap              = new HashMap<>();
        this.mScaleGestureDetector = new ScaleGestureDetector(context, this);
        this.mScaling              = false;
        this.mScale                = Float.NaN;
        this.mWrapContent          = false;
        this.mDropOnScale          = false;
        // Register ourself as the OnTouchListener.
        this.setOnTouchListener(this);
    }

    public RegionView(Context context, @Nullable AttributeSet attrs) {
        // Implement the Parent.
        super(context, attrs);
        // Initialize Member Variables.
        this.mGestureDetector      = new GestureDetector(context, this);
        this.mViewMap              = new HashMap<>();
        this.mScaleGestureDetector = new ScaleGestureDetector(context, this);
        this.mScaling              = false;
        this.mWrapContent          = false;
        this.mDropOnScale          = false;
        // Register ourself as the OnTouchListener.
        this.setOnTouchListener(this);
    }

    public RegionView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        // Implement the Parent.
        super(context, attrs, defStyleAttr);
        // Initialize Member Variables.
        this.mGestureDetector      = new GestureDetector(context, this);
        this.mViewMap              = new HashMap<>();
        this.mScaleGestureDetector = new ScaleGestureDetector(context, this);
        this.mScaling              = false;
        this.mWrapContent          = false;
        this.mDropOnScale          = false;
        // Register ourself as the OnTouchListener.
        this.setOnTouchListener(this);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public RegionView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        // Implement the Parent.
        super(context, attrs, defStyleAttr, defStyleRes);
        // Initialize Member Variables.
        this.mGestureDetector      = new GestureDetector(context, this);
        this.mViewMap              = new HashMap<>();
        this.mScaleGestureDetector = new ScaleGestureDetector(context, this);
        this.mScaling              = false;
        this.mWrapContent          = false;
        this.mDropOnScale          = false;
        // Register ourself as the OnTouchListener.
        this.setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(final View v, final MotionEvent event) {
        // Calculate the PointerId.
        final int lPointerId = event.getPointerId(event.getActionIndex());
        // Handle the TouchEvent.
        this.getGestureDetector().onTouchEvent(event);
        this.getScaleGestureDetector().onTouchEvent(event);
        // Did the user release a pointer?
        if(event.getAction() == MotionEvent.ACTION_UP) {
            // Was there a View associated with this Action?
            final View lView = this.getViewMap().get(lPointerId);
            // Does the View exist?
            if(lView != null) {
                // Remove the View from the Map.
                this.getViewMap().remove(lPointerId); /** TODO: Provide a Callback? */
            }
        }
        // Consume all events for now.
        return true;
    }

    @Override
    public boolean onDown(MotionEvent e) {
        // Calculate the PointerId.
        final Integer lPointerId = Integer.valueOf(e.getPointerId(e.getActionIndex()));
        // Fetch the View.
        final View    lView      = this.getViewFor(Math.round(e.getRawX()), Math.round(e.getRawY()));
        // Is it valid?
        if(lView != null) {
            // Watch the View.
            this.getViewMap().put(lPointerId, lView);
            // Configure the Anchor.
            lView.setPivotX(0);
            lView.setPivotY(0);
            // Assert that we handled the event.
            return true;
        }
        // Assert that we ignored the event.
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        // Are we not scaling?
        if(!this.isScaling()) {
            // Calculate the PointerId.
            final Integer lPointerId = Integer.valueOf(e1.getPointerId(e1.getActionIndex()));
            // Fetch the View.
            final View    lView      = this.getViewMap().get(lPointerId);
            // Is the scroll valid for a given View?
            if(lView != null) {
                // Calculate the Scaled Width and Height of the View.
                final float lWidth    = lView.getWidth()  * lView.getScaleX();
                final float lHeight   = lView.getHeight() * lView.getScaleY();
                // Declare the initial position.
                final int[] lPosition = new int[] { (int)(e2.getX() - ((lWidth)  / 2)), (int)(e2.getY() - ((lHeight) / 2)) };
                // Are we wrapping content?
                if(this.isWrapContent()) {
                    // Wrap the Position.
                    this.onWrapContent(lPosition, lWidth, lHeight);
                }
                // Update the Drag.
                this.onUpdateDrag(lView, lPosition);
            }
            // Assert we handled the scroll.
            return true;
        }
        // Otherwise, don't permit scrolling. Don't consume the MotionEvent.
        return false;
    }

    /** Forces X/Y values to be coerced within the confines of the RegionView. */
    private final void onWrapContent(final int[] pPosition, final float pWidth, final float pHeight) {
        // Limit the parameters. (Top-Left)
        pPosition[0] = Math.max(pPosition[0], 0);
        pPosition[1] = Math.max(pPosition[1],  0);
        // Limit the parameters. (Bottom-Right)
        pPosition[0] = Math.min(pPosition[0], (int)(this.getWidth()  - pWidth));
        pPosition[1] = Math.min(pPosition[1], (int)(this.getHeight() - pHeight));
    }

    /** Updates the Drag Position of a child View within the Layout. Implicitly, we update the LayoutParams of the View. */
    private final void onUpdateDrag(final View pView, final int pLeft, final int pTop) {
        // Allocate some new MarginLayoutParams.
        final MarginLayoutParams lMarginLayoutParams = new MarginLayoutParams(pView.getLayoutParams());
        // Update the Margin.
        lMarginLayoutParams.setMargins(pLeft, pTop, 0, 0);
        // Refactor the MarginLayoutParams into equivalent LayoutParams for the RelativeLayout.
        pView.setLayoutParams(new RelativeLayout.LayoutParams(lMarginLayoutParams));
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        // Calculate the ScaleFactor.
              float lScaleFactor = detector.getScaleFactor() - 1;
        // Fetch the Scaled View.
        final View  lView        = this.getViewMap().entrySet().iterator().next().getValue();
        // Update the ScaleFactor.
        final float lScale       = this.getScale() + lScaleFactor;
        // Calculate the Proposed Width and Height.
        final int   lWidth  = Math.round(lView.getWidth()  * lScale);
        final int   lHeight = Math.round(lView.getHeight() * lScale);
        // Is the View already too large for wrap content?
        if(lWidth >= this.getWidth() || lHeight >= this.getHeight()) {
            // Don't update the scale.
            return false;
        }
        // Persist this Scale for the View.
        lView.setScaleX(lScale);
        lView.setScaleY(lScale);
        // Assign the Scale.
        this.setScale(lScale);
        // Compute the Position.
        final int[] lPosition = new int[] { Math.round(detector.getFocusX()) - (lWidth / 2), Math.round(detector.getFocusY()) - (lHeight / 2) };
        // Are we wrapping the Position?
        if(this.isWrapContent()) {
            // Wrap the Position.
            this.onWrapContent(lPosition, lWidth, lHeight);
        }
        // Update the Drag.
        this.onUpdateDrag(lView, lPosition);
        // Assert that we handled the scale.
        return true;
    }

    /** Update the Drag. */
    private final void onUpdateDrag(final View pView, final int[] pPosition) {
        // Call the sub-implementation.
        this.onUpdateDrag(pView, pPosition[0], pPosition[1]);
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) { 
        // Is the user not dragging at all?
        if(this.getViewMap().size() == 1) {
            // Fetch the View.
            final View lView = this.getViewMap().entrySet().iterator().next().getValue();
            // Initialize the Scale.
            this.setScale(lView.getScaleX()); 
            // Assert that we've started scaling.
            this.setScaling(true);
            // Inform the callback.
            return true;
        }
        // Otherwise, don't allow scaling.
        return false;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        // Were we scaling?
        if(this.isScaling()) {
            // Assert that we've stopped scaling.
            this.setScaling(false);
            // Reset the Scale.
            this.setScale(Float.NaN);
            // Should we stop dragging now that we've finished scaling?
            if(this.isDropOnScale()) {
                // Clear the ViewMap.
                this.getViewMap().clear();
            }
        }
    }

    /** Returns the View colliding with the given co-ordinates. */
    private final View getViewFor(final int pX, final int pY) {
        // Declare the LocationBuffer.
        final int[] lLocationBuffer = new int[2];
        // Iterate the Views.
        for(int i = 0; i < this.getChildCount(); i++) {
            // Fetch the child View.
            final View lView = this.getChildAt(i);
            // Fetch its absolute position.
            lView.getLocationOnScreen(lLocationBuffer);
            // Determine if the MotionEvent collides with the View.
            if(pX > lLocationBuffer[0] && pY > lLocationBuffer[1] && (pX < lLocationBuffer[0] + (lView.getWidth() * lView.getScaleX())) && (pY < lLocationBuffer[1] + (lView.getHeight() * lView.getScaleY()))) {
                // Return the View.
                return lView;
            }
        }
        // We couldn't find a View.
        return null;
    }

    /* Unused Overrides. */
    @Override public void      onShowPress(MotionEvent e) {  }
    @Override public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }
    @Override public void      onLongPress(MotionEvent e) { }
    @Override public boolean       onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }

    /* Getters and Setters. */
    private final GestureDetector getGestureDetector() {
        return this.mGestureDetector;
    }

    private final ScaleGestureDetector getScaleGestureDetector() {
        return this.mScaleGestureDetector;
    }

    private final Map<Integer, View> getViewMap() {
        return this.mViewMap;
    }

    private final void setScaling(final boolean pIsScaling) {
        this.mScaling = pIsScaling;
    }

    private final boolean isScaling() {
        return this.mScaling;
    }

    private final void setScale(final float pScale) {
        this.mScale = pScale;
    }

    private final float getScale() {
        return this.mScale;
    }

    /** Defines whether we coerce the drag and zoom of child Views within the confines of the Layout. */
    public final void setWrapContent(final boolean pIsWrapContent) {
        this.mWrapContent = pIsWrapContent;
    }

    public final boolean isWrapContent() {
        return this.mWrapContent;
    }

    /** Defines whether a drag operation is considered 'finished' once the user finishes scaling a view. */
    public final void setDropOnScale(final boolean pIsDropOnScale) {
        this.mDropOnScale = pIsDropOnScale;
    }

    public final boolean isDropOnScale() {
        return this.mDropOnScale;
    }

}

Here I show an example use case:

package com.zonal.regionview;

import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.AnalogClock;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Allocate a RegionView.
        final RegionView lRegionView = new RegionView(this);
        // Add some example items to drag.
        lRegionView.addView(new AnalogClock(this));
        lRegionView.addView(new AnalogClock(this));
        lRegionView.addView(new AnalogClock(this));
        // Assert that we only want to drag Views within the confines of the RegionView.
        lRegionView.setWrapContent(true);
        // Assert that after we've finished scaling a View, we want to stop being able to drag it until a new drag is started.
        lRegionView.setDropOnScale(true);
        // Look at the RegionView.
        this.setContentView(lRegionView);
    }

}
Arrive answered 21/7, 2017 at 15:50 Comment(0)
H
2

In this example you can move the view within it's parent bounds no matter it's size, flawless animation, and catch clicks.

The reason that this solution is superior to other comments is that this approach uses a Directional Pad which calculate itself and won't relay on the View positions which is a the source for a-lot of bugs.

// we could use this gameobject as a wrapper that controls the touch event of the component(the table)
// and like so, we can have a click event and touch events
public abstract class GameObjectStackOverflow {

private static final int CLICK_DURATION = 175;
protected View view;
protected ViewGroup container;
protected Context mContext;

private boolean onMove = false;
private boolean firstAnimation = true;
private Animator.AnimatorListener listener;

protected float parentWidth;
protected float parentHeight;

protected float xmlHeight;
protected float xmlWidth;

// Those are the max bounds
// whiting the xmlContainer
protected float xBoundMax;
protected float yBoundMax;

// This variables hold the target
// ordinates for the next
// animation in case an animation
// is already in progress.
protected float targetX;
protected float targetY;

private float downRawX;
private float downRawY;

public GameObjectStackOverflow(@NonNull Context context, @NonNull ViewGroup container)
{
    mContext = context;
    this.container = container;
}

// This method is the reason the constructor
// does not get view to work with in the first
// place. This method helps us to work with
// android main thread in such way that we
// separate the UI stuff from the technical
// stuff
protected View initGraphicView(@NonNull LayoutInflater inflater, int resource, boolean add)
{
    view = inflater.inflate(resource, container, add);
    view.post(getOnViewAttach());
    view.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return onTouchEvent(event);
        }
    });
    return view;
}

// This method attach an existing
// view that is already inflated
protected void attachGraphicView(@NonNull final View view)
{
    this.view = view;
    view.post(getOnViewAttach());
}

// This method is anti-boiler code.
// attaching runnable to the view
// task queue to finish the
// initialization of the game object.
private Runnable getOnViewAttach()
{
    return new Runnable() {
        @Override
        public void run() {
            parentHeight = container.getHeight();
            parentWidth = container.getWidth();
            view.setX(currentX);
            view.setY(currentY);
        }
    };
}

private void click() {
    // recover the view to the previous location [not needed]
    // not needed
    //view.animate()
    //    .x(prevPosX)
    //    .y(prevPosY)
    //    .setDuration(0)
    //    .start();
}

// maybe restore the View view, Motion event
public boolean onTouchEvent(MotionEvent event)
{
    view.getParent().requestDisallowInterceptTouchEvent(true);
    //if(!selected) return false;
    switch (event.getAction())
    {
        case MotionEvent.ACTION_UP:
            if (event.getEventTime() - event.getDownTime() < CLICK_DURATION) click(); // are you missing break here?
            onMove = false;
            // if needed to update network entity do it here
            break;
        case MotionEvent.ACTION_DOWN:
            firstAnimation = true;
            xBoundMax = parentWidth - xmlWidth;
            yBoundMax = parentHeight - xmlHeight;
            downRawX = event.getRawX();
            downRawY = event.getRawY();
            break;

        case MotionEvent.ACTION_MOVE:
            if (!onMove) {
                if (event.getEventTime() - event.getDownTime() < CLICK_DURATION) break;
                else onMove = true;
            }

            // Calculating the position the
            // view should be posed at.
            float offsetX = event.getRawX() - downRawX;
            float offsetY = event.getRawY() - downRawY;
            downRawX = event.getRawX();
            downRawY = event.getRawY();
            targetX = currentX + offsetX;
            targetY = currentY + offsetY;

            // Checking if view
            // is within parent bounds
            if (targetX > parentWidth - xmlWidth) targetX = xBoundMax;
            else if (targetX < 0) targetX = 0;
            if (targetY > parentHeight - xmlHeight) targetY = yBoundMax;
            else if (targetY < 0) targetY = 0;

            // This check is becuase the user may just click on the view
            // So if it's a not a click, animate slowly but fastly
            // to the desired position
            if (firstAnimation) {
                firstAnimation = false;
                animate(70, getNewAnimationListener());
                break;
            }

            if (listener != null) break;
            animate(0, null);
            break;

        case MotionEvent.ACTION_BUTTON_PRESS:
        default:
            return false;
    }
    return true;
}

// this method gets used only in
// one place. it's wrapped in a method
// block because i love my code like
// i love women - slim, sexy and smart.
public Animator.AnimatorListener getNewAnimationListener() {
    listener = new Animator.AnimatorListener() {
        @Override public void onAnimationStart(Animator animation) { }
        @Override public void onAnimationCancel(Animator animation) { }
        @Override public void onAnimationRepeat(Animator animation) { }
        @Override public void onAnimationEnd(Animator animation) {
            animation.removeListener(listener);
            listener = null;
            view.setAnimation(null);
            animate(0, null);
        }
    };
    return listener;
}

float currentX = 0, currentY = 0;

private void animate(int duration, @Nullable Animator.AnimatorListener listener) {
    view.animate()
            .x(targetX)
            .y(targetY)
            .setDuration(duration)
            .setListener(listener)
            .start();
        currentX = targetX;
        currentY = targetY;
}

protected void setSize(float width, float height)
{
    xmlWidth = width;
    xmlHeight = height;
    RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
    layoutParams.width = (int) width;
    layoutParams.height = (int) height;
    view.setLayoutParams(layoutParams);
}

public View getView() {
    return view;
}


//This interface catches the onclick even
// that happened and need to decide what to do.
public interface GameObjectOnClickListener {
    void onGameObjectClick(GameObjectStackOverflow object);
}

public float getXmlWidth() {
    return xmlWidth;
}

public float getXmlHeight() {
    return xmlHeight;
}
}

This version got stripped from the big stuff which used to have network entity that gets updated live and such, it should work.


you should use it this way

public class Tree extends GameObject
{
    public Tree(Context context, ViewGroup container, View view, int width, int height) {
        super(context, manager, container);
        attachGraphicView(view);
        super.setSize(_width, _height);
    }
}

and than

mTree= new Tree(mContext, mContainer, xmlTreeView);     
mTree.getView().setOnTouchListener(getOnTouchListener(mTree));

you should have this too but this can be easily removed

//Construct new OnTouchListener that reffers to the gameobject ontouchevent
private View.OnTouchListener getOnTouchListener(final GameObject object) {
    return new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            return object.onTouchEvent(event);
        }
    };
}

If you have the container inside a ScrollView or double dimension ScrollView you should add this line to the onTouch

view.getParent().requestDisallowInterceptTouchEvent(true);
Hamblin answered 6/2, 2020 at 9:48 Comment(0)
C
1

Changed a bit a solution provided by @Vyacheslav Shylkin to remove dependencies of manually entered numbers.

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.RelativeLayout;

public class MainActivity extends Activity implements View.OnTouchListener
{
    private int       _xDelta;
    private int       _yDelta;
    private int       _rightMargin;
    private int       _bottomMargin;
    private ImageView _floatingView;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this._floatingView = (ImageView) findViewById(R.id.textView);

        this._floatingView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
        {
            @Override
            public boolean onPreDraw()
            {
                if (_floatingView.getViewTreeObserver().isAlive())
                    _floatingView.getViewTreeObserver().removeOnPreDrawListener(this);

                updateLayoutParams(_floatingView);
                return false;
            }
        });

        this._floatingView.setOnTouchListener(this);
    }

    private void updateLayoutParams(View view)
    {
        this._rightMargin = -view.getMeasuredWidth();
        this._bottomMargin = -view.getMeasuredHeight();

        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(view.getMeasuredWidth(), view.getMeasuredHeight());
        layoutParams.bottomMargin = this._bottomMargin;
        layoutParams.rightMargin = this._rightMargin;

        view.setLayoutParams(layoutParams);
    }

    @Override
    public boolean onTouch(View view, MotionEvent event)
    {
        if (view == this._floatingView)
        {
            final int X = (int) event.getRawX();
            final int Y = (int) event.getRawY();

            switch (event.getAction() & MotionEvent.ACTION_MASK)
            {
                case MotionEvent.ACTION_DOWN:
                    RelativeLayout.LayoutParams lParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
                    this._xDelta = X - lParams.leftMargin;
                    this._yDelta = Y - lParams.topMargin;
                    break;

                case MotionEvent.ACTION_MOVE:
                    RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
                    layoutParams.leftMargin = X - this._xDelta;
                    layoutParams.topMargin = Y - this._yDelta;
                    layoutParams.rightMargin = this._rightMargin;
                    layoutParams.bottomMargin = this._bottomMargin;
                    view.setLayoutParams(layoutParams);
                    break;
            }

            return true;
        }
        else
        {
            return false;
        }
    }
}
Comparison answered 7/7, 2017 at 16:18 Comment(0)
L
1

The same as @Alex Karshin's answer, I change a bit.

public class MovingObject implements OnTouchListener {
private RelativeLayout.LayoutParams lParams;
private PointF viewPoint, prePoint, currPoint;

public MovingObject() {
    lParams = null;
    viewPoint = new PointF();
    prePoint = new PointF();
    currPoint = new PointF();
}

public boolean onTouch(View view, MotionEvent event) {
    switch (event.getAction() & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
        viewPoint.set(view.getX(), view.getY());
        prePoint.set(event.getRawX(), event.getRawY());
        lParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
        break;
    case MotionEvent.ACTION_UP:
        break;
    case MotionEvent.ACTION_POINTER_DOWN:
        break;
    case MotionEvent.ACTION_POINTER_UP:
        break;
    case MotionEvent.ACTION_MOVE:
        currPoint.set(event.getRawX(), event.getRawY());
        moveToCurrentPoint(view);
        break;
    }
    view.invalidate();
    return true;
}

private void moveToCurrentPoint(View view) {
    float dx = currPoint.x - prePoint.x - prePoint.x + viewPoint.x;
    float dy = currPoint.y - prePoint.y - prePoint.y + viewPoint.y;
    lParams.leftMargin = (int) (prePoint.x + dx);
    lParams.topMargin = (int) (prePoint.y + dy);
    view.setLayoutParams(lParams);
}
}
Loginov answered 28/6, 2020 at 5:2 Comment(0)
P
0

//if you want to move your camera or whatever then do it by following method..in my //case I am implementing on camera you can apply it on whateveer u want

public class VideoCallActivity extends AppCompatActivity implements 
  View.OnTouchListener {
 FrameLayout myLayout1;

@SuppressLint("ClickableViewAccessibility")
@Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  //in the frame layout I am setting my camera
   myLayout1.setOnTouchListener(this);
  
  }

   float dX, dY;

@Override
public boolean onTouch(View view, MotionEvent event) {
    switch (event.getAction()) {
 //this is your code
        case MotionEvent.ACTION_DOWN:
            dX = view.getX() - event.getRawX();
            dY = view.getY() - event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            view.animate()
                    .x(event.getRawX() + dX)
                    .y(event.getRawY() + dY)
                    .setDuration(0)
                    .start();
            break;
        default:
            return false;
    }
    return true;
}
Perquisite answered 9/6, 2021 at 8:51 Comment(0)
P
0

Simple code for kotlin:

var dx = 0f
var dy = 0f

private fun setMyViewListener(): OnTouchListener {
    return OnTouchListener { view, event ->

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                dx = view.x - event.rawX
                dx = view.y - event.rawY
            }
            MotionEvent.ACTION_MOVE -> view.animate()
                    .x(event.rawX + dx)
                    //.y(event.rawY + dy) // uncomment this line to move y
                    .setDuration(0)
                    .start()
        }

        true
    }
}

And then call it like this:

var myView = findViewById<ConstraintLayout>(R.id.myView)
myView.setOnTouchListener(setMyViewListener())
Paleobotany answered 31/3, 2022 at 9:9 Comment(1)
Nice try mate but your code is not that smooth and having some issues while moving objects/views. Have a look once again on it.Lazor

© 2022 - 2024 — McMap. All rights reserved.