Проблемы прокрутки и заголовка PinnedHeaderListView
Фон
Я пытаюсь подражать тому, как приложение контактов Lollipop показывает закрепленные заголовки для первой буквы контактов, о чем я уже писалздесь.
Проблема
Поскольку исходный код (который найден здесь, в папке "PinnedHeaderListViewSample") не показывает других букв, кроме английских, мне пришлось немного изменить код, но этого было недостаточно. То же самое касается и самого заголовка, который теперь он должен был быть слева, а не над рядами.
Все работало нормально, пока я не протестировал его на языках RTL (иврит в моем случае), в то время как локаль устройства также была изменена на язык RTL (иврит в моем случае) .
По какой-то причине вещи становятся действительно странными как в прокрутке, так и в самом заголовке, и странная часть заключается в том, что это происходит на некоторых устройствах/версиях Android.
Например, на Galaxy S3 с Kitkat прокрутка и полоса прокрутки являются совершенно неправильно (я прокручиваю до верха, но расположение полосы прокрутки находится в середине).
На LG G2 с Android 4.2.2 у него также есть эта проблема, но он также не показывает заголовки (за исключением закрепленного заголовка), особенно на иврите.
На Galaxy S4 и на Huwawei Ascend P7 (оба работают на Kitkat) все работало отлично, независимо от того, что я делал.
Короче говоря, специальный сценарий таков:- используйте pinnedHeaderListView
- есть устройство используя локаль RTL, или сделать это через настройки разработчиков
- есть элементы listview на английском и иврите
- установите listView для отображения быстрого скроллера.
- прокрутите 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 ответ:
Ладно, я понятия не имею, что там делал 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&§ions.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); } } }