19

Desired effect I have a bunch of small images that I'd like to show on a "wall" and then let the user fling this wall in any direction and select an image.

Initial Idea as a possible implementation I was thinking a GridView that is larger than the screen can show - but all examples of using this widget indicate that the Gallery doesn't extend beyond the size of the screen.

Question What is the best widget to use to implement the desired effect ? A code sample would be especially beneficial.

EDIT... if someone has example code that will let me put about 30 images on a "wall" (table would be good) then I will accept that as the answer. Note that the "wall" should look like it extends beyond the edges of the display and allow a user to use the finger to drag the "wall" up down left right. Dragging should be in "free-form" mode. A single tap on an image selects it and a callback shall be detectable. I have created a bounty for this solution.

Someone Somewhere
  • 23,475
  • 11
  • 118
  • 166

4 Answers4

11

The solution is actually quite simple, but a pain. I had to implement exactly this within a program I recently did. There are a few tricksy things you have to get around though. It must be noted that different OS versions have different experiences. Certain expertise or cludging around is required to make this work as drawing is sometimes adversely affected by this method. While I have a working copy, the code I provide is not for everyone, and is subject to whatever other changes or customizations have been made in your code.

  • set android:layout_width to wrap_content
  • set android:layout_height to wrap_content
  • In your code:
    • determine how many rows you will have.
    • divide number of items by the number of rows.
    • add gridView.setStretchMode(NO_STRETCH);
    • add gridView.setNumColumns( number of Columns );
    • add gridView.setColumnWidth( an explicit width in pixels );
  • To Scroll:
    • You may simply use gridView.scrollBy() in your onTouchEvent()

These are the steps. All are required in order to get it to work. The key is the NO_STRETCH with an explicit number of columns at a specific width. Further details can be provided, if you need clarification. You may even use a VelocityTracker or onFling to handle flinging. I have snappingToPage enabled in mine. Using this solution, there is not even a requirement to override onDraw() or onLayout(). Those three lines of code get rid of the need for a WebView or any other embedding or nesting.

Bi-directional scrolling is also quite easy to implement. Here is a simple code solution:

  1. First, in your class begin tracking X and Y positions by making two class members. Also add State tracking;

    // Holds the last position of the touch event (including movement)
    int myLastX;
    int myLastY;
    
    // Tracks the state of the Touch handling
    final static private int TOUCH_STATE_REST = 0;
    final static private int TOUCH_STATE_SCROLLING = 1;
    int myState = TOUCH_STATE_REST;
    
  2. Second, make sure to check for Scrolling, that way you can still click or longclick the images themselves.

    @Override public boolean onInterceptTouchEvent(final MotionEvent ev)
    {//User is already scrolling something. If we haven't interrupted this already,
    // then something else is handling its own scrolling and we should let this be.
    // Once we return TRUE, this event no longer fires and instead all goes to 
    // onTouch() until the next TouchEvent begins (often beginning with ACTION_DOWN).
        if ((ev.getAction() == MotionEvent.ACTION_MOVE)
        &&  (myState != TOUCH_STATE_REST))
            return false;
    
    // Grab the X and Y positions of the MotionEvent
        final float _x = ev.getX();
        final float _y = ev.getY();
        switch (ev.getAction())
        {   case MotionEvent.ACTION_MOVE:
                final int _diffX = (int) Math.abs(_x - myLastX);
                final int _diffY = (int) Math.abs(_y - myLastY);
    
                final boolean xMoved = _diffX > 0;
                final boolean yMoved = _diffY > 0;
                if (xMoved || yMoved)
                   myState = TOUCH_STATE_SCROLLING;
                break;
            case MotionEvent.ACTION_DOWN:
            // Remember location of down touch
                myLastX = _x;
                myLastY = _y;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (myState == TOUCH_STATE_SCROLLING)
                // Release the drag
                    myState = TOUCH_STATE_REST;
    
        }
        //If we are not At Rest, start handling in our own onTouch()
        return myState != TOUCH_STATE_REST;
    }
    
  3. After the GridView knows that you are Scrolling, it will send all Touch Events to onTouch. Do your Scrolling here.

    @Override public boolean onTouchEvent(final MotionEvent ev)
    {
        final int action = ev.getAction();
        final float x = ev.getX();
        final float y = ev.getY();
        final View child;
    
        switch (action) 
        {
            case MotionEvent.ACTION_DOWN:
                //Supplemental code, if needed
                break;
            case MotionEvent.ACTION_MOVE:
                // This handles Scrolling only.
                if (myState == TOUCH_STATE_SCROLLING)
                {
                    // Scroll to follow the motion event
                    // This will update the vars as long as your finger is down.
                    final int deltaX = (int) (myLastX - x);
                    final int deltaY = (int) (myLastY - y);
                    myLastX = x;
                    myLastY = y;
                    scrollBy(deltaX, deltaY);
                }
                break;
            case MotionEvent.ACTION_UP:
            // If Scrolling, stop the scroll so we can scroll later.
                if (myState == TOUCH_STATE_SCROLLING)
                    myState = TOUCH_STATE_REST;
                break
            case MotionEvent.ACTION_CANCEL:
            // This is just a failsafe. I don't even know how to cancel a touch event
                 myState = TOUCH_STATE_REST;
        }
    
    return true;
    }
    

If course, this solution moves at X and Y at the same time. If you want to move just one direction at a time, you can differentiate easily by checking the greater of the X and Y differences. (i.e. Math.abs(deltaX) > Math.abs(deltaY)) Below is a partial sample for one directional scrolling, but can switch between X or Y direction.

3b. Change this in your OnTouch() if you want to handle one direction at a time:

            case MotionEvent.ACTION_MOVE:
                if (myState == TOUCH_STATE_SCROLLING)
                {
                    //This will update the vars as long as your finger is down.
                    final int deltaX = (int) (myLastX - x);
                    final int deltaY = (int) (myLastY - y);
                    myLastX = x;
                    myLastY = y;

                    // Check which direction is the bigger of the two
                    if (Math.abs(deltaX) > Math.abs(deltaY))
                        scrollBy(deltaX, 0);
                    else if (Math.abs(deltaY) > Math.abs(deltaX))
                        scrollBy(0, deltaY);
                    // We do nothing if they are equal.
                }
                break;

FuzzicalLogic

Jamshid
  • 340
  • 3
  • 12
Fuzzical Logic
  • 12,947
  • 2
  • 30
  • 58
  • I would have provided a full sample, but those are the only 3 lines that are important and they may be placed anywhere. I have three such GridViews and each places them in a different place (one in the initial declaration, one in an CursorAdapter, and one in a separate method that is called from multiple places. – Fuzzical Logic Oct 11 '11 at 22:52
  • 1
    And... as a side note, this took me a week of poking and prodding to find. I was so happy when I did. – Fuzzical Logic Oct 11 '11 at 22:53
  • What does mScroller refer to in this context? If you elaborate a little bit I'll award the bounty to you. – Andreas Eriksson Oct 12 '11 at 06:29
  • Ooops. Sorry. Ignore mScroller. That's a by-product of the code I took it out of. mScroller was a `android.widget.Scroller` object. Totally unnecessary, but useful for certain things. For instance, you can check whether the scrolling is still happening (animating) and decide further whether to continue. The real advantage to a `Scroller` (IMHO) is that you can post (delay) your Scroll actions, if you want. I've edited the code such that it is removed. I use it for some things, and others I find it completely unnecessary. – Fuzzical Logic Oct 12 '11 at 10:18
  • You sir, are a gentleman and a scholar. Thank you very much. I did have one final question though - the ACTION_MOVE event never seems to fire in the onIntercept method, but i got the scrolling to work by moving the myState = 1 into ACTION_DOWN instead. Are you sure you're pasting actual code that works, or can you possibly think of why it wouldn't fire? – Andreas Eriksson Oct 13 '11 at 06:19
  • Yes, this is the actual code that works. However, before I devised that solution, I quickly learned that multiple onIntercepts on nested views can conflict with each other. Now, what you have to know is that once it returns `true` once, EVERY other event will occur in the onTouchEvent until the Event is complete or aborted. Other than that.... hmmm... I had one class that I had to take the top two lines out because they were constantly just returning (not allowing ACTION_MOVE to fire. Those are the only two circumstances I can think of. – Fuzzical Logic Oct 13 '11 at 08:34
  • Ah. On a tangential note, would you have any pointers at all as to how to approach making sure the user can't scroll out of the bounds of the Grid? – Andreas Eriksson Oct 13 '11 at 08:40
  • `if (gridView.getScrollX() > 0 && gridView.getScrollX() < gridView.getMeasuredWidth())` is one possibility. A lot of it depends on the layout cycle of the entire `Activity` which I am still dealing with in one of my applications. That `if` statement seems to be pretty reliable (as long as you didn't override `onMeasure()`. – Fuzzical Logic Oct 13 '11 at 08:45
  • This all works quite well and good, but i have a couple of more questions, sadly. After some testing, i found that my OnInterceptTouchEvent only receives ACTION_DOWN, which means i can't do anything with the MOVE and UP bits. Also, for some reason the rows that aren't immediately visible when first loading the view are invisible, and i can't get them to show - there's an empty space of the appropriate height where the missing 3-4 rows should be. – Andreas Eriksson Oct 16 '11 at 20:01
  • You might be running out of memory. How many images are you showing? Are they the same size as the size you want to display? With images it can be tough to tell where you need to be. (Sometimes, depending on the device and operation, you may run out of memory, but the app won't force close. The only way you know is by reading the log). If you are, you might consider a lazy loading solution, or make some thumbnails, or a variety of things... Sometimes, all it takes is a few extra Garbage Collections... – Fuzzical Logic Oct 17 '11 at 15:05
  • In response to your onIntercept... Its only MEANT to fire on the ACTION_DOWN. If you've down everything right, you shouldn't have to worry about ACTION_MOVE or ACTION_UP. Those are more useful for special other functionality... Like starting a movement in one View and finishing in another. Once onIntercept returns `true`, your normal onTouch handles the rest, right? – Fuzzical Logic Oct 17 '11 at 15:07
  • Yeah, but it never returns true, since action_down just saves the place we fingered last and carries on. If nothing but Action_Down is ever fired, mystate is always touch_state_rest and the method never returns true. Or am i missing something here? Again, thanks a lot for your help and patience. – Andreas Eriksson Oct 18 '11 at 06:06
  • If the scrolling is working, it is sending back "true" just once. And that is on the instant your finger even thinks it is moving. If the scrolling is not working, it is not sending beck true at all. – Fuzzical Logic Oct 18 '11 at 17:19
  • I'm not quite sure why, but I only get the onInterceptTouchEvent at the start of the whole gesture - that is it sees the down, and if it returns true then my touch event gets the subsequent events, but if it returns false neither onInterceptTouchEvent nor onTouch trigger again until there has been an up event. Of course when the down event arrives you don't know it you are going to want to capture the event stream or not. The class I am using extends GridView. This is really irritating as this looks like such an elegant solution! – pootle Oct 24 '12 at 14:39
  • I thought perhaps dispatchTouchEvent would work, but it too sees only the first event in the series. I am beginning to suspect I will have to capture them all and then once I know it its not MY swipe, feed them all back in!
    Running on ICS (4.0.3)
    – pootle Oct 24 '12 at 14:40
  • That is absolutely correct. You have to continually capture all Touch Events during a swipe... Then you return false as soon as you know. – Fuzzical Logic Jan 27 '13 at 07:54
  • It looks good, but how to trigger the `GridView` (`Adapter`) to display the rest of the grid when scrolling down? let's say, I have a 4x4 grid,but only 2x2 items are visible. If I scroll right, I can see the rest of the items (since I specified column=4), but scrolling down, I only see 2. Thanks for your help! – mbmc May 29 '14 at 20:58
5

A GridView can have content that extends beyond the size of the screen, but only in the vertical direction.

Question: What is the best widget to use to implement the desired effect ?
The are no default widgets (at least prior to 3.0) which implement 2D scrolling. All of them implement 1D (scrolling in one direction) only. Sadly the solution to your problem is not that easy.

You could:

  • Place a TableLayout inside a custom ScrollView class, and handle the touch events yourself. This would allow you to do proper 2D panning, but would not be so good for large data-sets since there would be no view recycling
  • Modify GridView if possible to enable horizontal scrolling functionality (probably the best solution if you can do it)
  • Try putting a TableLayout inside a ScrollView (with layout_width="wrap_content) which you put inside a HorizontalScrollView - though this may work it wouldn't be the optimum solution
  • Do something with OpenGL and draw straight to a canvas - this is how the default Gallery 3D app does it's "wall of pictures"
Joseph Earl
  • 23,351
  • 11
  • 76
  • 89
0

I have implemented a GridView that overrides the onInterceptTouchEvent methods. I set the OnTouchListeneras explained by Fuzzical Logic. My issue is that setting a custom OnTouchListener, the objects of my gridView are not clickable anymore. Is that because the default OnTouchListener of a GridView calls onItemClickListener ?

I would like to have a 2 directions scrollable GridView but that has still cliackable objects. Should I implement my own method to check whether a touch matches a item or is there a easier way to achieve this ?

Grodak

Community
  • 1
  • 1
Gordak
  • 2,060
  • 22
  • 32
0

I think that the only Android native solution for this is to have an embebed WebView in which you can have that bi-dimensional scroll (and by the way is the only thing that has this capability).

Be pressing an image you can control the behavior by a callback from JavaScript to JAva.

This is not very "pretty" but... it's the only viable way to do what you want "out of the box".

neteinstein
  • 17,529
  • 11
  • 93
  • 123
  • webview is an interesting idea. I'd probably use this idea as a last resort. So far, I'm still in research mode and the best "sample" I've found is here: http://stackoverflow.com/questions/3058164/android-scrolling-an-imageview – Someone Somewhere May 12 '11 at 16:49
  • another research sample that maybe I can adjust for a wall of images: http://blog.sephiroth.it/2011/04/04/imageview-zoom-and-scroll/ – Someone Somewhere May 12 '11 at 16:54