Проблемы с прокруткой и заголовками 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. есть списки предметов на английском и иврите
  4. установите listView для отображения быстрой прокрутки.
  5. прокрутка listView с использованием быстрой прокрутки или как вы без него.

Код

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

РЕДАКТИРОВАТЬ: большой код POC доступен на Github, здесь.

"PinnedHeaderActivity.java"

Я добавил 2 еврейских предмета вверху, в поле "имена":

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

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

    listView.setFastScrollEnabled(true);

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

    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 имеет проблемы с этим типом пользовательского интерфейса?

РЕДАКТИРОВАТЬ: Кажется, что даже пример Google не обрабатывает элементы RTL на простом ListView:

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

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

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

  }
Другие вопросы по тегам