如何处理具有不同高度单元格的GridView?

7

注意:我承认标题有点模糊,但英语不是我的母语,我不确定如何用一句话描述问题。

背景

我正在尝试创建一个应用程序,显示所有应用程序信息在网格视图上。

网格视图的numColumns设置为auto_fit,以便其列数可以很好地适应各种屏幕。

问题

由于每个应用程序的信息有时可能很长,因此单元格的高度可能与相邻单元格不同。

我希望网格单元格的宽度相同(在所有情况下都有效),并且对于每一行,高度应该由该行单元格的最大高度确定。

这会导致发生奇怪的事情:

  1. 罕见:某些单元格变为空白。有时,在滚动和返回时,单元格会被填充...
  2. 相当普遍:某些单元格绘制在其他单元格上方。
  3. 相当普遍:某些单元格没有占用它们可以占用的空间,留下无法点击的空白空间。
  4. 非常罕见:在滚动时玩耍,如果你滚动到顶部,你会得到整个网格为空且无法滚动...

这里是一个截图,显示了#2和#3问题(它们通常不会一起显示):

enter image description here

我使用的代码

这是网格单元格的xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/list_divider_holo_dark" >

    <ImageView
        android:id="@+id/appIconImageView"
        android:layout_width="48dip"
        android:layout_height="48dip"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:adjustViewBounds="true"
        android:contentDescription="@string/app_icon"
        android:src="@drawable/ic_menu_help" />

    <TextView
        android:id="@+id/appLabelTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toLeftOf="@+id/isSystemAppImageView"
        android:layout_toRightOf="@+id/appIconImageView"
        android:ellipsize="marquee"
        android:text="label"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textStyle="bold"
        tools:ignore="HardcodedText" />

    <TextView
        android:id="@+id/appDescriptionTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/appLabelTextView"
        android:layout_below="@+id/appLabelTextView"
        android:layout_toLeftOf="@+id/isSystemAppImageView"
        android:ellipsize="marquee"
        android:text="description"
        tools:ignore="HardcodedText" />

    <ImageView
        android:id="@+id/isSystemAppImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/appDescriptionTextView"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:adjustViewBounds="true"
        android:contentDescription="@string/content_description_this_is_a_system_app"
        android:src="@drawable/stat_sys_warning" />

</RelativeLayout>

我尝试过的方法

我尝试了多种解决方案:

  1. 使用HorizontalListView似乎可以解决这个问题,但它不允许单击/长按项目。

  2. 我找到了一个很好的库,可以使textViews具有“跑马灯”效果 (在这里),但它的许可证并不那么宽松。

  3. 将单元格高度设置为固定值或将textViews设置为具有固定行数也可以工作,但这会削减textViews的内容(部分),而无法显示它们。

  4. 我还尝试使用LinearLayout代替RelativeLayout,但没有帮助。

  5. 可能的解决方案是获取网格视图上所有视图使用的最大高度(使用getView在所有项上),类似于此代码,但它假定您知道列数,并且您确实希望所有单元格的高度相同。

问题

我应该如何处理这个问题?

我真的应该使用类似于StaggeredGrid的东西吗?

有没有一种方法可以使textViews保持固定大小,同时允许滚动(手动或自动)显示其内容?


你尝试过将包含的相对布局的layout_height="wrap_content"设置吗? - Kuba Spatny
@KubaSpatny 是的,不幸的是,它没有帮助。:( - android developer
好的,StaggeredGrid 肯定可以工作,但可能有些过度。你提到了使用跑马灯效果库,你尝试过自己实现吗? - Kuba Spatny
我没有做,因为我不确定应该如何做,以及完成这个工作需要多长时间。我在考虑可能应该从HorizontalScrollView扩展它,但它将无法触摸。我还需要定制它,让它知道如何移动。 - android developer
TextView有android:maxLines="4"标签(或android:maxLength="10"),还有android:ellipsize="marquee",但是在ListView中的问题是只有当特定视图处于焦点状态时才会滚动。因此,您需要创建一个扩展TextView的类,如此处所示:https://dev59.com/UHI-5IYBdhLWcg3wfoa1 - Kuba Spatny
是的,我知道。这是我尝试使用它的其他测试的一部分。我现在尝试了你发送的链接,并使用了我使用过的跑马灯配置,但它根本不滚动(也没有自动滚动)。似乎只有在singleLine="true"时才能使用跑马灯功能。你认为可以对horizontalScrollView进行哪些操作,以便允许用户手动滚动,但不会干扰gridView单元格上的触摸?我尝试了这个:https://dev59.com/LGw15IYBdhLWcg3w0fKh#6384443,但它没有起作用...由于某种原因,gridView识别它并阻止了对单元格的触摸... - android developer
3个回答

6

除了上面的解决方案,还可以使用RecyclerViewGridLayoutManager:

recyclerView.setHasFixedSize(false);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));

0

好的,我已经找到了一个不错的解决方案,它将使行取决于它们的子项,但为此我不得不做出一些妥协:

  1. 这是一个适配器,用于listView,其行具有水平线性布局。

  2. 使用此代码的人应该设置单元格的选择器,并选择在单击或长按时要执行的操作。

  3. 使用此代码的人应该准备好为listView的额外最后单元格准备一个空单元格。

  4. 所使用的listView应将其listSelector设置为透明,以禁止单击行。

我希望任何人都能提出更好的解决方案,而不需要额外的视图。

示例用法:

_listAdapter=new GriddedListViewAdapter();
_listAdapter.setNumberOfColumns(numberOfColumns);
-listAdapter.setItems(items);
_listView.setAdapter(_listAdapter);

这是代码:

public abstract class GriddedListViewAdapter<ItemType> extends BaseAdapter
  {
  private int                       _numColumns =1;
  private final List<Row<ItemType>> _rows       =new ArrayList<Row<ItemType>>();
  protected final Context           _context    =App.global();
  private List<ItemType>            _items;

  public void setItems(final List<ItemType> items)
    {
    _items=items;
    prepareRows();
    notifyDataSetChanged();
    }

  private void prepareRows()
    {
    _rows.clear();
    final int itemsCount=_items.size();
    for(int i=0;i<itemsCount;i+=_numColumns)
      {
      final Row<ItemType> row=new Row<ItemType>();
      row.startIndex=i;
      row.items=new ArrayList<ItemType>(_numColumns);
      for(int j=0;j<_numColumns;++j)
        {
        ItemType item;
        if(i+j<itemsCount)
          item=_items.get(i+j);
        else item=null;
        row.items.add(item);
        }
      _rows.add(row);
      }
    }

  public void setNumberOfColumns(final int numColumns)
    {
    if(_numColumns==numColumns)
      return;
    _numColumns=numColumns;
    if(_items!=null)
      {
      prepareRows();
      notifyDataSetChanged();
      }
    }

  @Override
  public final int getCount()
    {
    return _rows.size();
    }

  @Override
  public final Row<ItemType> getItem(final int position)
    {
    return _rows.get(position);
    }

  @Override
  public final long getItemId(final int position)
    {
    return position;
    }

  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  @Override
  public final View getView(final int position,final View convertView,final ViewGroup parent)
    {
    final Row<ItemType> row=getItem(position);
    IcsLinearLayout rowLayout=(IcsLinearLayout)convertView;
    if(rowLayout==null)
      {
      rowLayout=new IcsLinearLayout(_context,null);
      rowLayout.setMeasureWithLargestChildEnabled(true);
      rowLayout.setShowDividers(IcsLinearLayout.SHOW_DIVIDER_MIDDLE);
      rowLayout.setDividerDrawable(_context.getResources().getDrawable(R.drawable.list_divider_holo_dark));
      rowLayout.setOrientation(LinearLayout.HORIZONTAL);
      rowLayout.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
      }
    final int childCount=rowLayout.getChildCount();
    for(int i=childCount;i>_numColumns;--i)
      rowLayout.removeViewAt(i-1);
    // reuse previous views of the row if possible
    for(int i=0;i<_numColumns;++i)
      {
      // reuse old views if possible
      final View cellConvertView=i<childCount ? rowLayout.getChildAt(i) : null;
      // fill cell with data
      final View cellView=getCellView(row.items.get(i),row.startIndex+i,cellConvertView,rowLayout);
      LinearLayout.LayoutParams layoutParams=(LinearLayout.LayoutParams)cellView.getLayoutParams();
      if(layoutParams==null)
        {
        layoutParams=new LinearLayout.LayoutParams(0,LayoutParams.MATCH_PARENT,1);
        layoutParams.gravity=Gravity.CENTER_VERTICAL;
        cellView.setLayoutParams(layoutParams);
        }
      else
        {
        final boolean needSetting=layoutParams.weight!=1||layoutParams.width!=0||layoutParams.height!=LayoutParams.MATCH_PARENT;
        if(needSetting)
          {
          layoutParams.width=0;
          layoutParams.height=LayoutParams.MATCH_PARENT;
          layoutParams.gravity=Gravity.CENTER_VERTICAL;
          layoutParams.weight=1;
          cellView.setLayoutParams(layoutParams);
          }
        }
      if(cellConvertView==null)
        rowLayout.addView(cellView);
      }
    return rowLayout;
    }

  @Override
  public final int getViewTypeCount()
    {
    return super.getViewTypeCount();
    }

  @Override
  public final int getItemViewType(final int position)
    {
    return super.getItemViewType(position);
    }

  /**
   * should handle getting a single cell view. <br/>
   * NOTE:read the parameters description carefully !
   * 
   * @param item
   * the item that is associated with the cell. if null, you should prepare an empty cell
   * @param rawPosition the position within the original list of items that is associated with the cell
   * @param convertView
   * a recycled cell. you must use it when it's not null, fill it with data, and return it
   * @param parent
   * the parent of the view. you should use it for inflating the view (but don't attach the view to the
   * parent)
   */
  public abstract View getCellView(ItemType item,int rawPosition,View convertView,ViewGroup parent);

  // ////////////////////////////////////
  // Row//
  // /////
  private static class Row<ItemType>
    {
    int                 startIndex;
    ArrayList<ItemType> items;
    }
  }

0

这里有一个可行的解决方案:http://obduro.nl/blog/the-solution-of-android-gridview-overlap/

该解决方案在链接中解释了以每行元素的最大高度为基础来计算每行高度的方法。

它通过捕获onScrollChanged事件并在FirstVisiblePosition更改时更新高度来实现此目的。

经过测试,这个解决方案非常有效,但有一个缺陷:一旦行大小增加,它就不会减小。

可以在这里找到实际代码:https://github.com/JJdeGroot/AutoGridView


1
不错,但是还有RecyclerView的解决方案。 - android developer
我一直把RecyclerView看作是ListView,而不是GridView,但我可能错了吗?RecyclerView可以有多列吗?GridView的问题在于它存在漏洞。 - 3c71
1
RecyclerView可以按照您的意愿显示项目。唯一的问题是,由于其高度模块化,它变得非常复杂,并且缺少一些我们习惯拥有的功能,例如快速滚动、页眉和页脚。对于快速滚动,我已经制作了一个库(在这里:https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller)。对于页脚和页眉,您需要将第一个项目和最后一个项目设置为不同类型,但遗憾的是,这与ListView类似。 - android developer
那么,使用RecyclerView发布答案可能会很有用? - 3c71

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