0

The sample application (shown below with my additions) is included in the Almeros library. It allows the user to perform multi-touch gestures to move, size, rotate, and shove an object on an unchanging background:

screen shots of sample application

I would like to implement a long click listener, but doing the obvious (adding implements View.OnLongClickListener and overriding public boolean onLongClick(View v) does not work (the method is never called).

What I have attempted to do is change the earth to a basketball through a long-click (both images are already included in the sample on github).

My question: How could long click events be used while preserving the existing gesture functionality provided by the library?

This is the sample code with a few of my few additions (which have been tagged with 20160326).

/**
 * Test activity for testing the different GestureDetectors.
 * 
 * @author Almer Thie (code.almeros.com)
 * Copyright (c) 2013, Almer Thie (code.almeros.com)
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 
 *  in the documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 
 * OF SUCH DAMAGE.
 */
// 20170326 - Commented 1, Added 1
//public class TouchActivity extends Activity implements OnTouchListener {
public class TouchActivity extends AppCompatActivity implements View.OnTouchListener, View.OnLongClickListener {
    private Matrix mMatrix = new Matrix();
    private float mScaleFactor = .4f;
    private float mRotationDegrees = 0.f;
    private float mFocusX = 0.f;
    private float mFocusY = 0.f;  
    private int mAlpha = 255;
    private int mImageHeight, mImageWidth;

    private ScaleGestureDetector mScaleDetector;
    private RotateGestureDetector mRotateDetector;
    private MoveGestureDetector mMoveDetector;
    private ShoveGestureDetector mShoveDetector; 

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

        // Determine the center of the screen to center 'earth'
        Display display = getWindowManager().getDefaultDisplay();
        mFocusX = display.getWidth()/2f;
        mFocusY = display.getHeight()/2f;

        // Set this class as touchListener to the ImageView
        ImageView view = (ImageView) findViewById(R.id.imageToMove);
        view.setOnTouchListener(this);
        //20160326 - Added 1
        view.setOnLongClickListener(this);

        // Determine dimensions of 'earth' image
        Drawable d      = this.getResources().getDrawable(R.drawable.earth);
        mImageHeight    = d.getIntrinsicHeight();
        mImageWidth     = d.getIntrinsicWidth();

        // View is scaled and translated by matrix, so scale and translate initially
        float scaledImageCenterX = (mImageWidth*mScaleFactor)/2; 
        float scaledImageCenterY = (mImageHeight*mScaleFactor)/2;

        mMatrix.postScale(mScaleFactor, mScaleFactor);
        mMatrix.postTranslate(mFocusX - scaledImageCenterX, mFocusY - scaledImageCenterY);
        view.setImageMatrix(mMatrix);

        // Setup Gesture Detectors
        mScaleDetector  = new ScaleGestureDetector(getApplicationContext(), new ScaleListener());
        mRotateDetector = new RotateGestureDetector(getApplicationContext(), new RotateListener());
        mMoveDetector   = new MoveGestureDetector(getApplicationContext(), new MoveListener());
        mShoveDetector  = new ShoveGestureDetector(getApplicationContext(), new ShoveListener());
    }

    @SuppressWarnings("deprecation")
    public boolean onTouch(View v, MotionEvent event) {
        mScaleDetector.onTouchEvent(event);
        mRotateDetector.onTouchEvent(event);
        mMoveDetector.onTouchEvent(event);
        mShoveDetector.onTouchEvent(event);

        float scaledImageCenterX = (mImageWidth*mScaleFactor)/2;
        float scaledImageCenterY = (mImageHeight*mScaleFactor)/2;

        mMatrix.reset();
        mMatrix.postScale(mScaleFactor, mScaleFactor);
        mMatrix.postRotate(mRotationDegrees,  scaledImageCenterX, scaledImageCenterY);
        mMatrix.postTranslate(mFocusX - scaledImageCenterX, mFocusY - scaledImageCenterY);

        ImageView view = (ImageView) v;
        view.setImageMatrix(mMatrix);
        view.setAlpha(mAlpha);

        return true; // indicate event was handled
    }

    // 20170326 - Added method
    @Override
    public boolean onLongClick(View v) {
        Log.v("20170326", "onLongClick called <<<<<<<<<<<<<<<<<<<<<<<<<<<");
        ImageView view = (ImageView) findViewById(R.id.imageToMove);
        view.setImageDrawable(this.getResources().getDrawable(R.drawable.basketball));
        return true;
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor(); // scale change since previous event

            // Don't let the object get too small or too large.
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f)); 

            return true;
        }
    }

    private class RotateListener extends RotateGestureDetector.SimpleOnRotateGestureListener {
        @Override
        public boolean onRotate(RotateGestureDetector detector) {
            mRotationDegrees -= detector.getRotationDegreesDelta();
            return true;
        }
    }   

    private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener {
        @Override
        public boolean onMove(MoveGestureDetector detector) {
            PointF d = detector.getFocusDelta();
            mFocusX += d.x;
            mFocusY += d.y;     

            // mFocusX = detector.getFocusX();
            // mFocusY = detector.getFocusY();
            return true;
        }
    }       

    private class ShoveListener extends ShoveGestureDetector.SimpleOnShoveGestureListener {
        @Override
        public boolean onShove(ShoveGestureDetector detector) {
            mAlpha += detector.getShovePixelsDelta();
            if (mAlpha > 255)
                mAlpha = 255;
            else if (mAlpha < 0)
                mAlpha = 0;

            return true;
        }
    }   

}
Dale
  • 5,520
  • 4
  • 43
  • 79

3 Answers3

1

As indicated by user mudit pant, having both Touch and Click listeners are a problem. So the approach should be to start again with the original sample code, and implement a LongPressListener private class in the same way that other listeners have been implemented:

// 20160327 - Added class
private class LongPressListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public void onLongPress(MotionEvent e) {
        super.onLongPress(e);
        ImageView mView = (ImageView) findViewById(R.id.imageView);
        mView.setImageDrawable(getApplicationContext().getResources().getDrawable(R.drawable.basketball));
    }
}

Then, in the same way the other detectors were integrated in the onTouch() method, add the new listener:

public boolean onTouch(View v, MotionEvent event) {
    mScaleDetector.onTouchEvent(event);
    mRotateDetector.onTouchEvent(event);
    mShoveDetector.onTouchEvent(event);
    mMoveDetector.onTouchEvent(event);

    // 20160327 - Added 1
    mLongPressDetector.onTouchEvent(event);
    .
    .
    .
}

And similarly, in onCreate() create another detector:

public void onCreate(Bundle savedInstanceState) {
    .
    .
    .

    // Setup Gesture Detectors
    .
    .
    .
    // 20170327 - Added 1
    mLongPressDetector = new GestureDetector(getApplicationContext(), new LongPressListener());
    .
    .
    .
}

Upon long press, the image changes to the basketball:

image changed to basket ball after long press

Dale
  • 5,520
  • 4
  • 43
  • 79
0

The problem is that you are having both Touch and clicklisteners. The touch listener is called as soon as you touch the device and since you are returning true from there, this event is not propogated to the click listener. Hence your code is not executing. The best way for you to handle this is starting a timer on touch down to mimick the long press behaviour. You should cancel this timer in touch up / move events. If your timer expires fire the long click event work. Hope this helps. :)

mudit pant
  • 104
  • 3
0

The answer here suffers from triggering unwanted long press events. In other words, if the user takes more than GestureDetector's LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(), the long press event is fired, even though the user might be just trying to move or rotate or size. Quick moves work fine, it's when they're trying to get precise, the long press event intercedes. My first thought was to increase the timeout, but it appears that it's system wide for all views, set at 500ms, and is not customizable.

Thus, an alternate solution which is very simple and does not need or use GestureDetector is to use Handler.postDelayed() along with a Runnable.

public boolean onTouch(View v, MotionEvent event) {
    mScaleDetector.onTouchEvent(event);
    mRotateDetector.onTouchEvent(event);
    mMoveDetector.onTouchEvent(event);
    mShoveDetector.onTouchEvent(event);

    // 20170329 - removed 1, added 'if' block
    //mLongPressDetector.onTouchEvent(event);
    if(event.getAction() == MotionEvent.ACTION_DOWN) {
        // Start a timer on each ACTION_DOWN
        handler.postDelayed(mLongPressed, 1000);
    }
    .
    .
    .
}

In each of the sample code onTouch() methods, the timer is removed. In all cases except onMove(), the callback is removed unconditionally. But in the case of the MoveListener, it gets triggered even when there is zero offset. So I'll paste only the altered MoveListener here, but you need to put handler.removeCallbacks(mLongPressed); in all the listeners:

private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener {
    @Override
    public boolean onMove(MoveGestureDetector detector) {
        PointF d = detector.getFocusDelta();
        mFocusX += d.x;
        mFocusY += d.y;

        // 20170329 - Added 'if' block - remove listener if there is non-zero offset
        if ((d.x + d.y) != 0f) {
            handler.removeCallbacks(mLongPressed);
            Log.v(TAG, "onMove removed " + d.x + ", " + d.y);
        }
        return true;
    }
}       

Finally, in your activity, you instantiate the Handler and implement the Runnable

// 20170329 - Added Handler and Runnable
final Handler handler = new Handler();
Runnable mLongPressed = new Runnable() {
    public void run() {
        Log.v(TAG, "mLongPressed Runnable triggered.");
        ImageView mView = (ImageView) findViewById(R.id.imageView);
        mView.setImageDrawable(getApplicationContext().getResources().getDrawable(R.drawable.basketball));
    }
};

This solution came from this MSquare answer.

Community
  • 1
  • 1
Dale
  • 5,520
  • 4
  • 43
  • 79