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
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 :-/
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!
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.
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.
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With