Android. 同时滚动两个ListView

24

好的,我要实现的目标是创建一个与Excel中“冻结窗格”效果相同的布局。也就是说,我想要一个标题行,它可以与主ListView水平滚动,以及一个左侧的ListView,该ListView可以与主ListView垂直滚动。在另一维度滚动时,标题行和左侧的ListView应保持不动。

这是xml布局:

<?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>

我接着在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);      
}

当用户滚动右侧 ListView 时,应该导致左侧 ListView 滚动。不幸的是它并没有这样做。

我在网上搜了一下,似乎 setSelectionFromTop() 函数不能用于嵌套在多个布局中的 ListView。

如果是这种情况,有人能建议一种使它们一起滚动的方法,或者一种不同的布局设置方式,或者完全不同的技术吗?


你尝试过使用smootHSCrollToPosition吗:http://developer.android.com/reference/android/widget/ListView.html#smoothScrollToPosition(int) - Waza_Be
感谢您的快速回复。然而,这不是解决方案。我希望能够使列表视图一起平滑滚动。 - s1ni5t3r
@s1ni5t3r,你有解决我正在面对的同样问题的方法吗? - CoronaPintu
4个回答

34

重写

我试着在一个ListView中传递滚动操作到另一个ListView中,但没有成功。因此,我选择了一种不同的方法:传递 MotionEvent。这样每个ListView可以计算自己的平滑滚动、快速滚动或其他任何操作。

首先,我们需要一些类变量:

ListView listView;
ListView listView2;

View clickSource;
View touchSource;

int offset = 0;

我添加到listView的每个方法都几乎相同适用于listView2,唯一的区别是listView2将引用listView(而不是它本身)。我没有包含重复的listView2代码。

其次,我们从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;
    }
});
为了避免循环逻辑:listView调用listView2,再调用listView……我使用了一个类变量touchSource来确定何时应传递MotionEvent。我假设您不希望在listView中点击一行也会导致在listView2中点击,因此我使用另一个类变量clickSource来防止这种情况发生。 第三点: 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
        }
    }
});

第四, 直接传递每个触摸事件并不完美,因为偶尔会出现差异。而OnScrollListener非常适合消除这些差异:

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) {}
});

(可选) 最后,你提到你的布局中listViewlistView2从不同高度开始导致了问题...我强烈建议修改你的布局来平衡这两个ListView,但我找到了一种解决方法。不过,这有点棘手。
在整个布局渲染完成之前,你无法计算两个布局之间的高度差,但是此时没有回调函数...所以我使用了一个简单的处理程序:

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]);
    }
};

假设半秒延迟足够渲染布局并在onResume()中启动计时器:

我认为,在onResume()方法中,等待半秒钟足以完成布局的渲染,并且开始计时器。

handler.sendEmptyMessageDelayed(0, 500);
如果您使用了偏移量,我想要明确的是,listView2 的 OnScroll 方法会减去偏移量而不是加上它:
listView2.setSelectionFromTop(firstVisibleItem, view.getChildAt(0).getTop() - offset);

希望能有所帮助!


1
谢谢Sam,我原本期望得到一个丑陋的简单解决方案来应对初始问题,但最终得到了一个完美执行的优雅解决方案。不过我还没有足够的声望给你的答案点赞。 - s1ni5t3r
1
我认为在 MotionEvent.ACTION_CANCEL 上也应该重置 clickSource 和 touchSource。 - Jon Willis
我有两个相邻的片段,我在静态列表视图变量中保留引用。问题是当我点击第一个列表视图中的项目时,第二个列表中的第一个项目也会自动被点击。就像第一个列表在第二个列表视图中调用了单击事件一样。下一个问题是当我滚动第一个列表视图,然后想通过移动第二个列表视图来滚动。在第一次尝试滚动时,只有第二个列表会滚动。然后它就像应该的那样工作了。我不知道它是否与我在列表上拥有的其他东西有关,第一个列表具有onitemclick监听器,而第二个列表具有btns。http://pastebin.com/ubDw2VY1 - MyWay
我看到你提到在执行操作之前检查点击的父元素,但是如果我要点击位于listview上的按钮,该怎么办?当我首先单击listview1,然后再单击listview2时,clickSource等于listview1。 - MyWay
@Sam!你是我的英雄! - desidigitalnomad
显示剩余10条评论

8

好的,我现在有一个答案了。问题是,如果listview嵌套在其他布局中,那么.setSelectionFromTop()方法将无法正常工作。经过一些思考后,我意识到我可以将布局设置为RelativeLayout,这样就可以获得相同的外观效果,而不必为复选框和listview嵌套布局。下面是新的布局:

<?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>

这基本上是与布局相关的代码。
在onCreate()中。
engListView=getListView();
engListView.setOnTouchListener(this);

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

以及监听器方法:

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());
}

实际上,我现在已经修改了代码,以便使用另一个ListView的onScrollListener移动根布局中的第一个ListView,并且它反过来使用onTouchListener移动另一个ListView。结果非常流畅和准确。如果有人想看代码,我可以发布它。感谢Sam给予的卓越建议。 - s1ni5t3r
通过这段代码一起滚动两个ListView非常卡顿,有没有办法让它更流畅? - whizzzkey

5
这篇文章帮我解决了我遇到的一个类似问题。它基于截取触摸事件。在这种情况下,同步适用于多个列表视图,并且完全对称。
主要挑战是防止列表项单击传播到其他列表视图。我需要仅由最初接收到触摸事件的列表视图分派点击和长按,包括特别的高亮反馈,当您只需在项目上轻触时(拦截onClick事件无用,因为在调用层次结构中太迟)。
关键是两次拦截触摸事件。首先,将初始触摸事件中继到其他列表视图。相同的onTouch处理程序函数捕获这些事件并将其提供给GestureDetector。在GestureDetector回调中,静态触摸事件(例如onDown等)被消耗(返回true),而动作手势则不会被消耗(返回false),以便可以进一步由视图本身分派并触发滚动。
在onScroll中使用setPositionFromTop对我来说不起作用,因为它使滚动行为极其缓慢。相反,使用onScroll来将初始滚动位置与添加到Syncer的新ListView对齐。
到目前为止唯一存在的问题是s1ni5t3r提出的问题。如果您双击listView,它们仍然会断开连接。
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) { }
}

编辑:使用一个状态变量可以解决双重挥动问题。

private boolean scrolling;

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

public void onScrollStateChanged(AbsListView view, int scrollState) 
{
    scrolling = scrollState != SCROLL_STATE_IDLE;
}

今晚我打算尝试实现这个。如果成功了,我会告诉你的。希望如果失败了,你能帮助我? :) - Kgrover
嗯,这似乎根本不起作用 :( 它们几乎立即不同步。 - Kgrover
很抱歉听到这个消息,Kgrover。即使您在列表上没有“双重挥动”,它是否仍然无法同步?如果不是,那么上面答案中的编辑可能是一个解决方案 - 通过这种方式,我的应用程序中的列表视图现在非常稳定。也许这也取决于您的手机速度有多快(我的是S3,我还没有在任何其他手机上测试过)。 - Glemi
感谢@Glemi提供了一个不错的实现,你为我类似的问题提供了一个好的解决方案。 - atabek
@Glemi 我无法在我的代码中使用它,我的意思是如何在列表视图上实现您的类,将ListScrollSyncer类附加到哪里?假设我有listview1和listview2,并且我希望在滚动listview2时自动滚动listview1。 - Amitabh Sarkar

3

我通过将ListView更改为RecyclerView来解决它,使用RecyclerView .addOnScrollListener来同步滚动事件。而且没有差异,非常完美!

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);
            }
        }
    });

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接