PinnedHeaderListView scrolling and header issues

3.9k Views Asked by At

Background

I'm trying to mimic the way that the contacts app of Lollipop shows the pinned headers for the contacts' first letter, as i've written about here.

The problem

Since the original code (which is found here, in the "PinnedHeaderListViewSample" folder) doesn't show letters other than the English ones, I had to change the code a bit, but that wasn't enough. Same goes for the header itself, which had to be on the left now other than being above the rows.

Everything worked fine, till I've tested it on RTL languages (Hebrew in my case), while the device's locale was also changed to an RTL language (Hebrew in my case) .

For some reason, things get really weird in both the scrolling and the header itself, and the weird part is that it occurs on some devices/versions of Android.

For example, on Galaxy S3 with Kitkat, the scrolling and the scrollbar is completely wrong (I scroll to the top, yet the scrollbar's location is on the middle).

On LG G2 with Android 4.2.2, it also has this problem, but it also doesn't show the headers (except for the pinned header), especially not those in Hebrew.

On Galaxy S4 and on Huwawei Ascend P7 (both running Kitkat), everything worked fine no matter what I did.

In short, the special scenario is:

  1. Use a pinnedHeaderListView
  2. have the device using an RTL locale, or do it via the developers settings
  3. have listview items in both English and Hebrew
  4. set the listView to show the fast-scroller.
  5. scroll listView using either the fast-scroller or like you do without it.

The code

The code amount is very large, plus I've made 2 POCs, while one of them is quite different from the code that I've started with (to make it look like on Lollipop). so I'll try to show the minimal amount.

EDIT: the big POC code is available on Github, here .

"PinnedHeaderActivity.java"

I've added 2 Hebrew items to the top, into the "names" field:

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

in "setupListView" method, I've made the fast scrollbar visible:

    listView.setFastScrollEnabled(true);

in "NamesAdapter" CTOR, I've made it support more than the English Alphabet:

    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"

In "getSectionForPosition" method, I've changed it to:

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" />

Here are 2 screenshots, one that doesn't look good, and another that looks ok:

Galaxy S3 kitkat and also LG G2 4.2.2 - don't show Hebrew headers, and have weird scrolling near the bottom (goes very fast to the bottom compared to the rest of the scrolling):

enter image description here

Galaxy S4 kitkat - shows the headers fine, but the scrolling is weird at the bottom:

enter image description here For some reason, the Galaxy S4 didn't mirror the UI as it should, even though I've chosen it on the developer options, so it might also be the reason for why it showed the headers fine.

What I've tried

Besides trying out 2 POCs that I've made (one that is much more similar to the material design style and it's more complex), I've tried various ways to use the layouts, and also tried to use LayoutDirection in order to force the headers to show.

The even harder problem is to solve the fast-scrollbar, which works really weird on the more complex POC and a bit weird on the simple one (which scrolls fast near the bottom).

The question

What is the correct way to solve those issues?

Why does RTL have issues with this type of UI ?

EDIT: It seems that even Google's example doesn't handle RTL items well on a simple ListView:

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

When it has Hebrew contacts, the scroller goes "crazy".

1

There are 1 best solutions below

0
On BEST ANSWER

OK, I have no idea what Google did there since the code is very un-readable, so I made my own class, and it works fine.

The only thing that you must remember is to sort the items before sending them to my class, and if you wish to have only uppercase letters for the headers, you must sort the items accordingly (so that all items that start with a specific letter will be in the same chunk, no matter if it's uppercase or not).

The solution is available on GitHub, here: https://github.com/AndroidDeveloperLB/ListViewVariants

Here's the code:

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

  }