How to get location (on screen) of row in listview
Asked Answered
P

3

7

I have implemented some gesture detection code so that I can detect when a row in my listview (which is situated in a FrameLayout) has been swiped.

public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
        int itemId = MainActivity.this.getListView().pointToPosition((int) e1.getX(),(int) e1.getY());
        Offer order = (Offer)MainActivity.this.getListView().getAdapter().getItem(itemId);
        View v = MainActivity.this.getListView().getChildAt(itemId);
    }
}

I want to display a view over the top of the swiped row with a set of context sensitive options for that row.

My problems is that the following methods:

v.getTop()
v.getBottom()

return correct values only before the view has been scrolled. I could probably do some calculation to work out offsets using scroll positions etc but I also noticed that I only get values when I swipe a row that is visible on screen to begin with. If I scroll down the list and then swipe a row (that was not originally off-screen) then these methods return null values.

The methods below seem to suffer from the same problem...

v.getLocationOnScreen(loc)
v.getLocationInWindow(loc)

Ultimately, I am looking to find the distance between the top of the visible listView and the row that has been swiped. I will then use this distance to add a view to the parent FrameLayout with the appropriate height padding (so that it covers the swiped row).

Any help would be much appreciated!

Piperine answered 26/1, 2010 at 13:43 Comment(0)
P
17

There are two different ways of indexing ListView items, by position in the adapter, or by visible place on the screen. An item will always be at the same position within the adapter (unless you alter the list). The index into view.getChildAt() will vary depending on what is visible on the screen.

The position being returned by pointToPosition is the position within the adapter. If you want to get the physical view, you need to account for if the view is scrolled. Try this:

int adapterIndex = MainActivity.this.getListView().pointToPosition((int) e1.getX(),(int) e1.getY());
int firstViewItemIndex = MainActivity.this.getListView().getFirstVisiblePosition();
int viewIndex = adapterIndex - firstViewItemIndex;
View v = MainActivity.this.getListView().getChildAt(viewIndex);
Phillisphilly answered 26/1, 2010 at 16:32 Comment(8)
Thanks for the reply. Actually I'm not keeping track of the view objects. At runtime, I dynamically get hold of the row when it is swiped. I then make the above calls to try and figure out it's location on the screen. I'm not changing the row itself I simply want to display a view over the top of the row. I have a scroll listener that then removes the new view as soon as the list is scrolled again. A demonstration of the feature can be seen in this demo of tweetie 2 for the iPhone: youtube.com/watch?v=Vxjj3F5MCpQ#t=1m12sPiperine
Could you elaborate on how you dynamically get a hold of the row at runtime, when it is swiped?Phillisphilly
Using GestureDetector and GestureListener on the ListView. I have updated the code snippet above to include the onFling method (and params) which is called when a user swipes their finger on the list. Some simple maths is used to check that the user is swiping from left to right, then pointToPosition is used to find which row the swipe corresponds to.Piperine
Thanks. OK... PointToRowId and PointToPosition both return the correct id (sequence number in list) for the row that is swiped. However, my subsequent call to 'View v = MainActivity.this.getListView().getChildAt(itemId);' returns null when referencing an itemId that was off-screen to begin with (or outside of the 800px height of my Nexus One)Piperine
Did you check the values of RowId vs Position after you have scrolled? They really shouldn't be the same thing...Phillisphilly
I just checked and they both return the same value.Piperine
Ok, sorry, I think you are right.. It depends on the implementation of getItemId in your Adapter implementation. The documentation says that it should take in a "position" and return a "rowId", but in practice it often just returns position. I think the point about indexing in adapter vs listView is still true though. See my edit above.Phillisphilly
Thanks for this. Question. I'm attempting to animate each row separately, but it animates each row in succession as they are swiped. So, 1, then 1 and 2, then 1, 2, and 3 animate. Little help?Salerno
P
1

I've solved this but it feels like a bit of a hack and probably isn't the most efficient implementation. Surely there must be a cleaner way to do this using the Android APIs?

In the code below, I have added a HashMap to my ArrayAdaptor to keep track of which on screen views are holding which rows (since these are recycled as you scroll the list)

All changes that I made to my original adaptor have been commented below.

 class RestaurantAdapter extends ArrayAdapter<Offer> {
            Map hash = null;  //HashMap to keep track of rowId and on screen View

            RestaurantAdapter() {
                super(MainActivity.this, android.R.layout.simple_list_item_1, model);
                hash = new HashMap(); //instantiate HashMap in constructor
            }

             //method to return correct View on screen for row id
            public View getViewOnScreen(int rowId) {
                return (View)hash.get(rowId);
            }

            public View getView(int position, View convertView,
                                                    ViewGroup parent) {
                View row=convertView;
                hash.put(position, convertView); // add/update view for row position as it is recycled 

                RestaurantWrapper wrapper=null;

                if (row==null) {                                                    
                    LayoutInflater inflater=getLayoutInflater();

                    row=inflater.inflate(R.layout.row, parent, false);
                    wrapper=new RestaurantWrapper(row);
                    row.setTag(wrapper);
                }
                else {
                    wrapper=(RestaurantWrapper)row.getTag();
                }

                wrapper.populateFrom(model.get(position));

                return(row);
            }


        }
Piperine answered 27/1, 2010 at 10:34 Comment(2)
I tried this solution, and it didn't work for me. I couldn't modify the returned view. Anyway, using getChildAt did work on the list did work in my case. I'm not sure why I couldn't modify the returned view using this method.Detonation
Actually getChildAt didn't work for me for the same reason it didn't work for you, so I ended up using the other answer on this page and that worked.Detonation
V
1

This is the best way I have found to detect which row was touched.

            Rect rect = new Rect();
            int childCount = mListView.getChildCount();
            int[] listViewCoords = new int[2];
            mListView.getLocationOnScreen(listViewCoords);
            int listX = (int) event.getRawX() - listViewCoords[0];
            int listY = (int) event.getRawY() - listViewCoords[1];

            View child;
            for (int i = 0; i < childCount; i++) {
                child = mListView.getChildAt(i);
                child.getHitRect(rect);
                if (rect.contains(listX, listY)) {
                    mDownView = child;
                    break;
                }
            }

You can use this in onTouch method at ACTION_DOWN event. Greetings.

Vital answered 14/4, 2014 at 12:10 Comment(1)
Thanks. However, getChildAt() does return the first visible child (and not the first child in the list). To make it work you can use for(int i = 0 ; i < mListView.getLastVisiblePosition() - mListView.getFirstVisiblePosition() ; ++i) Then, the touched element is in position "i + mListView.getFirstVisiblePosition".Farnesol

© 2022 - 2024 — McMap. All rights reserved.