背景
我试图模仿 Lollipop 的联系人应用程序显示联系人首字母的固定标题的方式,正如我所写的 here .
问题
由于原始代码(位于“PinnedHeaderListViewSample”文件夹中的 here 中)没有显示除英文以外的字母,因此我不得不稍微更改代码,但这还不够。标题本身也是如此,它现在必须在左侧,而不是在行上方。
一切正常,直到我在 RTL 语言(在我的情况下是希伯来语)上对其进行了测试,而设备的语言环境也更改为 RTL 语言(在我的情况下是希伯来语)。
出于某种原因,滚动和标题本身都变得非常奇怪,奇怪的是它发生在某些设备/版本的 Android 上。
例如,在带有 Kitkat 的 Galaxy S3 上,滚动和滚动条完全错误(我滚动到顶部,但滚动条的位置在中间)。
在装有 Android 4.2.2 的 LG G2 上,它也有这个问题,但它也没有显示标题(固定标题除外),尤其是希伯来语中的标题。
在 Galaxy S4 和 Huwawei Ascend P7(都运行 Kitkat)上,无论我做什么,一切正常。
简而言之,特殊情况是:
编码
代码量非常大,加上我做了 2 个 POC,其中一个与我开始使用的代码有很大不同(使其看起来像在 Lollipop 上)。所以我会尽量显示最小的数量。
编辑:大 POC 代码可在 Github 上获得,here .
"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" />
这是两张截图,一张看起来不太好,另一张看起来还不错:
Galaxy S3 kitkat 和 LG G2 4.2.2 - 不显示希伯来语标题,并且在底部附近有奇怪的滚动(与其余滚动相比,滚动到底部非常快):
Galaxy S4 kitkat - 显示标题很好,但底部的滚动很奇怪:
出于某种原因,尽管我在开发人员选项中选择了 Galaxy S4,但它并没有像应有的那样镜像 UI,所以这也可能是它显示标题正常的原因。
我试过的
除了尝试我制作的 2 个 POC(一个更类似于 Material 设计风格并且更复杂的 POC),我尝试了各种使用布局的方法,还尝试使用 LayoutDirection 来强制要显示的标题。
更难的问题是解决快速滚动条,它在更复杂的 POC 上非常奇怪,在简单的 POC 上有点奇怪(在底部附近快速滚动)。
问题
解决这些问题的正确方法是什么?
为什么 RTL 对这种类型的 UI 有问题?
编辑:似乎即使是 Google 的示例也不能在简单的 ListView 上很好地处理 RTL 项目:
http://developer.android.com/training/contacts-provider/retrieve-names.html
当它有希伯来语联系人时,滚动条会“发疯”。
最佳答案
好的,我不知道谷歌在那里做了什么,因为代码非常不可读,所以我自己做了一个类,它工作正常。
您必须记住的唯一一件事是在将项目发送到我的类(class)之前对其进行排序,如果您希望标题只有大写字母,则必须相应地对项目进行排序(以便所有以特定字母开头的项目将在同一个 block 中,不管它是否大写)。
该解决方案在 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);
}
}
}
关于android - PinnedHeaderListView 滚动和标题问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27676367/