Проблемы с прокруткой и заголовками 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 с использованием быстрой прокрутки или как вы без него.
Код
Количество кода очень большое, плюс я сделал 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&§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);
}
}
}