24

OK. What I'm trying to achieve is a layout that does the same effect as frozen panes in Excel. That is I want a header row that scrolls horizontally with the main ListView and a left hand ListView that scrolls vertically with the main ListView. The header row and the left hand listview should remain stationary when scrolling in the other dimension.

Here is the xml layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recordViewLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <LinearLayout android:layout_width="160dp"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <CheckBox
            android:id="@+id/checkBoxTop"
            android:text="Check All"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <ListView android:id="@+id/engNameList"
            android:layout_width="160dp"
            android:layout_height="wrap_content"/>
    </LinearLayout> 


    <HorizontalScrollView  
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout android:id="@+id/scroll"  
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <include layout="@layout/record_view_line" android:id="@+id/titleLine" />

            <ListView 
                android:id="@android:id/list"
                android:layout_height="wrap_content"
                android:layout_width="match_parent"/>

        </LinearLayout>

    </HorizontalScrollView>
</LinearLayout>

I'm then using this code in the ListActivity

public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    View v = recordsListView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();

((ListView)findViewById(R.id.engNameList)).setSelectionFromTop(firstVisibleItem, top);      
}

This should cause the left hand ListView to scroll when the right hand one is scrolled by the user. Unfortunately it doesn't.

I've had a bit of a google about and it seems the setSelectionFromTop() function will not work on a ListView that is nested inside more than one layout.

If this is the case can anyone suggest a way to get them to scroll together or a different way to set up the layout or a different technique altogether.

s1ni5t3r
  • 711
  • 2
  • 7
  • 17
  • Have you tried smootHSCrollToPosition: http://developer.android.com/reference/android/widget/ListView.html#smoothScrollToPosition(int) – Waza_Be Sep 09 '12 at 20:05
  • Thanks for the quick reply. However this isn't the solution. I am looking to get the listviews to scroll smoothly together. – s1ni5t3r Sep 09 '12 at 20:34
  • @s1ni5t3r you got solution same issue i am facing. – CoronaPintu Jun 30 '14 at 05:56

4 Answers4

34

Rewrite

I didn't have much luck with passing the scrolling actions in one ListView to another. So I chose a different method: passing the MotionEvent. This lets each ListView calculate their own smooth scroll, fast scroll, or anything else.

First, we'll need some class variables:

ListView listView;
ListView listView2;

View clickSource;
View touchSource;

int offset = 0;

Every method that I add to listView will be almost identical for listView2, the only difference is that listView2 will reference listView (not itself). I didn't include the repetitive listView2 code.

Second, let's start with the OnTouchListener:

listView = (ListView) findViewById(R.id.engNameList);
listView.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(touchSource == null)
            touchSource = v;

        if(v == touchSource) {
            listView2.dispatchTouchEvent(event);
            if(event.getAction() == MotionEvent.ACTION_UP) {
                clickSource = v;
                touchSource = null;
            }
        }

        return false;
    }
});

To prevent circular logic: listView calls listView2 calls listView calls... I used a class variable touchSource to determine when a MotionEvent should be passed. I assumed that you don't want a row click in listView to also click in listView2, so I used another class variable clickSource to prevent this.

Third, the OnItemClickListener:

listView.setOnItemClickListener(new OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if(parent == clickSource) {
            // Do something with the ListView was clicked
        }
    }
});

Fourth, passing every touch event isn't perfect because occasional discrepancies appear. The OnScrollListener is perfect for eliminating these:

listView.setOnScrollListener(new OnScrollListener() {
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if(view == clickSource) 
            listView2.setSelectionFromTop(firstVisibleItem, view.getChildAt(0).getTop() + offset);
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {}
});

(Optional) Lastly, you mentioned that you have trouble since listView and listView2 begin at different heights in your layout... I highly recommend modifying your layout to balance the ListViews, but I found a way to address this. However it is a little tricky.
You cannot calculate the difference in height between the two layouts until after the entire layout have been rendered, but there is no callback for this moment... so I use a simple handler:

Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        // Set listView's x, y coordinates in loc[0], loc[1]
        int[] loc = new int[2];
        listView.getLocationInWindow(loc);

        // Save listView's y and get listView2's coordinates
        int firstY = loc[1];
        listView2.getLocationInWindow(loc);

        offset = firstY - loc[1];
        //Log.v("Example", "offset: " + offset + " = " + firstY + " + " + loc[1]);
    }
};

I assume that a half second delay is long enough to render the layout and start the timer in onResume():

handler.sendEmptyMessageDelayed(0, 500);

If you do use an offset I want to be clear that listView2's OnScroll method subtracts the offset rather than adds it:

listView2.setSelectionFromTop(firstVisibleItem, view.getChildAt(0).getTop() - offset);

Hope that helps!

Sam
  • 86,580
  • 20
  • 181
  • 179
  • Not sure if I'm doing something wrong but although the dispatchTouchEvent is called it doesn't seem to send any touch event to the other listview. OnTouchEvent is not called for the other listview anyway. – s1ni5t3r Sep 10 '12 at 10:58
  • I noticed that `isFocused()` didn't behave the way I first thought so I switched tactics. Hope this helps. – Sam Sep 10 '12 at 18:38
  • Magic. This is the solution. Works perfectly. Thanks Sam. – s1ni5t3r Sep 10 '12 at 19:28
  • Oh Dear. I spoke too soon. This method works well but if you try to scoot the list (that is you give it another push while it's still in motion) the two lists become disconnected. – s1ni5t3r Sep 14 '12 at 14:51
  • I updated my code and tested it with ListViews nested in various layouts and even starting different heights, I've managed to get them synced again. Let me know if you find another flaw in more extensive testing! – Sam Sep 15 '12 at 15:35
  • I'll have to give this atry later and see how it works for me. It looks like the fully comprehensive solution now that I can get .setSelectionFromTop() to work. The main problem I had was that .setSelectionFromTop() would not work when I had the second listview in a layout that was itself in a layout. I wasn't that happy about only one listview being touchable and your code should solve that. I came here for a simple ugly solution and may have a beautiful one. – s1ni5t3r Sep 18 '12 at 17:27
  • 1
    Thanks Sam, I was expecting a simple ugly solution to get around the initial problem but have come away with an elegant solution that performs perfectly. I'm afraid I don't have the rep to vote your answer up though. – s1ni5t3r Sep 18 '12 at 17:47
  • @Sam This is unsyncing a lot of the time. I am using a headerview to create an offset. Could you consider helping me out?http://stackoverflow.com/questions/15725367/parallel-list-scrolling-side-by-side-android – Kgrover Mar 31 '13 at 23:17
  • 1
    I think you should also reset clickSource and touchSource on `MotionEvent.ACTION_CANCEL` – Jon Willis Oct 17 '13 at 17:27
  • Hey Sam, You saved me, this is the best sollution which is working everywhere, Thanks – shyam.y Jun 17 '14 at 11:40
  • @Sam this is gud solution but on both listview scrol it's change position. – CoronaPintu Jun 30 '14 at 06:01
  • I have two fragments one by another, I keep reference in static listview variable.Problem is when I click on the item in first listview, cause automatically first item from secondly list is clicked as well.Like first list invoke click event in second listview. Next problem is when I scroll the first listview and then want to scroll by moving second one. In first try of scrolling only second list scrolls. Then it works like it should. I don't know if it's connected with other stuff which I have on the lists, first list has onitemclick listener and second has btns. http://pastebin.com/ubDw2VY1 – MyWay Sep 16 '14 at 13:23
  • I see that u wrote about checking parent of click before i make action but how should i do for clicking on button which is on the listview, but when I make it with but when I first click on the listview1 and I click on listview2 the clickSource is equal the listview1 – MyWay Sep 16 '14 at 15:00
  • @Sam!. You're my hero! – desidigitalnomad Mar 24 '15 at 22:12
  • This is the simplest way !!! Easy to implement two listview sync scroll. If someone want to nest listview to HorizontalScrollView, there is one problem listview only get down event and cannot get up event when HorizontalScrollView finish scroll. So when HorizontalScrollView stop scroll, and mClickSource should be set to null. – vvv Nov 29 '16 at 12:40
8

OK. I have an answer now. The problem being that .setSelectionFromTop() would only work if the listview was in the top layout (ie. not nested). Afters some head scratching I realised that I could make my layout a RelativeLayout and get the same look but without having to nest layouts for the checkbox and listview. This is the new layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recordViewLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <CheckBox android:id="@+id/checkBoxTop"
        android:text="Check All"
        android:layout_width="160dp"
        android:layout_height="wrap_content"/>


    <ListView android:id="@+id/engNameList"
        android:layout_width="160dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/checkBoxTop"/>       

    <HorizontalScrollView  
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/checkBoxTop">

        <LinearLayout android:id="@+id/scroll"  
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <include layout="@layout/record_view_line" android:id="@+id/titleLine" />

            <ListView 
                android:id="@android:id/list"
                android:layout_height="wrap_content"
                android:layout_width="match_parent"/>

        </LinearLayout>

    </HorizontalScrollView>

</RelativeLayout>

This basically is the code that goes with the layout.

In onCreate()

engListView=getListView();
engListView.setOnTouchListener(this);

recordListView=(ListView)findViewById(R.id.recordList);
recordListView.setOnScrollListener(this);

and the listener methods:

public boolean onTouch(View arg0, MotionEvent event) {
    recordListView.dispatchTouchEvent(event);
    return false;
}

public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    View v=view.getChildAt(0);
    if(v != null)
        engListView.setSelectionFromTop(firstVisibleItem, v.getTop());
}
Max Base
  • 639
  • 1
  • 7
  • 15
s1ni5t3r
  • 711
  • 2
  • 7
  • 17
  • In fact I have now changed the code as well so that the first ListView (the one in the root layout) is moved using the onScrollListener of the other and it in turn moves the other ListView using an onTouchListener. The result is very smooth and accurate. If anyone wants to see the code I can post it. Thanks to Sam for his impressive input. – s1ni5t3r Oct 05 '12 at 07:32
  • Scrolling together 2 ListViews by this code is very freeze, is there a way to make it smooth? – whizzzkey Jun 23 '14 at 08:44
5

This thread has helped me find a solution for a similar problem I've been struggling with for a while. It is also based on intercepting touch events. In this case the syncing works for multiple listviews and is entirely symmetric.

The major challenge was to prevent list item clicks to propagate to the other listviews. I need clicks and long clicks dispatched only by the listview that initially received the touch event, including in particular the highlight feedback when you just touch down on an item (intercepting onClick event is no use since it's too late in the calling hierarchy).

The key to this is intercepting the touch event twice. First, the initial touch event is relayed to the other listviews. The same onTouch handler function catches these and feeds them to a GestureDetector. In the GestureDetector callbacks the static touch events (onDown etc.) are consumed (return true) whereas the motion gestures aren't (return false) such that they can be further dispatched by the view itself and trigger the scrolling.

Using setPositionFromTop inside onScroll didn't work for me because it makes the scrolling behavior extremely sluggish. OnScroll is used instead to align initial scroll positions as new ListViews are added to the Syncer.

The only problem that persists so far is the one brought up by s1ni5t3r above. If you double-fling the listView then they still become disconnected.

public class ListScrollSyncer
    implements AbsListView.OnScrollListener, OnTouchListener, OnGestureListener
{
    private GestureDetector gestureDetector;
    private Set<ListView>   listSet = new HashSet<ListView>();
    private ListView currentTouchSource;

    private int currentOffset = 0;
    private int currentPosition = 0;

    public void addList(ListView list)
    {
        listSet.add(list);
        list.setOnTouchListener(this);
        list.setSelectionFromTop(currentPosition, currentOffset);

        if (gestureDetector == null)
            gestureDetector = new GestureDetector(list.getContext(), this);
    }

    public void removeList(ListView list)
    {
        listSet.remove(list);
    }

    public boolean onTouch(View view, MotionEvent event)
    {
        ListView list = (ListView) view;

        if (currentTouchSource != null)
        {
            list.setOnScrollListener(null);
            return gestureDetector.onTouchEvent(event);
        }
        else
        {
            list.setOnScrollListener(this);
            currentTouchSource = list;

            for (ListView list : listSet)
                if (list != currentTouchSource)
                    list.dispatchTouchEvent(event);

            currentTouchSource = null;
            return false;
        }
    }

    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
    {
        if (view.getChildCount() > 0)
        {
            currentPosition = view.getFirstVisiblePosition();
            currentOffset   = view.getChildAt(0).getTop();
        }
    }

    public void onScrollStateChanged(AbsListView view, int scrollState) { }

    // GestureDetector callbacks
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }
    public boolean onSingleTapUp(MotionEvent e) { return true; }
    public boolean onDown(MotionEvent e) { return true; }
    public void onLongPress(MotionEvent e) { }
    public void onShowPress(MotionEvent e) { }
}

edit: the double-fling issue can be resolved by using a state variable.

private boolean scrolling;

public boolean onDown(MotionEvent e) { return !scrolling; }

public void onScrollStateChanged(AbsListView view, int scrollState) 
{
    scrolling = scrollState != SCROLL_STATE_IDLE;
}
Glemi
  • 676
  • 1
  • 7
  • 17
  • I'm going to take a shot at implementing this tonight. I'll let you know if it works out. Hopefully you can help me if it doesn't? :) – Kgrover Apr 01 '13 at 01:54
  • Hm, this doesn't seem to work at all :( They get unsynced almost immediately. – Kgrover Apr 01 '13 at 04:02
  • Sorry to hear this Kgrover. Does it get unsynced even if you do not "double-fling" on the list? If not then the edit in the answer above might be a solution - with this the listviews in my app are now very stable. Perhpas it also depends on how fast your phone is (Mine is an S3 and I haven't tested it on any other phone). – Glemi Jun 10 '13 at 21:41
  • Thanks @Glemi for a nice implementation, you gave a nice solution to my similar problem. – atabek Jan 22 '15 at 05:30
  • @Glemi I am not able to use it on my code, I mean how to implement your class on list view, where to attach ListScrollSyncer class? Say I have listview1 and listview2, and I want listview1 to be auto scrolled when listview2 is being scrolled. – Amitabh Sarkar Apr 05 '17 at 11:37
3

I resolve it by changing ListView to RecyclerView , use RecyclerView .addOnScrollListener for synchronizing the scroll event. And there is not discrepancies,it's perfect!

private void syncScrollEvent(RecyclerView leftList, RecyclerView rightList) {

    leftList.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return rightList.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
        }
    });
    rightList.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return leftList.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
        }
    });


    leftList.addOnScrollListener(new RecyclerView.OnScrollListener() {

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
             if (recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
                 rightList.scrollBy(dx, dy);
            }
        }
    });
    rightList.addOnScrollListener(new RecyclerView.OnScrollListener() {
         @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            if (recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
                leftList.scrollBy(dx, dy);
            }
        }
    });

}
Peng Lin
  • 31
  • 3