3

I have a custom SwipeRefreshLayout that has a custom GridView inside it. I want to change the column number of that GridView based on pinch/zoom gesture. I have successfully implemented it. The problem is, the scale is too sensitive.

For example, i have column number 3-5. It is easy to scale to 3 or 5, but to make it 4 is hard, since the scale itself is too sensitive.

Here is my custom SwipeRefreshLayout class

/**
 * This class contains fix from http://stackoverflow.com/questions/23989910/horizontalscrollview-inside-swiperefreshlayout
 */
public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {

    private int mTouchSlop;
    private float mPrevX;
    private ScaleGestureDetector mScaleGestureDetector;
    private ScaleListener mScaleListener;

    public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public void setScaleListener(Context context, ScaleListener scaleListener) {
        this.mScaleListener = scaleListener;
        mScaleGestureDetector = new ScaleGestureDetector(context, new MyOnScaleGestureListener());
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (mScaleGestureDetector != null) {
            mScaleGestureDetector.onTouchEvent(event);
        }

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_POINTER_DOWN:
                setEnabled(false);
                if (mScaleListener != null) {
                    mScaleListener.onTwoFingerStart();
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                setEnabled(true);
                mPrevX = MotionEvent.obtain(event).getX();
                if (mScaleListener != null) {
                    mScaleListener.onTwoFingerEnd();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                mPrevX = MotionEvent.obtain(event).getX();
                break;

            case MotionEvent.ACTION_MOVE:
                final float eventX = event.getX();
                float xDiff = Math.abs(eventX - mPrevX);

                if (xDiff > mTouchSlop) {
                    return false;
                }
        }

        return super.onInterceptTouchEvent(event);
    }

    class MyOnScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        private static final String TAG = "MyOnScaleGestureListene";

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            if (mScaleListener != null) {
                // Too sensitive, must change to other approach
                float scaleFactor = detector.getScaleFactor();
                Log.d(TAG, "onScale: " + scaleFactor);
                if (scaleFactor > 1F) {
                    mScaleListener.onScaleUp(scaleFactor);
                } else if (scaleFactor < 1F) {
                    mScaleListener.onScaleDown(scaleFactor);
                } else {
                    // no scale
                }
            }
            return true;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
        }
    }

    public interface ScaleListener {
        void onTwoFingersStart();

        void onTwoFingersEnd();

        void onScaleUp(float scaleFactor);

        void onScaleDown(float scaleFactor);
    }
}

Is there any way to reduce the sensitivity of ScaleGestureDetector.SimpleOnScaleGestureListener? If there is no way, is there any alternatives to solve it?

Here is a short video that showed the problem https://www.youtube.com/watch?v=0MItDNZ_o4c

HendraWD
  • 2,984
  • 2
  • 31
  • 45

2 Answers2

5

You should have a tolerance distance, which is called "slop" in Android world. If the gesture is less than that tolerance you should just disregard it.

private static final int SPAN_SLOP = 7;

...

@Override
public boolean onScale(@NonNull ScaleGestureDetector detector) {
    if (gestureTolerance(detector)) {
        // performing scaling
    }
    return true;
}

private boolean gestureTolerance(@NonNull ScaleGestureDetector detector) {
    final float spanDelta = Math.abs(detector.getCurrentSpan() - detector.getPreviousSpan());
    return spanDelta > SPAN_SLOP;
}

As an example you can see showcase open-source app made by @rallat.

enter image description here

Here you can find source code and presentation at GOTO Copenhagen 2016.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
  • Why 7 for SPAN_SLOP? Is it in pixel? If so do you think it is better if we convert it to dp instead? – HendraWD Mar 31 '17 at 05:01
  • I have tried it, and it is not match with my requirement, because `spanDelta` always resets if my fingers too slow. And if my fingers too fast, it still hard to get 4 column numbers. – HendraWD Mar 31 '17 at 05:49
  • As you can see from [docs](https://developer.android.com/reference/android/view/ScaleGestureDetector.html#getCurrentSpan()) it's a value in pixels. Basically, you should test and find appropriate tolerance value for your use case. – azizbekian Mar 31 '17 at 05:50
  • 1
    But from the code you provided, i got an idea to save initial distance using `detector.getCurrentSpan()`, and compare it directly to `detector.getCurrentSpan()` every time onScale called. – HendraWD Mar 31 '17 at 05:57
1

I am able to solve the problem to meet my requirement. @azizbekian's answer was not really meet my requirement because spanDelta always resets if my fingers too slow and if my fingers too fast, it can scale, but still hard to get 4 columns. But from his code example, i am able to create a workaround.

I just need to save initial scale distance(span) using detector.getCurrentSpan()

@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
    mInitialDistance = detector.getCurrentSpan();
    return true;
}

And then compare it directly with detector.getCurrentSpan() every time onScale() called. Then reset initial distance with mInitialDistance = detector.getCurrentSpan(); if scale occurred.

@Override
public boolean onScale(ScaleGestureDetector detector) {
    if (gestureTolerance(detector)) {
        if (mScaleListener != null) {
            float scaleFactor = detector.getScaleFactor();
            if (scaleFactor > 1F) {
                mScaleListener.onScaleUp(scaleFactor);
                mInitialDistance = detector.getCurrentSpan();
            } else if (scaleFactor < 1F) {
                mScaleListener.onScaleDown(scaleFactor);
                mInitialDistance = detector.getCurrentSpan();
            }
        }
    }
    return true;
}

private boolean gestureTolerance(@NonNull ScaleGestureDetector detector) {
    final float currentDistance = detector.getCurrentSpan();
    final float distanceDelta = Math.abs(mInitialDistance - currentDistance);
    return distanceDelta > mScaleTriggerDistance;
}

Here is the complete code of my custom SwipeRefreshLayout

public class MyCustomSwipeRefreshLayout extends SwipeRefreshLayout {

    private static final String TAG = "OneTouchRefreshFreeSwip";
    private static final float DEFAULT_SCALE_TRIGGER_DISTANCE = 48;// in dp
    private int mTouchSlop;
    private float mPrevX;

    private ScaleGestureDetector mScaleGestureDetector;
    private ScaleListener mScaleListener;

    private float mScaleTriggerDistance;
    private float mInitialDistance;

    public MyCustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public void setScaleListener(Context context, ScaleListener scaleListener) {
        this.mScaleListener = scaleListener;
        mScaleGestureDetector = new ScaleGestureDetector(context, new MyOnScaleGestureListener());
        mScaleTriggerDistance = Util.dp2px(DEFAULT_SCALE_TRIGGER_DISTANCE, context);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (mScaleGestureDetector != null) {
            mScaleGestureDetector.onTouchEvent(event);
        }

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_POINTER_DOWN:
                setEnabled(false);
                if (mScaleListener != null) {
                    mScaleListener.onTwoFingersStart();
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                if (mScaleListener != null) {
                    mScaleListener.onTwoFingersEnd();
                }
                mPrevX = MotionEvent.obtain(event).getX();
                setEnabled(true);
                return true;
            case MotionEvent.ACTION_DOWN:
                mPrevX = MotionEvent.obtain(event).getX();
                break;

            case MotionEvent.ACTION_MOVE:
                final float eventX = event.getX();
                float xDiff = Math.abs(eventX - mPrevX);

                if (xDiff > mTouchSlop) {
                    return false;
                }
        }

        return super.onInterceptTouchEvent(event);
    }

    private boolean gestureTolerance(@NonNull ScaleGestureDetector detector) {
        final float currentDistance = detector.getCurrentSpan();
        final float distanceDelta = Math.abs(mInitialDistance - currentDistance);
        return distanceDelta > mScaleTriggerDistance;
    }

    class MyOnScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            if (gestureTolerance(detector)) {
                if (mScaleListener != null) {
                    float scaleFactor = detector.getScaleFactor();
                    if (scaleFactor > 1F) {
                        mScaleListener.onScaleUp(scaleFactor);
                        mInitialDistance = detector.getCurrentSpan();
                    } else if (scaleFactor < 1F) {
                        mScaleListener.onScaleDown(scaleFactor);
                        mInitialDistance = detector.getCurrentSpan();
                    }
                }
            }
            return true;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            mInitialDistance = detector.getCurrentSpan();
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
        }
    }

    public interface ScaleListener {
        void onTwoFingersStart();

        void onTwoFingersEnd();

        void onScaleUp(float scaleFactor);

        void onScaleDown(float scaleFactor);
    }
}
HendraWD
  • 2,984
  • 2
  • 31
  • 45