如何在不使ListView折叠的情况下将其放入ScrollView中?

359

我在解决这个问题时搜索了很多方案,但唯一的答案似乎是 "不要把ListView放进ScrollView中"。然而,我还没有看到任何真正的解释 为什么 不应该这样做。唯一能找到的原因是Google认为你不应该这样做。但是我想这样做,所以我尝试了。

那么问题来了,如何将ListView置于ScrollView中而不使其折叠到最小高度?


11
因为当设备使用触摸屏时,很难区分两个嵌套的可滚动容器。在传统的桌面上使用滚动条来滚动容器是可行的。 - Romain Guy
57
当然有。如果它们在内圈触碰到了,滚动那个。如果内圈太大了,那就是糟糕的用户界面设计。这并不意味着这个想法总体上是不好的。 - DougW
5
我刚在我的平板应用上发现了这个问题。虽然在智能手机上看起来并不太糟糕,但在平板电脑上却很糟糕,因为用户很容易分不清是要滚动外部ScrollView还是内部ScrollView。@DougW:我同意,这是一个糟糕的设计。 - jellyfish
4
@Romain - 这篇文章相当陈旧,所以我想知道这里提出的问题是否已经得到解决,或者是否仍然没有好的方法可以在 ScrollView 中使用 ListView(或者其他不需要手动编写所有 ListView 细节的 LinearLayout 的替代方法)? - Jim
3
@Jim:“或者有一种替代方案,不需要将ListView的所有细节手动编码到LinearLayout中”--将所有内容放在ListView内。ListView可以拥有多个行样式,以及非可选行。 - CommonsWare
显示剩余8条评论
26个回答

263

这是我的解决方案。我对Android平台还比较新,我相信这有点像hackish,特别是关于直接调用.measure并直接设置LayoutParams.height属性的部分,但它确实起作用。

您只需要调用Utility.setListViewHeightBasedOnChildren(yourListView),它将被重新调整大小以完全适应其项的高度。

public class Utility {
    public static void setListViewHeightBasedOnChildren(ListView listView) {
        ListAdapter listAdapter = listView.getAdapter();
        if (listAdapter == null) {
            // pre-condition
            return;
        }

        int totalHeight = listView.getPaddingTop() + listView.getPaddingBottom();

        for (int i = 0; i < listAdapter.getCount(); i++) {
            View listItem = listAdapter.getView(i, null, listView);
            if (listItem instanceof ViewGroup) {
                listItem.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
             }

             listItem.measure(0, 0);
             totalHeight += listItem.getMeasuredHeight();
        }

        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
        listView.setLayoutParams(params);
    }
}

66
你刚刚重新创建了一个非常昂贵的LinearLayout :) - Romain Guy
82
除了 LinearLayout 没有 ListView 所具有的所有内在优点,如分隔线、页眉/页脚视图、与手机 UI 主题颜色匹配的列表选择器等等。说实话,没有理由不能滚动内部容器直到到达末尾,然后停止拦截触摸事件,以便外部容器滚动。每个桌面浏览器在使用滚轮时都会这样做。如果你通过轨迹球导航,Android 本身可以处理嵌套滚动,那么为什么不通过触摸呢? - Neil Traft
29
如果listItemViewGroup的实例,那么执行listItem.measure(0,0)会抛出空指针异常。在listItem.measure语句之前,我添加了以下代码:if (listItem instanceof ViewGroup) listItem.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); - Siklab.ph
4
针对除了0以外的ListView填充问题的修复方法: int totalHeight = listView.getPaddingTop() + listView.getPaddingBottom(); - Paul
8
ListView 中的项具有可变高度时,使用此方法存在一些缺陷。调用 getMeasuredHeight() 仅返回目标最小高度,而不是实际高度。我尝试使用 getHeight(),但这会返回 0。请问有谁知道如何解决这个问题? - Matt Huggins
显示剩余23条评论

199
使用 ListView 来使其不滚动非常昂贵,而且与 ListView 的整个目的相违背。你不应该这样做。只需使用 LinearLayout 即可。

70
源自我,因为过去 2 到 3 年来我一直负责 ListView :) ListView 做了很多额外的工作来优化适配器的使用。试图绕过它仍会导致 ListView 做很多 LinearLayout 不必做的工作。我不会详细解释 ListView 的实现,因为这里没有足够的空间来解释。这也无法解决 ListView 在触摸事件方面的行为方式。而且,如果您的 hack 今天有效,也不能保证将来的版本中它仍将有效。使用 LinearLayout 确实不需要太多工作。 - Romain Guy
7
那是一个合理的回应。如果我可以给您一些反馈,我有更丰富的iPhone经验,我可以告诉您,在性能特征和使用案例(或反模式)方面,苹果的文档写得要好得多。总的来说,安卓文档更加分散且缺乏重点。我知道这背后有一些原因,但那是一个漫长的话题,如果您想聊一下,请告诉我。 - DougW
6
你可以轻松编写一个静态方法,从适配器中创建一个LinearLayout。 - Romain Guy
125
这不是“如何在ScrollView中放置ListView而不会导致它崩溃”的答案...你可以指出这么做不应该,但同时给出如何实现的答案,那就是一个好答案。你知道ListView除了滚动之外还有一些其他很酷的功能,这些功能LinearLayout没有。 - Jorge Fuentes González
45
如果您有一个包含ListView和其他视图的区域,并且希望所有内容一起滚动,该怎么办?在ScrollView中放置ListView似乎是一个合理的用例。但是,使用LinearLayout替换ListView不是一个解决方案,因为那样您将无法使用适配器。 - Barry Fruitman
显示剩余17条评论

89

这肯定会起作用……
你只需将布局XML文件中的<ScrollView></ScrollView>替换为此自定义ScrollView,例如:<com.tmd.utils.VerticalScrollview></com.tmd.utils.VerticalScrollview>

package com.tmd.utils;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ScrollView;

public class VerticalScrollview extends ScrollView{

    public VerticalScrollview(Context context) {
        super(context);
    }

     public VerticalScrollview(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public VerticalScrollview(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                    Log.i("VerticalScrollview", "onInterceptTouchEvent: DOWN super false" );
                    super.onTouchEvent(ev);
                    break;

            case MotionEvent.ACTION_MOVE:
                    return false; // redirect MotionEvents to ourself

            case MotionEvent.ACTION_CANCEL:
                    Log.i("VerticalScrollview", "onInterceptTouchEvent: CANCEL super false" );
                    super.onTouchEvent(ev);
                    break;

            case MotionEvent.ACTION_UP:
                    Log.i("VerticalScrollview", "onInterceptTouchEvent: UP super false" );
                    return false;

            default: Log.i("VerticalScrollview", "onInterceptTouchEvent: " + action ); break;
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        super.onTouchEvent(ev);
        Log.i("VerticalScrollview", "onTouchEvent. action: " + ev.getAction() );
         return true;
    }
}

4
非常好。它还可以将Google Maps碎片放入ScrollView中,并且仍然可以缩放。谢谢。 - Magnus Johansson
有人注意到这种方法的缺点了吗?它太简单了,我有点担心啊:o - Marija Milosevic
1
好的解决方案,应该被接受的答案 +1!我已经将它与下面的答案(来自djunod)混合在一起,效果很棒! - tanou
1
这是唯一的修复方法,让我能够同时滚动和单击子元素。非常感谢。 - pellyadolfo

25

与其将ListView放在ScrollView中,我们可以将ListView作为ScrollView使用。需要放在ListView中的内容可以放在ListView中。顶部和底部的其他布局可以通过将布局添加到ListView的头部和尾部来放置。因此,整个ListView将给您滚动的体验。


3
在我看来,这是最优雅和合适的答案。关于这种方法的更多细节,请参阅此答案:https://dev59.com/k2Ml5IYBdhLWcg3we283#20475821 - adamdport

21

有许多情况下,将ListView放在ScrollView中非常合理。

以下是基于DougW的建议编写的代码...可在片段中使用,占用更少的内存。

public static void setListViewHeightBasedOnChildren(ListView listView) {
    ListAdapter listAdapter = listView.getAdapter();
    if (listAdapter == null) {
        return;
    }
    int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(), MeasureSpec.AT_MOST);
    int totalHeight = 0;
    View view = null;
    for (int i = 0; i < listAdapter.getCount(); i++) {
        view = listAdapter.getView(i, view, listView);
        if (i == 0) {
            view.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth, LayoutParams.WRAP_CONTENT));
        }
        view.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
        totalHeight += view.getMeasuredHeight();
    }
    ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
    listView.setLayoutParams(params);
    listView.requestLayout();
}

在每个嵌入的列表视图上调用setListViewHeightBasedOnChildren(listview)


当列表项的大小不固定时,即某些可扩展到多行的文本视图时,它就无法正常工作。有什么解决办法吗? - Khobaib
请查看我的评论 - 它提供了完美的解决方案 - http://stackoverflow.com/a/23315696/1433187 - Khobaib
在API 19上对我有用,但在API 23上没有:这里会在listView下面添加很多空白空间。 - Lucker10

17

ListView实际上已经能够测量自身的高度以足够显示所有项,但当您仅指定wrap_content (MeasureSpec.UNSPECIFIED)时,它不会执行此操作。当给定一个高度为MeasureSpec.AT_MOST时,它将执行此操作。有了这个知识,您可以创建一个非常简单的子类来解决这个问题,它比以上任何解决方案都要好得多。使用此子类时仍应该使用wrap_content。

public class ListViewForEmbeddingInScrollView extends ListView {
    public ListViewForEmbeddingInScrollView(Context context) {
        super(context);
    }

    public ListViewForEmbeddingInScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ListViewForEmbeddingInScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 4, MeasureSpec.AT_MOST));
    }
}

将 heightMeasureSpec 设置为 AT_MOST,并设置一个非常大的值 (Integer.MAX_VALUE >> 4),会导致 ListView 测量所有子项的高度,然后根据其高度设置 ListView 的高度。

与其他解决方案相比,这种方法有几个优点:

  1. 它可以正确测量所有内容(包括padding和分隔线)
  2. 它可以在测量过程中测量 ListView
  3. 由于优点 #2,它可以正确处理宽度或项目数量的更改,而无需任何附加代码

缺点是,你可能会认为这是依赖于SDK中未记录的行为,这可能会发生变化。另一方面,你可能会认为这才是 ListView 中 wrap_content 应该工作的方式,而当前的 wrap_content 行为实际上是错误的。

如果你担心这种行为可能会在将来发生变化,那么你应该简单地复制 ListView.java 中的 onMeasure 函数和相关函数到自己的子类中,然后使 UNSPECIFIED 路径通过 onMeasure 运行 AT_MOST 路径。

顺便说一下,我认为这是在处理少量列表项时完全有效的方法。与 LinearLayout 相比可能效率低下,但当列表项较少时,使用 LinearLayout 是不必要的优化,因此是不必要的复杂性。


也适用于列表视图中的可变高度项! - Denny
@Jason Y,Integer.MAX_VALUE >> 4 这个是什么意思?那里的4是什么意思? - KJEjava48
@Jason Y,只有在我将Integer.MAX_VALUE >> 2更改为Integer.MAX_VALUE >> 21后,它才能正常工作。为什么会这样?而且它只能与ScrollView一起使用,而不能与NestedScrollView一起使用。 - KJEjava48

13

2
这适用于LinearLayout,正如他所演示的那样。但对于ListView则不起作用。根据该帖子的日期,我想他写这篇文章是为了提供他的替代方案的示例,但这并没有回答问题。 - DougW
这是快速解决方案。 - user6558741

11

您可以创建一个不可滚动的自定义 ListView

public class NonScrollListView extends ListView {

    public NonScrollListView(Context context) {
        super(context);
    }
    public NonScrollListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public NonScrollListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
                    Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
            super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
            ViewGroup.LayoutParams params = getLayoutParams();
            params.height = getMeasuredHeight();    
    }
}
在您的布局资源文件中。
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <!-- com.Example Changed with your Package name -->

    <com.Example.NonScrollListView
        android:id="@+id/lv_nonscroll_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </com.Example.NonScrollListView>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/lv_nonscroll_list" >

        <!-- Your another layout in scroll view -->

    </RelativeLayout>
</RelativeLayout>

在Java文件中:

创建自定义列表视图的对象,而不是ListView,如下所示: NonScrollListView non_scroll_list = (NonScrollListView) findViewById(R.id.lv_nonscroll_list);


8

我们无法同时使用两个滚动条。我们需要获取ListView的总长度,并根据总高度扩展ListView。然后我们可以直接将ListView添加到ScrollView中,或者使用LinearLayout,因为ScrollView只有一个子元素。 在您的代码中复制setListViewHeightBasedOnChildren(lv)方法并扩展listview,然后您就可以在scrollview中使用listview了。 \ layout xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
 <ScrollView

        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
         android:background="#1D1D1D"
        android:orientation="vertical"
        android:scrollbars="none" >

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:background="#1D1D1D"
            android:orientation="vertical" >

            <TextView
                android:layout_width="fill_parent"
                android:layout_height="40dip"
                android:background="#333"
                android:gravity="center_vertical"
                android:paddingLeft="8dip"
                android:text="First ListView"
                android:textColor="#C7C7C7"
                android:textSize="20sp" />

            <ListView
                android:id="@+id/first_listview"
                android:layout_width="260dp"
                android:layout_height="wrap_content"
                android:divider="#00000000"
               android:listSelector="#ff0000"
                android:scrollbars="none" />

               <TextView
                android:layout_width="fill_parent"
                android:layout_height="40dip"
                android:background="#333"
                android:gravity="center_vertical"
                android:paddingLeft="8dip"
                android:text="Second ListView"
                android:textColor="#C7C7C7"
                android:textSize="20sp" />

            <ListView
                android:id="@+id/secondList"
                android:layout_width="260dp"
                android:layout_height="wrap_content"
                android:divider="#00000000"
                android:listSelector="#ffcc00"
                android:scrollbars="none" />
  </LinearLayout>
  </ScrollView>

   </LinearLayout>

Activity类中的onCreate方法:

 import java.util.ArrayList;
  import android.app.Activity;
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.ListAdapter;
  import android.widget.ListView;

   public class MainActivity extends Activity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.listview_inside_scrollview);
    ListView list_first=(ListView) findViewById(R.id.first_listview);
    ListView list_second=(ListView) findViewById(R.id.secondList);
    ArrayList<String> list=new ArrayList<String>();
    for(int x=0;x<30;x++)
    {
        list.add("Item "+x);
    }

       ArrayAdapter<String> adapter=new ArrayAdapter<String>(getApplicationContext(), 
          android.R.layout.simple_list_item_1,list);               
      list_first.setAdapter(adapter);

     setListViewHeightBasedOnChildren(list_first);

      list_second.setAdapter(adapter);

    setListViewHeightBasedOnChildren(list_second);
   }



   public static void setListViewHeightBasedOnChildren(ListView listView) {
    ListAdapter listAdapter = listView.getAdapter();
    if (listAdapter == null) {
        // pre-condition
        return;
    }

    int totalHeight = 0;
    for (int i = 0; i < listAdapter.getCount(); i++) {
        View listItem = listAdapter.getView(i, null, listView);
        listItem.measure(0, 0);
        totalHeight += listItem.getMeasuredHeight();
    }

    ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight
            + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
    listView.setLayoutParams(params);
      }

好的修复!它按照需要工作。谢谢。在我的情况下,我有一堆视图在列表之前,完美的解决方案是在视图上只有一个垂直滚动条。 - Juampa

6

这是我唯一行之有效的方法:

在Lollipop版本及以上,您可以使用

yourtListView.setNestedScrollingEnabled(true);

启用或禁用此视图的嵌套滚动。如果需要向后兼容旧版本的操作系统,则必须使用RecyclerView。


这对于在AppCompat活动中的DialogFragment上的API 22上的列表视图没有任何影响。它们仍然会折叠。@Phil Ryan的答案(https://dev59.com/aXA75IYBdhLWcg3wDUm2#24645448)稍作修改后有效。 - Hakan Çelik Mk1

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