Search code examples
androidlistviewonscrolllistener

How to getY() for ListView items


In a horizontal LinearLayout (matching the screen width), I have two ListView instances (with dynamic list items) and a TextView in between. Using the TextView as a canvas, I would like to draw (Bezier) lines connecting either the selected list item in the left ListView with all of the list items in the right ListView or vice versa. Now it is perfectly possilbe that either the selected list item or any of the list items on the other side are outside the visible area of the ListView. In this case I would like to draw those lines (entering from above/below or leaving above/below) only as far as they are visible. In order to draw those lines, I need x and y values at the beginning and the end of each line. x is just textView.getLeft() and textView.getRight(), but y depends on the current scroll position of the two lists.

My problem is to get the current y values for specific ListView items:

list.getChildAt(indexOfCurrentlySelectedItem).getY()

I need to access those values once the list has been laid out. I need to determine which list items are and which list items are not visible (Do the list items not visible have a y value? A view? A recyclable view (using the holder pattern)?). I need to be able to iterate through a list I order to pass those y values to my onDraw method of the TextView. And finally I need to install a mechanism to redraw the lines after scrolling has occurred.

How do I get a hold of the current y values (after layout and after scrolling)? Do I have to keep track of the visible positions in a OnScrollListener? Do I have to listen for the end of the list layout in a OnGlobalLayoutListener? Do I (also) have to listen for the end of the list item layout in a OnGlobalLayoutListener for every list item?


Solution

  • Okay, I found a good solution: the OnScrollListener provides all I need, while the OnGlobalLayoutListener is not needed here.

    public class ArrayListAdapter extends ArrayAdapter<String>
        implements AbsListView.OnScrollListener
    
    ...
    
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {}
    
    @Override
    public synchronized void onScroll(final AbsListView listView, int firstVisibleItem,
                                      int visibleItemCount, int totalItemCount) {
        List<Integer>yList = new ArrayList<>();
        int checkedPosition = listView.getCheckedItemPosition();
        double yChecked = 0;
    
        for (int pos = 0; pos < totalItemCount; pos++) {
            CheckedTextView tv = (CheckedTextView) this
                    .getViewByPosition(pos, listView, firstVisibleItem,
                            firstVisibleItem + visibleItemCount - 1);
            if (pos == checkedPosition) {
                yChecked = tv.getY();
            }
            yList.add(pos, tv.getY());
        }        
        // Here I pass the fresh y-values to the fragment holding the 
        // two ListViews. After some further calculations the fragment
        // passes the data to the TextView where it is stored and
        // this.invalidate is called to eventually trigger the onDraw method.
        originalFragment.setScrollData(yList, yChecked);
    }
    
    private View getViewByPosition(int pos, AbsListView listView, int firstVisibleItem,
                                   int lastVisibleItem) {
        if (pos < firstVisibleItem || pos > lastVisibleItem) {
            // The next line is the only downside of this solution I have
            // found so far. If the convertView of the getView(int
            // position, View convertView, ViewGroup parent) method is null
            // the holder pattern I applied in my adapter becomes useless.
            // In my case the recreation of the CheckedTextViews is fast
            // enough though.
            return listView.getAdapter().getView(pos, null, listView);
        } else {
            return listView.getChildAt(pos - firstVisibleItem);
        }
    }