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!