14

I'm trying to create an app that can move an ImageView on your device like dragging and when I put like 75% of the ImageView out of the screen show a Toast for example. I've been reading about MotionEvent and onTouchListener and I've followed this question, but it doesn't convince me.

Edit

My current code is :

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {

    int windowwidth;
    int windowheight;
    private ImageView mImageView;
    private ViewGroup mRrootLayout;
    private int _xDelta;
    private int _yDelta;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DisplayMetrics displaymetrics = new DisplayMetrics();
        this.getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
        windowwidth = displaymetrics.widthPixels;
        windowheight = displaymetrics.heightPixels;
        mRrootLayout = (ViewGroup) findViewById(R.id.root);
        mImageView = (ImageView) mRrootLayout.findViewById(R.id.im_move_zoom_rotate);

        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(150, 150);
        mImageView.setLayoutParams(layoutParams);
        mImageView.setOnTouchListener(this);
    }
    public boolean onTouch(View view, MotionEvent event) {
        final int X = (int) event.getRawX();
        final int Y = (int) event.getRawY();
        if(X == 0){
            Toast.makeText(this, "OUT", Toast.LENGTH_SHORT).show();
        }
        else if (Y == 0){
            Toast.makeText(this, "OUT", Toast.LENGTH_SHORT).show();
        }
        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;
        }
        mRrootLayout.invalidate();
        return true;
    }
}

I did those if and else if just to know if the ImageView is getting out of the device, on the left and right side of the device seems like it's okay, but I'd like to make it cleaner and not hardwritted, also I don't get the LayoutParams(150,150) why 150? Also I don't get why I have to create a RelativeLayout.LayoutParams and why I have to put

layoutParams.rightMargin = -250;
layoutParams.bottomMargin = -250;

I did the if/else if because I want to delete when the user want to put the ImageView out of the device, so I need to control when he tries to, at the moment I only got it TOP/LEFT/RIGHT not down, I also get the dimensions of my device just to try if X or Y is the same as height or widht just show the Toast but it's not doing it correctly.

Now my ImageView is the ic_launcher but it will be bigger (almost middle screen).

NOTE

If you know any other way to do that easier or cleanest, feel free to put it here, I don't care about my code, I can adapt it, I just want it to be clear and not hardcoded.

Skizo-ozᴉʞS ツ
  • 19,464
  • 18
  • 81
  • 148
  • I came across a similar problem and then got a solution. [Checking Drag Out of Bounds](https://stackoverflow.com/questions/22015840/dragging-button-is-going-out-of-the-screen) – Abdul Ahad Oct 20 '17 at 07:18
  • If you know how to solve it (IF TESTED) feel free to put an answer and I'll test it out :) – Skizo-ozᴉʞS ツ Oct 20 '17 at 08:53

6 Answers6

10

Your routine works for the most part. In the following code, I have commented out sections that are not needed and made notations for those parts that need some explanation. Here is what the finished product looks like:

enter image description here

This graphic explains how the left margin is calculated. The same type of calculation applies to the top margin.

enter image description here

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {

    int windowwidth; // Actually the width of the RelativeLayout.
    int windowheight; // Actually the height of the RelativeLayout.
    private ImageView mImageView;
    private ViewGroup mRrootLayout;
    private int _xDelta;
    private int _yDelta;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // We are interested when the image view leaves its parent RelativeLayout
        // container and not the screen, so the following code is not needed.
//        DisplayMetrics displaymetrics = new DisplayMetrics();
//        this.getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
//        windowwidth = displaymetrics.widthPixels;
//        windowheight = displaymetrics.heightPixels;
        mRrootLayout = (ViewGroup) findViewById(R.id.root);
        mImageView = (ImageView) mRrootLayout.findViewById(R.id.im_move_zoom_rotate);

        // These these following 2 lines that address layoutparams set the width
        // and height of the ImageView to 150 pixels and, as a side effect, clear any
        // params that will interfere with movement of the ImageView.
        // We will rely on the XML to define the size and avoid anything that will
        // interfere, so we will comment these lines out. (You can test out how a layout parameter
        // can interfere by setting android:layout_centerInParent="true" in the ImageView.
//        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(150, 150);
//        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(150, 150);
//        mImageView.setLayoutParams(layoutParams);
        mImageView.setOnTouchListener(this);

        // Capture the width of the RelativeLayout once it is laid out.
        mRrootLayout.post(new Runnable() {
            @Override
            public void run() {
                windowwidth = mRrootLayout.getWidth();
                windowheight = mRrootLayout.getHeight();
            }
        });
    }

    // Tracks when we have reported that the image view is out of bounds so we
    // don't over report.
    private boolean isOutReported = false;

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

        // Check if the image view is out of the parent view and report it if it is.
        // Only report once the image goes out and don't stack toasts.
        if (isOut(view)) {
            if (!isOutReported) {
                isOutReported = true;
                Toast.makeText(this, "OUT", Toast.LENGTH_SHORT).show();
            }
        } else {
            isOutReported = false;
        }
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                // _xDelta and _yDelta record how far inside the view we have touched. These
                // values are used to compute new margins when the view is moved.
                _xDelta = X - view.getLeft();
                _yDelta = Y - view.getTop();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
                // Do nothing
                break;
            case MotionEvent.ACTION_MOVE:
                RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view
                    .getLayoutParams();
                // Image is centered to start, but we need to unhitch it to move it around.
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    lp.removeRule(RelativeLayout.CENTER_HORIZONTAL);
                    lp.removeRule(RelativeLayout.CENTER_VERTICAL);
                } else {
                    lp.addRule(RelativeLayout.CENTER_HORIZONTAL, 0);
                    lp.addRule(RelativeLayout.CENTER_VERTICAL, 0);
                }
                lp.leftMargin = X - _xDelta;
                lp.topMargin = Y - _yDelta;
                // Negative margins here ensure that we can move off the screen to the right
                // and on the bottom. Comment these lines out and you will see that
                // the image will be hemmed in on the right and bottom and will actually shrink.
                lp.rightMargin = view.getWidth() - lp.leftMargin - windowwidth;
                lp.bottomMargin = view.getHeight() - lp.topMargin - windowheight;
                view.setLayoutParams(lp);
                break;
        }
        // invalidate is redundant if layout params are set or not needed if they are not set.
//        mRrootLayout.invalidate();
        return true;
    }

    private boolean isOut(View view) {
        // Check to see if the view is out of bounds by calculating how many pixels
        // of the view must be out of bounds to and checking that at least that many
        // pixels are out.
        float percentageOut = 0.50f;
        int viewPctWidth = (int) (view.getWidth() * percentageOut);
        int viewPctHeight = (int) (view.getHeight() * percentageOut);

        return ((-view.getLeft() >= viewPctWidth) ||
            (view.getRight() - windowwidth) > viewPctWidth ||
            (-view.getTop() >= viewPctHeight) ||
            (view.getBottom() - windowheight) > viewPctHeight);
    }
}

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/im_move_zoom_rotate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:src="@drawable/circle" />

</RelativeLayout>
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • nice answers and well explained, I did it with my ic_launcher and it's not working at 100% it says OUT when is going out by TOP/BOTTOM but left/right it's not working 100%, it's necessary to put layout_widht 150dp? couldn't be wrap_content? my image will be almost the same size of the screen, but now I'm testing it with ic_launcher.... – Skizo-ozᴉʞS ツ Oct 25 '17 at 13:09
  • @Skizo-ozᴉʞS That is curious. It works OK for me, but our environments must differ. Can you post the relevant XML layout that you are using and a description of your ic_launcher? Is it just a standard ic_launcher icon? I am wondering if your image isn't larger than it appears. btw, I updated how the negative margins are set in the code. See updated answer. – Cheticamp Oct 25 '17 at 13:37
  • Yes, see my [`XML LAYOUT`](https://paste.ofcode.org/8Pbh3kARdeFTFiE55EQAep), now it doesn't move because I put it on the middle of the screen... (I want it on the middle of the screen though), and I put 250sp but I'd like to put it bigger.... – Skizo-ozᴉʞS ツ Oct 25 '17 at 13:42
  • Look @Cheticamp, I'm using this [image](http://www.kidsmathgamesonline.com/images/pictures/shapes/circle.jpg) now, as I'm seeing 75% is too much, could it be less? like 50% or a little bit less? I've edited the [`xml layout`](https://paste.ofcode.org/7kMaYPB8943TdjFs99Bhk4) – Skizo-ozᴉʞS ツ Oct 25 '17 at 13:49
  • @Skizo-ozᴉʞS Code changed to allow for initial placement in center of screen. See the `removeRule()` statements in `onTouch()`. Added parameter to vary the percentage of the image that must be "out" before toast is shown. See `isOut()`. – Cheticamp Oct 25 '17 at 14:06
  • I know `removeRule()` but I'm using min15 not min17, but well.. I guess putting this : `android:layout_width="wrap_content" android:layout_height="wrap_content"` will work, so thanks... seems like it's working perfect, I'll depure it asap :D Thanks bro – Skizo-ozᴉʞS ツ Oct 25 '17 at 14:11
  • @Skizo-ozᴉʞS I changed the code to accommodate – Cheticamp Oct 25 '17 at 17:06
  • Means that API 15 will center it as well? What do you think it's better to put it on wrap_content or d the comprobation that you did? – Skizo-ozᴉʞS ツ Oct 25 '17 at 18:05
  • @Skizo-ozᴉʞS The code will work with API 15 as well as API 17+. I updated the XML to reflect `wrap_content` like your updated XML and the centering of the image. – Cheticamp Oct 25 '17 at 19:12
  • One question why I can not set the image to the center? If I put the image to the center of the screen it won't drag... – Skizo-ozᴉʞS ツ Nov 08 '17 at 12:19
  • @Skizo-ozᴉʞS How are you centering the image? Is it with `layout_centerHorizontal` and `layout_centerVertical` or another way such as `layout_centerInParent`? If "center in parent", you will need to add the code `lp.removeRule(RelativeLayout.CENTER_IN_PARENT);`. – Cheticamp Nov 08 '17 at 12:30
  • Ah I didn't remember to uncomment the if stuff (build sdk) now is working, thanks, but it means that if one versions of android it won't be centered? – Skizo-ozᴉʞS ツ Nov 08 '17 at 12:56
  • @Skizo-ozᴉʞS It will be centered for all versions of Android if you use the code in the answer. – Cheticamp Nov 08 '17 at 13:14
  • wouldn't it be easier to listen touch events on the view instead of the activity? – stdout Mar 21 '18 at 09:59
3

The framework has a class called View.OnDragListener. See the Drag & Drop tutorial.

See also additionally the DraggablePanel project if you want to study how this can be done.

Yash Sampat
  • 30,051
  • 12
  • 94
  • 120
3

I'm using this method to drag an ImageView , I hope that this can help you : So I defined those attributes of the class :

 private float xCoOrdinate, yCoOrdinate;
 private double screenCenterX, screenCenterY;

Then I implement this code under the OnCreate() method of the activity :

 mRrootLayout.getBackground().setAlpha(255);

    /**
     * Calculate max hypo value and center of screen.
     */
    final DisplayMetrics display = getResources().getDisplayMetrics();
    screenCenterX = display.widthPixels / 2;
    screenCenterY = (display.heightPixels - getStatusBarHeight()) / 2;
    final double maxHypo = Math.hypot(screenCenterX, screenCenterY);

    mImageView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            /**
             * Calculate hypo value of current imageview position according to center
             */
            double centerYPos = mImageView.getY() + (mImageView.getHeight() / 2);
            double centerXPos = mImageView.getX() + (mImageView.getWidth() / 2);
            double a = screenCenterX - centerXPos;
            double b = screenCenterY - centerYPos;
            double hypo = Math.hypot(a, b);

            /**
             * change alpha of background of layout
             */
            alpha = (int) (hypo * 255) / (int) maxHypo;
            if (alpha < 255)
                mRrootLayout.getBackground().setAlpha(255 - alpha);

            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    xCoOrdinate = mImageView.getX() - event.getRawX();
                    yCoOrdinate = mImageView.getY() - event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    mImageView.animate().x(event.getRawX() + xCoOrdinate).y(event.getRawY() + yCoOrdinate).setDuration(0).start();
                    break;
                case MotionEvent.ACTION_UP:
                     if (alpha > 50) {
                        Toast.makeText(ImageViewerActivity.this, "Out", Toast.LENGTH_SHORT).show();
                        return false;
                    } else {
                        Toast.makeText(ImageViewerActivity.this, "In", Toast.LENGTH_SHORT).show();
                        mImageView.animate().x(0).y((float) screenCenterY - mImageView.getHeight() / 2).setDuration(100).start();
                        mRrootLayout.getBackground().setAlpha(255);
                    }
                default:
                    return false;
            }
            return true;
        }
    });
Imene Noomene
  • 3,035
  • 5
  • 18
  • 34
  • Nmm could you put a simple toast instead of animate it? I mean if you are going to get out of the device a toast (when the image is arround 50% out of the screen)? – Skizo-ozᴉʞS ツ Oct 23 '17 at 10:46
  • I added for you the toasts sis . Try it and if you have question don't hesitate to ask me – Imene Noomene Oct 23 '17 at 17:01
  • PS: The Toasts will be shown when you invoke ACTION_UP and it will display "Out" if you are 50% out of the screen and "in" if you still in 50% of the screen – Imene Noomene Oct 23 '17 at 17:06
  • Well the 50% of the image was a random number, but yes, this night I'll make a test and I'll let you know, but it can not be caught on action_move? – Skizo-ozᴉʞS ツ Oct 23 '17 at 17:17
  • I've tested it and seems like it does not work, as a [`getStatusBarHeight()` method I got this from this question](https://stackoverflow.com/a/28965901/4385913), it's okay? It does say OUT if I'm on the middle of the screen – Skizo-ozᴉʞS ツ Oct 23 '17 at 19:04
3

A working example of how to move all the views contained in a RelativeLayout using onTouch. Hope it will help :

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
    private RelativeLayout mRelLay;
    private float mInitialX, mInitialY;
    private int mInitialLeft, mInitialTop;
    private View mMovingView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRelLay = (RelativeLayout) findViewById(R.id.relativeLayout);

        for (int i = 0; i < mRelLay.getChildCount(); i++)
            mRelLay.getChildAt(i).setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        RelativeLayout.LayoutParams mLayoutParams;

        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mMovingView = view;
                mLayoutParams = (RelativeLayout.LayoutParams) mMovingView.getLayoutParams();
                mInitialX = motionEvent.getRawX();
                mInitialY = motionEvent.getRawY();
                mInitialLeft = mLayoutParams.leftMargin;
                mInitialTop = mLayoutParams.topMargin;
                break;

            case MotionEvent.ACTION_MOVE:
                if (mMovingView != null) {
                    mLayoutParams = (RelativeLayout.LayoutParams) mMovingView.getLayoutParams();
                    mLayoutParams.leftMargin = (int) (mInitialLeft + motionEvent.getRawX() - mInitialX);
                    mLayoutParams.topMargin = (int) (mInitialTop + motionEvent.getRawY() - mInitialY);
                    mMovingView.setLayoutParams(mLayoutParams);
                }
                break;

            case MotionEvent.ACTION_UP:
                mMovingView = null;
                break;
        }

        return true;
    }
}
from56
  • 3,976
  • 2
  • 13
  • 23
  • seems like it's working but looks like on the left it's not detecting it, right? I'd like to get a toast or something when user tries to move the image out of the device TOP/DOWN/RIGHT/LEFT you got me? – Skizo-ozᴉʞS ツ Oct 23 '17 at 10:41
  • Then you should limit movement adding in ACTION_MOVE something like if(topMargin < 0) topMargin = 0; and if(topMargin + height > windowheight) topMargin = windowheight - height; and the same for left margin and width. – from56 Oct 24 '17 at 10:36
  • Could you make a sample of that so I can test and I can approve it? – Skizo-ozᴉʞS ツ Oct 24 '17 at 10:39
3

Update

Add right/bottom margin in step 3 to prevent image zoomed. You can see if you not change right/bottom margin, image will zoomed by relative layout. preview solution old and new

  1. getMeasuredHeight/Width avoid MATCH_PARENT and WRAP_CONTENT.
  2. If there is a toolbar/actionbar then topMargin + height > relativeLayout's height also applies to out of bottom determination.
  3. record state of out of bound avoid toast continuous appear.

    public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
    
        Point lastPoint = new Point();
        RelativeLayout relativeLayout;
    
        boolean lastOutOfTop = false;
        boolean lastOutOfLeft = false;
        boolean lastOutOfRight = false;
        boolean lastOutOfBottom = false;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            (findViewById(R.id.imageView)).setOnTouchListener(this);
            relativeLayout = (RelativeLayout)findViewById(R.id.relativeLayout);
        }
    
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            //1. user's finger
            final Point point = new Point((int) event.getRawX(), (int) event.getRawY());
    
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    // 2. record the last touch point
                    lastPoint = point;
                    break;
                case MotionEvent.ACTION_UP:
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    break;
                case MotionEvent.ACTION_MOVE:
                    // 3. get the move offset
                    final Point offset = new Point(point.x-lastPoint.x, point.y-lastPoint.y);
                    RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view
                            .getLayoutParams();
                    layoutParams.leftMargin += offset.x;
                    layoutParams.topMargin += offset.y;
                    // * also check right/bottom Margin
                    layoutParams.rightMargin = relativeLayout.getMeasuredWidth() - layoutParams.leftMargin+view.getMeasuredWidth();
                    layoutParams.bottomMargin = relativeLayout.getMeasuredHeight() - layoutParams.topMargin+view.getMeasuredHeight();
                    view.setLayoutParams(layoutParams);
                    // 4. record the last touch point
                    lastPoint = point;
                    break;
            }
    
            // 5. check bounds
            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
            boolean outOfTop = layoutParams.topMargin < 0;
            boolean outOfLeft = layoutParams.leftMargin < 0;
            boolean outOfBottom = layoutParams.topMargin+view.getMeasuredHeight() > relativeLayout.getMeasuredHeight();
            boolean outOfRight = layoutParams.leftMargin+view.getMeasuredWidth() > relativeLayout.getMeasuredWidth();
    
            // 6. if out of bound
            if (outOfLeft&&!lastOutOfLeft) Toast.makeText(this, "OUT Left", Toast.LENGTH_SHORT).show();
            if (outOfTop&&!lastOutOfTop) Toast.makeText(this, "OUT Top", Toast.LENGTH_SHORT).show();
            if (outOfBottom&&lastOutOfBottom) Toast.makeText(this, "OUT Bottom", Toast.LENGTH_SHORT).show();
            if (outOfRight&&lastOutOfRight)  Toast.makeText(this, "OUT Right", Toast.LENGTH_SHORT).show();
    
            // 7. record
            lastOutOfTop = outOfTop;
            lastOutOfLeft = outOfLeft;
            lastOutOfBottom = outOfBottom;
            lastOutOfRight = outOfRight;
            return true;
        }
    }
    
Codus
  • 1,433
  • 1
  • 14
  • 18
1

You can achieve this via this code.

DisplayMetrics metrics = getResources().getDisplayMetrics();
int windowWidth = metrics.widthPixels;
int windowHeight = metrics.heightPixels;

Now in your onTouch method, calculate if the target location exceeds the above dimensions.

if( currentXLocation + deltaX > windowWidth ){

// this will ensure that target location 
// is always <= windowHeight
deltaX = windowWidth - currentXLocation; 

} else if( currentXLocation + deltaX < 0){

deltaX = -(currentXLocation);

} else if (...){

// perform similar calculations for the rest 

}
Abdul Ahad
  • 435
  • 4
  • 14