184

Possible Duplicate:
Horizontal ListView in Android?

Like many things in Android, you wouldn't think this would be such a hard problem but ohhh, by golly, would you be wrong. And, like many things in Android, the API doesn't even provide a reasonably extensible starting point. I'll be damned if I'm going to roll my own ListView, when all I want is to take the thing and turn it on its side. \rant

Okay, now that I'm done fuming, let's talk about the problem itself. What I need is basically something exactly like the Gallery, but without the center-locking feature. I don't really need ListView's listSelector but it's a nice-to-have. Mostly, I could do what I want with a LinearLayout inside a ScrollView, but I need the child views to come from a ListAdapter and I would really like to have a view recycler. And I really don't want to write any layout code.

I peeked into the source code for some of these classes...

Gallery: It looks like I could use the Gallery if I override most of the 'onXyz' methods, copy all their source code, but refrain from calling scrollIntoSlots(). But I'm sure that if I do that I'll run into some member field that's inaccessible or some other unforeseen consequence.

AbsSpinner: Since the mRecycler field is package-private I doubt I'll be able to extend this class.

AbsListView: It looks like this class is only meant for vertical scrolling, so no help there.

AdapterView: I've never had to extend this class directly. If you tell me it's easy to do, and that it's easy to roll my own RecycleBin, I'll be very skeptical but I'll give it a shot.

I suppose I could possibly copy both AbsSpinner and Gallery to get what I want... hopefully those classes aren't using some package-private variable I can't access. Do y'all think that's a good practice? Does anyone have any tutorials or third-party solutions that might put me in the right direction?

Update:
The only solution I've found so far is to do it all myself. Since asking this question, I have overridden AdapterView and implemented my own "HorizontalListView" from scratch. The only way to truly override the Gallery's center-locking feature is to override the private scrollIntoSlots method, which I believe would require generating a subclass at runtime. If you're bold enough to do that, it's arguably the best solution, but I don't want to rely on undocumented methods that could change.

Swathi EP below suggested that I give the Gallery an OnTouchListener and override the scroll functionality. If you don't care about having fling support in your list, or if it's okay for the views to snap to the center at the end of the fling animation, then this will work for you! However, in the end it still proves impossible to remove the center-locking feature without removing fling support. And I ask you, what kind of list doesn't fling?

So, alas, this did not work for me. :-( But if you're interested in this approach, read on...

I also had to make some additions to Swathi's code to get what I wanted. In GestureListener.onTouch, in addition to delegating to the gesture detector, I also had to return true for ACTION_UP and ACTION_CANCEL events. That successfully disabled the center-locking feature, but it also disabled flinging. I was able to re-enable fling by having my own GestureListener delegate to the Gallery's onFling method. If you want to try it out, go into your ApiDemos sample code and replace the Gallery1.java class with the following code:

import com.example.android.apis.R;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.GestureDetector;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.View.OnTouchListener;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;

public class Gallery1 extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.gallery_1);

        // Reference the Gallery view
        final Gallery g = (Gallery) findViewById(R.id.gallery);

        // Set the adapter to our custom adapter (below)
        g.setAdapter(new ImageAdapter(this));

        // Set a item click listener, and just Toast the clicked position
        g.setOnItemClickListener(new OnItemClickListener() {
            public void onItemClick(AdapterView parent, View v, int position, long id) {
                Toast.makeText(Gallery1.this, "" + position, Toast.LENGTH_SHORT).show();
            }
        });

        // Gesture detection
        final GestureDetector gestureDetector = new GestureDetector(new MyGestureDetector(g));
        OnTouchListener gestureListener = new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                boolean retVal = gestureDetector.onTouchEvent(event);
                int action = event.getAction();
                if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                    retVal = true;
                    onUp();
                }
                return retVal;
            }

            public void onUp() {
                // Here I am merely copying the Gallery's onUp() method.
                for (int i = g.getChildCount() - 1; i >= 0; i--) {
                    g.getChildAt(i).setPressed(false);
                }
                g.setPressed(false);
            }
        };
        g.setOnTouchListener(gestureListener);

        // We also want to show context menu for longpressed items in the gallery
        registerForContextMenu(g);
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        menu.add(R.string.gallery_2_text);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
        Toast.makeText(this, "Longpress: " + info.position, Toast.LENGTH_SHORT).show();
        return true;
    }

    public class ImageAdapter extends BaseAdapter {
        int mGalleryItemBackground;

        public ImageAdapter(Context c) {
            mContext = c;
            // See res/values/attrs.xml for the <declare-styleable> that defines
            // Gallery1.
            TypedArray a = obtainStyledAttributes(R.styleable.Gallery1);
            mGalleryItemBackground = a.getResourceId(
                    R.styleable.Gallery1_android_galleryItemBackground, 0);
            a.recycle();
        }

        public int getCount() {
            return mImageIds.length;
        }

        public Object getItem(int position) {
            return position;
        }

        public long getItemId(int position) {
            return position;
        }

        public View getView(int position, View convertView, ViewGroup parent) {
            ImageView i = new ImageView(mContext);

            i.setImageResource(mImageIds[position]);
            i.setScaleType(ImageView.ScaleType.FIT_XY);
            i.setLayoutParams(new Gallery.LayoutParams(136, 88));

            // The preferred Gallery item background
            i.setBackgroundResource(mGalleryItemBackground);

            return i;
        }

        private Context mContext;

        private Integer[] mImageIds = {
                R.drawable.gallery_photo_1,
                R.drawable.gallery_photo_2,
                R.drawable.gallery_photo_3,
                R.drawable.gallery_photo_4,
                R.drawable.gallery_photo_5,
                R.drawable.gallery_photo_6,
                R.drawable.gallery_photo_7,
                R.drawable.gallery_photo_8
        };
    }

    public class MyGestureDetector extends SimpleOnGestureListener {

        private Gallery gallery;

        public MyGestureDetector(Gallery gallery) {
            this.gallery = gallery;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 
                float velocityY) {
            return gallery.onFling(e1, e2, velocityX, velocityY);
        }
    }

}
Community
  • 1
  • 1
Neil Traft
  • 18,367
  • 15
  • 63
  • 70

8 Answers8

48

After reading this post, I have implemented my own horizontal ListView. You can find it here: http://dev-smart.com/horizontal-listview/ Let me know if this helps.

tomrozb
  • 25,773
  • 31
  • 101
  • 122
Paul
  • 1,718
  • 1
  • 13
  • 4
  • One question: why do you do thread synchronization in your implementation? Everything runs 100% on UI thread. – Paul Turchenko Sep 08 '11 at 17:45
  • 8
    This widget DOES NOT respect the wrap_content layout param. If you don't hardcode it's height it will make you spend the rest of the day trying to figure out why none of your views below it are showing up as it gobbles all of the screen space. – John Oct 07 '11 at 06:05
  • 3
    I've added support for "wrap_content" in layout_height attribute. You can found this fix in forked version of HorizontalViewList: https://github.com/inazaruk/android-dev-smart-lib – inazaruk Aug 08 '12 at 13:30
  • Nice work, but I have tried your horizontal list but I have a problem, with custom xml item, custom adapter, and image downloader (cache), the images lost reference and I have to double click to display them. With native vertical container this problem does not happen. Image Downloader link: http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html Thanks for your help! – Hpsaturn Nov 21 '12 at 21:36
  • I tried putting this Horizontal ListView inside a listView.(nested) And the performance(scroll effect) is awful. Has anyone tried nesting HorizontalListView inside a ListView as "Pulse" app does? – eugene Nov 28 '12 at 03:22
  • @PaulTurchenko I am having problem with this HorizontalListVIew.It is creating empty space While scrolling from right to left, so that It is always increasing space at left side(before the first item). Can you help me on this. – Noundla Sandeep Apr 26 '13 at 05:49
  • I used Dev-Smart's HorizontalListView it's perfect but I'm not able to apply a pressed style for an item click on the list view. Any suggestions? – nesimtunc Jun 04 '13 at 17:07
  • 1
    I cannot find the sourcecode for generating jar file. The link to github had some changes: "Started new github project at http://github.com/dinocore1/Android-Horizontal-ListView" can anyone point me the sourcecode or tha jar file itself? – Duna Aug 20 '13 at 22:09
  • 1
    @Lunatikul Sure: https://github.com/dinocore1/DevsmartLib-Android – Mark Phillip Sep 26 '13 at 17:33
  • setSelection is broken... – desgraci Oct 08 '13 at 19:29
  • it doesn't work with android-support-v7-appcompat – Guilherme Gregores Mar 23 '14 at 15:06
  • 1
    Paul, the link is not working on her, may indicate an alternative? – Fernando Leal Jul 25 '14 at 11:50
  • This Library was a great workaround, Thanks to the author Paul .... But using Recycler view its very easy now ! .... Anyone looking here should definitely look at recyclerview. – Devrath Apr 15 '15 at 10:27
12

Have you looked into using a HorizontalScrollView to wrap your list items? That will allow each of your list items to be horizontally scrollable (what you put in there is up to you, and can make them dynamic items similar to ListView). This will work well if you are only after a single row of items.

Thira
  • 1,555
  • 1
  • 13
  • 24
  • 25
    Two reasons I don't go with this approach: (1) It doesn't extend `AdapterView`, so I can't use it interchangeably with my `ListView` and `ListAdapter` code, and (2) There is no recycling of views. Maybe it would be possible to tack this functionality onto the scroll view, but it seems to me that it would mess with the scrolling behavior, and it would be nearly as much work as simply extending `AdapterView`. – Neil Traft Oct 19 '10 at 13:41
4

You know, it might be possible to use an existing ListView with some judicious overriding of dispatchDraw() (to rotate the Canvas by 90 degrees), onTouch() (to swap the X and Y of the MotionEvent coords) and maybe onMeasure() or whatever to fool it into thinking it's y by x rather than x by y...

I have no idea if this would actually work but it'd be fun to find out. :)

Reuben Scratton
  • 38,595
  • 9
  • 77
  • 86
  • 3
    Haha, I like your attitude... sort of afraid to try it... I'm going to try out Swathi EP's suggestion first. – Neil Traft Nov 12 '10 at 18:23
  • So, I went ahead and implemented my own `AdapterView` subclass, as I figured it would be just as much work and less risk. Trying your suggestion would be a fun experiment, though. :-) – Neil Traft Nov 14 '10 at 00:46
  • With the added bonus that the technique could work for any View type. The hard bit would be to find practical applications! :) – Reuben Scratton Nov 15 '10 at 17:55
  • Having looked into this, as long as your solution doesn't require re-implementing code from the super class in your subclass that uses mLeft or any m[property] for that matter you might be in luck. You simple can't access these properties from your own custom classes, and this renders nearly every solution impossible. – Emile Nov 25 '10 at 14:46
4

This might be a very late reply but it is working for us. We are using the same gallery provided by Android, just that, we have adjusted the left margin such a way that the screens left end is considered as Gallery's center. That really worked well for us.

  • 1
    Gallery has been deprecated and replaced by HorizontalScrollView in the API since this was posted. Just slap a LinearLayout onto it and you can now add items through code. Works great, though no recycling huge amounts of items. – G_V Nov 12 '14 at 08:25
3

I used Pauls (see his answer) Implementation of HorizontalListview and it works, thank you so much for sharing!

I slightly changed his HorizontalListView-Class (btw. Paul there is a typo in your classname, your classname is "HorizontialListView" instead of "HorizontalListView", the "i" is too much) to update child-views when selected.

UPDATE: My code that I posted here was wrong I suppose, as I ran into trouble with selection (i think it has to do with view recycling), I have to go back to the drawing board...

UPDATE 2: Ok Problem solved, I simply commented "removeNonVisibleItems(dx);" in "onLayout(..)", I guess this will hurt performance, but since I am using only very small Lists this is no Problem for me.

I basically used this tutorial here on developerlife and just replaced ListView with Pauls HorizontalListView, and made the changes to allow for "permanent" selection (a child that is clicked on changes its appearance, and when its clicked on again it changes it back).

I am a beginner, so probably many ugly things in the code, let me know if you need more details.

Community
  • 1
  • 1
free
  • 31
  • 2
  • It's nice that he got it working, but it doesn't recycle views, the data is static, the order of items isn't guaranteed and it's really hard to adjust/fix without knowing exactly what you're doing due to the lack of comments in it. If you know exactly what you're doing, you don't need this solution in the first place. Man, I wish the android team would just implement this already. I only keep a few items per scrollview to keep it fast, while using a LinkedList to guarantee positioning on redraws. It's not ideal, but functions well enough. – G_V Nov 12 '14 at 08:22
2

Gallery is the best solution, i have tried it. I was working on one mail app, in which mails in the inbox where displayed as listview, i wanted an horizontal view, i just converted listview to gallery and everything worked fine as i needed without any errors. For the scroll effect i enabled gesture listener for the gallery. I hope this answer may help u.

Swathi EP
  • 3,864
  • 6
  • 26
  • 25
  • What do you mean by "i enabled gesture listener for the gallery?" Did you just override the Gallery's `onScroll` method and do the scrolling yourself? If so, how did you tell the gallery's children to reposition themselves? – Neil Traft Nov 01 '10 at 13:38
  • I wrote an class extending SimpleOnGestureListener, in which i have overridden onfling() method. The below code shows you gesture detection and gesturelistener of my gallery: – Swathi EP Nov 02 '10 at 06:28
  • 1
    // Gesture detection gestureDetector = new GestureDetector(new MyGestureDetector()); gestureListener = new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { if (gestureDetector.onTouchEvent(event)) { return true; } return false; } }; myGallery.setOnTouchListener(gestureListener); – Swathi EP Nov 02 '10 at 06:29
  • I've been really busy and haven't gotten the chance to try this out, but this sounds very promising. Hopefully I will get around to it this weekend... – Neil Traft Nov 12 '10 at 18:24
  • Tried out your solution today. It wasn't quite as simple as you made it sound, and ultimately it was inadequate for my needs. I'll add the details to my original post for any who are interested. +1 for effort. – Neil Traft Nov 13 '10 at 23:49
0

Have you looked into the ViewFlipper component? Maybe it can help you.

http://developer.android.com/reference/android/widget/ViewFlipper.html

With this component, you can attach two or more view childs. If you add some translate animation and capture Gesture detection, you can have a nicely horizontal scroll.

brent
  • 91
  • 3
  • I'll definitely look into that if I ever implement a Home-screen-like paging view. But since only one thing can be shown at a time, it's not truly like a list view, and would exhibit the center-locking behavior that I'm trying to avoid. – Neil Traft Nov 14 '10 at 00:48
-1

My app uses a ListView in portraint mode which is simply switches to Gallery in landscape mode. Both of them use one BaseAdapter. This looks like shown below.

       setContentView(R.layout.somelayout);
       orientation = getResources().getConfiguration().orientation;

       if ( orientation == Configuration.ORIENTATION_LANDSCAPE )
       {

            Gallery gallery = (Gallery)findViewById( R.id.somegallery );

            gallery.setAdapter( someAdapter );

            gallery.setOnItemClickListener( new OnItemClickListener() {
            @Override
            public void onItemClick( AdapterView<?> parent, View view,
                    int position, long id ) {

                onClick( position );
            }
        });
       }
       else
       {
            setListAdapter( someAdapter );

            getListView().setOnScrollListener(this);
       }    

To handle scrolling events I've inherited my own widget from Gallery and override onFling(). Here's the layout.xml:

    <view 
    class="package$somegallery"
    android:id="@+id/somegallery" 
    android:layout_height="fill_parent" 
    android:layout_width="fill_parent">
    </view>

and code:

    public static class somegallery extends Gallery
    {
        private Context mCtx;

        public somegallery(Context context, AttributeSet attrs)
        {
            super(context, attrs);
            mCtx = context;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {

            ( (CurrentActivity)mCtx ).onScroll();

            return super.onFling(e1, e2, velocityX, velocityY);
        }
   }
ackio
  • 662
  • 1
  • 5
  • 12
  • So far as I can see, this will still have the Gallery's center-locking behavior. What does your activity's `onScroll()` method do? – Neil Traft Nov 14 '10 at 00:45
  • Actually i removed some piece of code here before calling onScroll() since it doesn't cover the question. If it's interesting to you i call getLastVisiblePosition() which is used in onScroll to determide whether the last item is visible and do some logic. Concerning center-locking - yes it has this behavior. – ackio Nov 15 '10 at 03:51