android - PinnedHeaderListView 滚动和标题问题

标签 android android-listview right-to-left pinned-header-list-view

背景

我试图模仿 Lollipop 的联系人应用程序显示联系人首字母的固定标题的方式,正如我所写的 here .

问题

由于原始代码(位于“PinnedHeaderListViewSample”文件夹中的 here 中)没有显示除英文以外的字母,因此我不得不稍微更改代码,但这还不够。标题本身也是如此,它现在必须在左侧,而不是在行上方。

一切正常,直到我在 RTL 语言(在我的情况下是希伯来语)上对其进行了测试,而设备的语言环境也更改为 RTL 语言(在我的情况下是希伯来语)。

出于某种原因,滚动和标题本身都变得非常奇怪,奇怪的是它发生在某些设备/版本的 Android 上。

例如,在带有 Kitkat 的 Galaxy S3 上,滚动和滚动条完全错误(我滚动到顶部,但滚动条的位置在中间)。

在装有 Android 4.2.2 的 LG G2 上,它也有这个问题,但它也没有显示标题(固定标题除外),尤其是希伯来语中的标题。

在 Galaxy S4 和 Huwawei Ascend P7(都运行 Kitkat)上,无论我做什么,一切正常。

简而言之,特殊情况是:

  • 使用 pinnedHeaderListView
  • 让设备使用 RTL 语言环境,或通过开发人员设置
  • 进行设置
  • 有英语和希伯来语的 ListView 项目
  • 设置 listView 以显示快速滚动条。
  • 使用快速滚动器滚动 listView 或像没有它一样滚动。

  • 编码

    代码量非常大,加上我做了 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 - 不显示希伯来语标题,并且在底部附近有奇怪的滚动(与其余滚动相比,滚动到底部非常快):

    enter image description here

    Galaxy S4 kitkat - 显示标题很好,但底部的滚动很奇怪:

    enter image description here
    出于某种原因,尽管我在开发人员选项中选择了 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&&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);
          }
        }
    
      }
    

    关于android - PinnedHeaderListView 滚动和标题问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27676367/

    相关文章:

    带有希伯来语(从右到左)文本右对齐的Android edittext

    ios - 以编程方式更改 Xib 方向的最佳方法

    java - 玩家丢失时如何启动 "new Game.class"?

    java - 短信应用程序 android 获取线程更新

    java - 提供 track drawable 来切换会弄乱外观

    android - 在对话框的 ListView 中获取数据时出错?

    android - 在什么情况下适配器不会更新附加 View

    android - android :supportsRtl in Androidmanifest 警告

    java - 从 IntentService 更新 ui 的最佳方式?

    java - 如何摇动 ViewPager 以向用户显示有更多页面可用?