Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ScrollView and Gallery interfering

Tags:

android

I have a Gallery composed of many ScrollViews each of which occupies the whole screen. problem is the ScrollViews' onTouchEvent returns true and therefore prevent any other view in the DOM to handle the same event (which is swallowed after being processed at the ScrollView level). As a result the Gallery doesn't scroll anymore. On the other hand if I override onTouchEvent like this:

   @Override
   public boolean onTouchEvent(MotionEvent ev) {
      super.onTouchEvent(ev);
      return false; // <<<<<<<<<<<<<<<<<
   }   

then the Gallery receives its on event to process but the SrollView doesnt scroll anymore. Either way you lose! or do you?

problem sounds puzzling but I am sure if u stumbled upon it in the past u r gonna recognize it straight away as it a freaking damn one!

thanks

like image 701
nourdine Avatar asked Jul 03 '10 12:07

nourdine


4 Answers

Here's my attempt at gallery that works with vertical ScrollViews.

It uses its own instance of GestureDetector and feeds it with MotionEvents from onInterceptTouchEvent.

When gesture detector recognizes a scroll, we determine whether it's horizontal or vertical and lock on the direction until the gesture is finished. This avoids diagonal scrolling.

If it's a horizontal scroll, onInterceptTouchEvent will return true so that future motion events go to inherited Gallery.onTouchEvent to do the actual scrolling.

Gallery's own gesture detector (mGestureDetector in Gallery.java) doesn't get all motion events and thus sometimes reports huge sudden scrolls that cause gallery to jump around. I've put in a a nasty hack that discards those.

The code:

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.Gallery;

public class BetterGallery extends Gallery {
    /* This gets set when we detect horizontal scrolling */
    private boolean scrollingHorizontally = false;

    /* This gets set during vertical scrolling. We use this to avoid detecting
     * horizontal scrolling when vertical scrolling is already in progress
     * and vice versa. */
    private boolean scrollingVertically = false;

    /* Our own gesture detector, Gallery's mGestureDetector is private.
     * We'll feed it with motion events from `onInterceptTouchEvent` method. */
    private GestureDetector mBetterGestureDetector;

    public BetterGallery(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mBetterGestureDetector = new GestureDetector(new BetterGestureListener());
    }

    public BetterGallery(Context context, AttributeSet attrs) {
        super(context, attrs);
        mBetterGestureDetector = new GestureDetector(new BetterGestureListener());
    }

    public BetterGallery(Context context) {
        super(context);
        mBetterGestureDetector = new GestureDetector(new BetterGestureListener());
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
            float velocityY) {
        // Limit velocity so we don't fly over views
        return super.onFling(e1, e2, 0, velocityY);
    } 

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // Documentation on this method's contract:
        // http://developer.android.com/reference/android/view/ViewGroup.html#onInterceptTouchEvent(android.view.MotionEvent)

        // Reset our scrolling flags if ACTION_UP or ACTION_CANCEL
        switch (ev.getAction()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            scrollingHorizontally = false;
            scrollingVertically = false;
        }       

        // Feed our gesture detector
        mBetterGestureDetector.onTouchEvent(ev);

        // Intercept motion events if horizontal scrolling is detected
        return scrollingHorizontally;
    }


    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        // Hack: eat jerky scrolls caused by stale state in mGestureDetector
        // which we cannot directly access
        if (Math.abs(distanceX) > 100) return false;

        return super.onScroll(e1, e2, distanceX, distanceY);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Reset our scrolling flags if ACTION_UP or ACTION_CANCEL
        switch(event.getAction()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            scrollingHorizontally = false;
            scrollingVertically = false;
        }

        super.onTouchEvent(event);
        return scrollingHorizontally;
    }

    private class BetterGestureListener implements GestureDetector.OnGestureListener {

        @Override
        public boolean onDown(MotionEvent arg0) {
            return false;
        }

        @Override
        public boolean onFling(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
            return false;
        }

        @Override
        public void onLongPress(MotionEvent arg0) {
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (scrollingHorizontally || scrollingVertically) {
                // We already know we're scrolling, ignore this callback.
                // This avoids changing scrollingHorizontally / scrollingVertically
                // flags mid-scroll.
                return false;
            }

            scrollingHorizontally |= Math.abs(distanceX) > Math.abs(distanceY);
            // It's a scroll, and if it's not horizontal, then it has to be vertical
            scrollingVertically = !scrollingHorizontally;

            return false;
        }

        @Override
        public void onShowPress(MotionEvent arg0) {

        }

        @Override
        public boolean onSingleTapUp(MotionEvent arg0) {
            return false;
        }
    }
}

Warning: word "Better" in class name is likely misleading!

Update:

Forgot to mention, I've also set the activity to forward its onTouchEvent to gallery:

@Override
public boolean onTouchEvent(MotionEvent event) {
    return mGallery.onTouchEvent(event);
}

Update 2:

I've made some improvements to this code and put it up on bitbucket. There's also an example app. It demonstrates that this widget has issues with ListView as child :-/

enter image description here

Update 3:

Switched from Gallery to HorizontalScrollView as the base class for my custom widget. More on this here. Flings work, ListViews and ExpandableListViews as children work, tested on Android 1.6, 2.2, 2.3.4. Its behaviour is now quite close to that of Google apps, IMO.

Update 4:

Google has published ViewPager!

like image 65
Pēteris Caune Avatar answered Oct 18 '22 02:10

Pēteris Caune


This has given me headaches and I thought I'd post a solution based on chrisschell's answer because it helped me a lot.

Here's the new and improved gallery.

public class FriendlyGallery extends Gallery {
    FriendlyScrollView currScrollView;

    public FriendlyGallery(Context context) {
        super(context);
    }
    public FriendlyGallery(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(ev);  
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        currScrollView = getCurrScrollView();
        return super.onInterceptTouchEvent(ev);     
    }
    @Override
    public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if(currScrollView != null)
            currScrollView.scrollBy(0, (int) distanceY);
        return super.onScroll(e1, e2, distanceX, distanceY);
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if(currScrollView != null)
            currScrollView.fling(-(int) distanceY);
        return super.onFling(e1, e2, distanceX, distanceY);     
    }

    private FriendlyScrollView getCurrScrollView() {
        //I have a load more button that shouldn't be cast to a scrollview
        int pos = getFirstVisiblePosition();
        if(pos != getAdapter().getCount()-1)
            return (FriendlyScrollView)this.getSelectedView();
        else
            return null;
    }
}

And the scrollview.

public class FriendlyScrollView extends ScrollView {

    public FriendlyScrollView(Context context) {
        super(context);
    }
    public FriendlyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;       
    }
}

Hope this helps and thanks again to chrisschell for pointing me in the right direction.

like image 35
Chris Allen Avatar answered Oct 18 '22 02:10

Chris Allen


I wrote this custom class to be able to deal with that problem, and so far it seems to be a decent solution. It's not perfect and I haven't stuck a fork in it to call it done, but hopefully this may be of some use to someone :-)

import android.content.Context;
import android.view.MotionEvent;
import android.widget.Gallery;
import android.widget.ScrollView;

public class GalleryFriendlyScrollView extends ScrollView{

    private static int NONE = -1;
    private static int DOSCROLLVIEW = 0;
    private static int DOGALLERY = 1;

    private float lastx = 0;
    private float lasty = 0;
    private float firstx = 0;
    private float firsty = 0;
    private float lastRawx = 0;
    private float lastRawy = 0;
    private int gestureRecipient = NONE;
    private boolean donewithclick = true;
    private Gallery parent = null;
    private boolean firstclick = true;

    public GalleryFriendlyScrollView(Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean retthis = true;
        //Indicating a fresh click
        if(donewithclick){
            firstx = ev.getX();
            firsty = ev.getY();
            lastx = firstx;
            lasty = firsty;
            lastRawx = ev.getRawX();
            lastRawy = ev.getRawY();
            donewithclick = false;
            firstclick = true;
        }
        //We don't know where these gesture events are supposed to go to. 
        //We have movement on the x and/or why axes, so we can determine where they should go now.
        if((gestureRecipient == NONE) && (lastx != ev.getX() || lasty != ev.getY())){
            //Determine whether there's more movement vertically or horizontally
            float xdiff = ev.getX() - lastx;
            float ydiff = ev.getY() - lasty;
            if(xdiff < 0)xdiff = xdiff * -1;
            if(ydiff < 0)ydiff = ydiff * -1;
            if((xdiff) > (ydiff)){
                gestureRecipient = DOGALLERY;
            } else {
                gestureRecipient = DOSCROLLVIEW;
            }
        }
        if(gestureRecipient == DOGALLERY){
            if(!firstclick){
                //After you drag the screen left or right a bit, the baseline for where it calculates
                //x and y from changes, so we need to adjust the offset to our original baseline
                float offsetx = (((ev.getX() - lastx) - (ev.getRawX() - lastRawx)) * -1);
                float offsety = (((ev.getY() - lasty) - (ev.getRawY() - lastRawy)) * -1);
                ev.offsetLocation(offsetx, offsety);
            }
            retthis = getGallery().onTouchEvent(ev);
            firstclick = false;
        } else if(gestureRecipient == DOSCROLLVIEW){
            retthis = super.onTouchEvent(ev);
        }
        if(ev.getAction() == MotionEvent.ACTION_UP){
            //User's finger has been lifted
            if(((firstx == ev.getX()) && (firsty == ev.getY()))){
                //Since there isn't any movement in either direction, it's a click
                getGallery().onSingleTapUp(ev);
                super.onTouchEvent(ev);
            }
            donewithclick = true;
            gestureRecipient = NONE;
        }
        //And record our coordinate data
        lastx = ev.getX();
        lasty = ev.getY();
        lastRawx = ev.getRawX();
        lastRawy = ev.getRawY();
        return retthis;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        getGallery().onTouchEvent(ev);
        return super.onInterceptTouchEvent(ev);
    }

    private Gallery getGallery(){
        //Gets the gallery, in this case assuming the ScrollView is the direct child in the gallery.
        //Adjust as needed
        if(parent == null){
            parent = (Gallery) this.getParent();
        }
        return parent;
    }
}

I'd love to hear of people's experiences with this, and any suggestions you have.

like image 39
atraudes Avatar answered Oct 18 '22 02:10

atraudes


I found the Gallery scrolling-behaviour of atraudes' solution less than optimal so I looked for an other solution. Finally I found one which works perfectly for me:

  1. Create a class FriendlyGallery by extending Gallery and overwrite onTouchEvent, onInterceptTouchEvent, onScroll and onFling. Use the onInterceptTouchEvent to "register" the the currently displayed ScrollView with the FriendlyGallery and the "remote control" this ScrollView (using it scrollBy and fling Methods) within onScroll and onFling of FriendlyGallery (velocityY of onFling = -1*velocityY of fling !!!). Return true for onTouchEvent to capture the Touch Events here!
  2. Create a class FriendlyScrollView by extending ScrollView and overwrite onTouchEvent, onInterceptTouchEvent just returning "false" in order to keep the Touch Events from interfering with your "remote controlling" the ScrollView.
like image 1
chrisschell Avatar answered Oct 18 '22 04:10

chrisschell