Проблемы прокрутки и заголовка PinnedHeaderListView


Фон

Я пытаюсь подражать тому, как приложение контактов Lollipop показывает закрепленные заголовки для первой буквы контактов, о чем я уже писалздесь.

Проблема

Поскольку исходный код (который найден здесь, в папке "PinnedHeaderListViewSample") не показывает других букв, кроме английских, мне пришлось немного изменить код, но этого было недостаточно. То же самое касается и самого заголовка, который теперь он должен был быть слева, а не над рядами.

Все работало нормально, пока я не протестировал его на языках RTL (иврит в моем случае), в то время как локаль устройства также была изменена на язык RTL (иврит в моем случае) .

По какой-то причине вещи становятся действительно странными как в прокрутке, так и в самом заголовке, и странная часть заключается в том, что это происходит на некоторых устройствах/версиях Android.

Например, на Galaxy S3 с Kitkat прокрутка и полоса прокрутки являются совершенно неправильно (я прокручиваю до верха, но расположение полосы прокрутки находится в середине).

На LG G2 с Android 4.2.2 у него также есть эта проблема, но он также не показывает заголовки (за исключением закрепленного заголовка), особенно на иврите.

На Galaxy S4 и на Huwawei Ascend P7 (оба работают на Kitkat) все работало отлично, независимо от того, что я делал.

Короче говоря, специальный сценарий таков:
  1. используйте pinnedHeaderListView
  2. есть устройство используя локаль RTL, или сделать это через настройки разработчиков
  3. есть элементы listview на английском и иврите
  4. установите listView для отображения быстрого скроллера.
  5. прокрутите listView, используя либо быстрый скроллер, либо как вы это делаете без него.

Код

Количество кода очень велико, плюс я сделал 2 POCs, в то время как один из них сильно отличается от кода, с которого я начал (чтобы он выглядел как на леденце). поэтому я постараюсь показать минимальное сумма.

EDIT: большой POC-код доступен на Github, здесь .

"PinnedHeaderActivity.java "

Я добавил 2 элемента на иврите наверх, в поле "имена":

        "אאא",
        "בבב",

В методе "setupListView" я сделал видимой быструю полосу прокрутки:

    listView.setFastScrollEnabled(true);

В" NamesAdapter " CTOR, я сделал его поддержку больше, чем английский алфавит:

    public NamesAdapter(Context context, int resourceId, int textViewResourceId, String[] objects) {
        super(context, resourceId, textViewResourceId, objects);
        final SortedSet<Character> set = new TreeSet<Character>();
        for (final String string : objects) {
            final String trimmed = string == null ? "" : string.trim();
            if (!TextUtils.isEmpty(trimmed))
                set.add(Character.toUpperCase(trimmed.charAt(0)));
            else
                set.add(' ');
        }
        final StringBuilder sb = new StringBuilder();
        for (final Character character : set)
            sb.append(character);
        this.mIndexer = new StringArrayAlphabetIndexer(objects, sb.toString());
    }

"StringArrayAlphabetIndexer.java "

В метод" getSectionForPosition", я изменил его на:

public int getSectionForPosition(int position) {
    try {
        if (mArray == null || mArray.length == 0)
            return 0;
        final String curName = mArray[position];
        // Linear search, as there are only a few items in the section index
        // Could speed this up later if it actually gets used.
        // TODO use binary search
        for (int i = 0; i < mAlphabetLength; ++i) {
            final char letter = mAlphabet.charAt(i);
            if (TextUtils.isEmpty(curName) && letter == ' ')
                return i;
            final String targetLetter = Character.toString(letter);
            if (compare(curName, targetLetter) == 0)
                return i;
        }
        return 0; // Don't recognize the letter - falls under zero'th section
    } catch (final Exception ex) {
        return 0;
    }
}

List_item.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

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

        <include layout="@layout/list_item_header" />

        <include
            layout="@android:layout/simple_list_item_1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="50dp" />
    </FrameLayout>

    <View
        android:id="@+id/list_divider"
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:background="@android:drawable/divider_horizontal_dark" />

</LinearLayout>

List_item_header.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/header_text"
    android:layout_width="25dip"
    android:layout_height="25dip"
    android:textStyle="bold"
    android:background="@color/pinned_header_background"
    android:textColor="@color/pinned_header_text"
    android:textSize="14sp"
    android:paddingLeft="6dip"
    android:gravity="center" />

Вот 2 скриншота, один из которых не выглядит хорошо, а другой выглядит нормально:

Galaxy S3 kitkat, а также LG G2 4.2.2-не показывают заголовки на иврите и имеют странную прокрутку внизу (очень быстро идет вниз по сравнению с остальной прокруткой):

Введите описание изображения здесь

Galaxy S4 kitkat-показывает заголовки в порядке, но прокрутка странная внизу:

Введите описание изображения здесьПо какой-то причине Galaxy S4 не отражал пользовательский интерфейс, как это должно быть, хотя я выбрал его в настройках разработчика, так что это также может быть причиной того, почему он показал заголовки нормально.

Что я пробовал

Помимо того, что я опробовал 2 PoC, которые я сделал (один из которых гораздо больше похож на стиль дизайна материала, и он более сложный), я пробовал различные способы использования макетов, а также пытался использовать Layoutdirection значение для того, чтобы заставить заголовках показывать.

Еще более сложная проблема заключается в том, чтобы решить быструю полосу прокрутки, которая работает очень странно на более сложном POC и немного странно на простом (который быстро прокручивается в нижней части).

Вопрос

Каков правильный способ решения этих проблем?

Почему RTL имеет проблемы с этим типом пользовательского интерфейса ?

EDIT: похоже, что даже пример Google плохо обрабатывает RTL-элементы на простом ListView:

http://developer.android.com/training/contacts-provider/retrieve-names.html

Когда у него есть контакты на иврите, скроллер "сходит с ума".

1 3

1 ответ:

Ладно, я понятия не имею, что там делал Google, так как код очень нечитабелен, поэтому я создал свой собственный класс, и он отлично работает.

Единственное, что вы должны помнить, это сортировка элементов перед отправкой их в мой класс, и если вы хотите иметь только прописные буквы для заголовков, вы должны отсортировать элементы соответственно (так, чтобы все элементы, которые начинаются с определенной буквы, были в том же куске, независимо от того, прописные они или нет).

Решение доступно на GitHub, здесь: https://github.com/AndroidDeveloperLB/ListViewVariants

Вот код:

StringArrayAlphabetIndexer

public class StringArrayAlphabetIndexer extends SectionedSectionIndexer
  {
  /**
   * @param items                   each of the items. Note that they must be sorted in a way that each chunk will belong to
   *                                a specific header. For example, chunk with anything that starts with "A"/"a", then a chunk
   *                                that all of its items start with "B"/"b" , etc...
   * @param useOnlyUppercaseHeaders whether the header will be in uppercase or not.
   *                                if true, you must order the items so that each chunk will have its items start with either the lowercase or uppercase letter
   */
  public StringArrayAlphabetIndexer(String[] items,boolean useOnlyUppercaseHeaders)
    {
    super(createSectionsFromStrings(items,useOnlyUppercaseHeaders));
    }

  private static SimpleSection[] createSectionsFromStrings(String[] items,boolean useOnlyUppercaseHeaders)
    {
    //get all of the headers of the sections and their sections-items:
    Map<String,ArrayList<String>> headerToSectionItemsMap=new HashMap<String,ArrayList<String>>();
    Set<String> alphabetSet=new TreeSet<String>();
    for(String item : items)
      {
      String firstLetter=TextUtils.isEmpty(item)?" ":useOnlyUppercaseHeaders?item.substring(0,1).toUpperCase(Locale.getDefault()):
          item.substring(0,1);
      ArrayList<String> sectionItems=headerToSectionItemsMap.get(firstLetter);
      if(sectionItems==null)
        headerToSectionItemsMap.put(firstLetter,sectionItems=new ArrayList<String>());
      sectionItems.add(item);
      alphabetSet.add(firstLetter);
      }
    //prepare the sections, and also sort each section's items :
    SimpleSection[] sections=new SimpleSection[alphabetSet.size()];
    int i=0;
    for(String headerTitle : alphabetSet)
      {
      ArrayList<String> sectionItems=headerToSectionItemsMap.get(headerTitle);
      SimpleSection simpleSection=new AlphaBetSection(sectionItems);
      simpleSection.setName(headerTitle);
      sections[i++]=simpleSection;
      }
    return sections;
    }

  public static class AlphaBetSection extends SimpleSection
    {
    private ArrayList<String> items;

    private AlphaBetSection(ArrayList<String> items)
      {
      this.items=items;
      }

    @Override
    public int getItemsCount()
      {
      return items.size();
      }

    @Override
    public String getItem(int posInSection)
      {
      return items.get(posInSection);
      }

    }


  }

SectionedSectionIndexer

public class SectionedSectionIndexer implements SectionIndexer {
    private final SimpleSection[] mSectionArray;

    public SectionedSectionIndexer(final SimpleSection[] sections) {
        mSectionArray = sections;
        //
        int previousIndex = 0;
        for (int i = 0; i < mSectionArray.length; ++i) {
            mSectionArray[i].startIndex = previousIndex;
            previousIndex += mSectionArray[i].getItemsCount();
            mSectionArray[i].endIndex = previousIndex - 1;
        }
    }

    @Override
    public int getPositionForSection(final int section) {
        final int result = section < 0 || section >= mSectionArray.length ? -1 : mSectionArray[section].startIndex;
        return result;
    }

    /** given a flat position, returns the position within the section */
    public int getPositionInSection(final int flatPos) {
        final int sectionForPosition = getSectionForPosition(flatPos);
        final SimpleSection simpleSection = mSectionArray[sectionForPosition];
        return flatPos - simpleSection.startIndex;
    }

    @Override
    public int getSectionForPosition(final int flatPos) {
        if (flatPos < 0)
            return -1;
        int start = 0, end = mSectionArray.length - 1;
        int piv = (start + end) / 2;
        while (true) {
            final SimpleSection section = mSectionArray[piv];
            if (flatPos >= section.startIndex && flatPos <= section.endIndex)
                return piv;
            if (piv == start && start == end)
                return -1;
            if (flatPos < section.startIndex)
                end = piv - 1;
            else
                start = piv + 1;
            piv = (start + end) / 2;
        }
    }

    @Override
    public SimpleSection[] getSections() {
        return mSectionArray;
    }

    public Object getItem(final int flatPos) {
        final int sectionIndex = getSectionForPosition(flatPos);
        final SimpleSection section = mSectionArray[sectionIndex];
        final Object result = section.getItem(flatPos - section.startIndex);
        return result;
    }

    public Object getItem(final int sectionIndex, final int positionInSection) {
        final SimpleSection section = mSectionArray[sectionIndex];
        final Object result = section.getItem(positionInSection);
        return result;
    }

    public int getRawPosition(final int sectionIndex, final int positionInSection) {
        final SimpleSection section = mSectionArray[sectionIndex];
        return section.startIndex + positionInSection;
    }

    public int getItemsCount() {
        if (mSectionArray.length == 0)
            return 0;
        return mSectionArray[mSectionArray.length - 1].endIndex + 1;
    }

    // /////////////////////////////////////////////
    // Section //
    // //////////
    public static abstract class SimpleSection {
        private String name;
        private int startIndex, endIndex;

        public SimpleSection() {
        }

        public SimpleSection(final String sectionName) {
            this.name = sectionName;
        }

        public String getName() {
            return name;
        }

        public void setName(final String name) {
            this.name = name;
        }

        public abstract int getItemsCount();

        public abstract Object getItem(int posInSection);

  @Override
  public String toString()
    {
    return name;
    }
  }

}

BasePinnedHeaderListViewAdapter

public abstract class BasePinnedHeaderListViewAdapter extends BaseAdapter implements SectionIndexer, OnScrollListener,
    PinnedHeaderListView.PinnedHeaderAdapter
  {
    private SectionIndexer _sectionIndexer;
    private boolean mHeaderViewVisible = true;

    public void setSectionIndexer(final SectionIndexer sectionIndexer) {
        _sectionIndexer = sectionIndexer;
    }

    /** remember to call bindSectionHeader(v,position); before calling return */
    @Override
    public abstract View getView(final int position, final View convertView, final ViewGroup parent);

    public abstract CharSequence getSectionTitle(int sectionIndex);

    protected void bindSectionHeader(final TextView headerView, final View dividerView, final int position) {
        final int sectionIndex = getSectionForPosition(position);
        if (getPositionForSection(sectionIndex) == position) {
            final CharSequence title = getSectionTitle(sectionIndex);
            headerView.setText(title);
            headerView.setVisibility(View.VISIBLE);
            if (dividerView != null)
                dividerView.setVisibility(View.GONE);
        } else {
            headerView.setVisibility(View.GONE);
            if (dividerView != null)
                dividerView.setVisibility(View.VISIBLE);
        }
        // move the divider for the last item in a section
        if (dividerView != null)
            if (getPositionForSection(sectionIndex + 1) - 1 == position)
                dividerView.setVisibility(View.GONE);
            else
                dividerView.setVisibility(View.VISIBLE);
        if (!mHeaderViewVisible)
            headerView.setVisibility(View.GONE);
    }

    @Override
    public int getPinnedHeaderState(final int position) {
        if (_sectionIndexer == null || getCount() == 0 || !mHeaderViewVisible)
            return PINNED_HEADER_GONE;
        if (position < 0)
            return PINNED_HEADER_GONE;
        // The header should get pushed up if the top item shown
        // is the last item in a section for a particular letter.
        final int section = getSectionForPosition(position);
        final int nextSectionPosition = getPositionForSection(section + 1);
        if (nextSectionPosition != -1 && position == nextSectionPosition - 1)
            return PINNED_HEADER_PUSHED_UP;
        return PINNED_HEADER_VISIBLE;
    }

    public void setHeaderViewVisible(final boolean isHeaderViewVisible) {
        mHeaderViewVisible = isHeaderViewVisible;
    }

    public boolean isHeaderViewVisible() {
        return this.mHeaderViewVisible;
    }

    @Override
    public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
            final int totalItemCount) {
        ((PinnedHeaderListView) view).configureHeaderView(firstVisibleItem);
    }

    @Override
    public void onScrollStateChanged(final AbsListView arg0, final int arg1) {
    }

    @Override
    public int getPositionForSection(final int sectionIndex) {
        if (_sectionIndexer == null)
            return -1;
        return _sectionIndexer.getPositionForSection(sectionIndex);
    }

    @Override
    public int getSectionForPosition(final int position) {
        if (_sectionIndexer == null)
            return -1;
        return _sectionIndexer.getSectionForPosition(position);
    }

    @Override
    public Object[] getSections() {
        if (_sectionIndexer == null)
            return new String[] { " " };
        return _sectionIndexer.getSections();
    }

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

IndexedPinnedHeaderListViewAdapter

public abstract class IndexedPinnedHeaderListViewAdapter extends BasePinnedHeaderListViewAdapter
  {
  private int _pinnedHeaderBackgroundColor;
  private int _pinnedHeaderTextColor;

  public void setPinnedHeaderBackgroundColor(final int pinnedHeaderBackgroundColor)
    {
    _pinnedHeaderBackgroundColor=pinnedHeaderBackgroundColor;
    }

  public void setPinnedHeaderTextColor(final int pinnedHeaderTextColor)
    {
    _pinnedHeaderTextColor=pinnedHeaderTextColor;
    }

  @Override
  public CharSequence getSectionTitle(final int sectionIndex)
    {
    return getSections()[sectionIndex].toString();
    }

  @Override
  public void configurePinnedHeader(final View v,final int position,final int alpha)
    {
    final TextView header=(TextView)v;
    final int sectionIndex=getSectionForPosition(position);
    final Object[] sections=getSections();
    if(sections!=null&&sections.length!=0)
      {
      final CharSequence title=getSectionTitle(sectionIndex);
      header.setText(title);
      }
    if(VERSION.SDK_INT<VERSION_CODES.HONEYCOMB)
      if(alpha==255)
        {
        header.setBackgroundColor(_pinnedHeaderBackgroundColor);
        header.setTextColor(_pinnedHeaderTextColor);
        }
      else
        {
        header.setBackgroundColor(Color.argb(alpha,Color.red(_pinnedHeaderBackgroundColor),
            Color.green(_pinnedHeaderBackgroundColor),Color.blue(_pinnedHeaderBackgroundColor)));
        header.setTextColor(Color.argb(alpha,Color.red(_pinnedHeaderTextColor),
            Color.green(_pinnedHeaderTextColor),Color.blue(_pinnedHeaderTextColor)));
        }
    else
      {
      header.setBackgroundColor(_pinnedHeaderBackgroundColor);
      header.setTextColor(_pinnedHeaderTextColor);
      header.setAlpha(alpha/255.0f);
      }
    }

  }