How to mimic the listView stickey-items like on Lollipop's contacts app?
Asked Answered
R

5

17

Background

Google has recently published a new version of Android, which has a contact app that look like this :

enter image description here

The problem

I need to mimic this kind of list, but I can't find out how to do it well.

This consists of 3 main components:

  1. the stickey titles, which stay as long as the top contact has the name that starts with this letter.

  2. The circular photos or circle+letter (for contacts that don't have photos)

  3. the PagerTitleStrip, which has uniform spacing between the items, and tries to show them all on the screen (or allow to scroll if needed).

What I've found

  1. There are plenty of third party libraries, but that handle stickey-headers, which are a part of the items of the listView itself. They don't have the title on the left, but on the top of the items.

    Here, the left side moves in a different way than the right side. It can stick, and if there is no need to stick (because the section has one item, for example) it scrolls.

  2. I've noticed that for the circular photos, I can use the new (?) class called "RoundedBitmapDrawableFactory" (which creates "RoundedBitmapDrawable" ). This will probably allow me to put a photo into a circular shape nicely. However, it won't work for letters (used for contacts that don't have a photo), but I think I can put a textView on top, and set the background to a color.

    Also, I've noticed that in order to use the "RoundedBitmapDrawable" well (to make it truly circular), I must provide it with a square sized bitmap. Otherwise, it will have a weird shape.

  3. I've tried using "setTextSpacing" to minimize the space between the items, but it doesn't seem to work. I also couldn't find any way to style/customize the PagerTitleStrip to be like on the contacts app.

    I've also tried using "PagerTabStrip" , but it also didn't help.

The question

How can you mimic the way that Google has implemented this screen?

More specifically:

  1. How can I make the left side behave like on the contacts app?

  2. Is this the best way to implement the circular photo? Do I really have to crop the bitmap into a square before using the special drawable ? Are there any design guidelines for which colors to use? Is there a more official way to use the circle-text cell?

  3. How do you style the PagerTitleStrip to have the same look&feel as on the contacts app?


Github project

EDIT: for #1 and #2, I've made a project on Github, here. Sadly I've also found an important bug, so I've also published about it here.

Riel answered 23/12, 2014 at 13:32 Comment(3)
I have to develop the first part of your question in my app too. May be you can take a look at the PinnedHeaderListView class - android.googlesource.com/platform/packages/apps/ContactsCommon/…Cogitative
@Cogitative Found the solution for all of those issues, though none of them are quite official...Riel
For future readers of the question, I recently forked a sticky header repo for Android and made it a "sticky side index" as noted above. See sticky-alphabet-index on GitHub for the code/implementation.Frederique
R
7

ok, I've managed to solve all of the issues I've written about:

1.I changed the way that the third party library works (I don't remember where I got the library from, but this one is very similar) , by changing the layout of each row, so that the header would be on the left of the content itself. It's just a matter of a layout XML file and you're pretty much done. Maybe I will publish a nice library for both of those solutions.

2.This is the view I've made. It's not an official implementation (didn't find any), so I made something by myself. It can be more efficient, but at least it's quite easy to understand and also quite flexible:

public class CircularView extends ViewSwitcher {
    private ImageView mImageView;
    private TextView mTextView;
    private Bitmap mBitmap;
    private CharSequence mText;
    private int mBackgroundColor = 0;
    private int mImageResId = 0;

    public CircularView(final Context context) {
        this(context, null);
    }

    public CircularView(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        addView(mImageView = new ImageView(context), new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT, Gravity.CENTER));
        addView(mTextView = new TextView(context), new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT, Gravity.CENTER));
        mTextView.setGravity(Gravity.CENTER);
        if (isInEditMode())
            setTextAndBackgroundColor("", 0xFFff0000);
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int measuredWidth = getMeasuredWidth();
        final int measuredHeight = getMeasuredHeight();
        if (measuredWidth != 0 && measuredHeight != 0)
            drawContent(measuredWidth, measuredHeight);
    }

    @SuppressWarnings("deprecation")
    private void drawContent(final int measuredWidth, final int measuredHeight) {
        ShapeDrawable roundedBackgroundDrawable = null;
        if (mBackgroundColor != 0) {
            roundedBackgroundDrawable = new ShapeDrawable(new OvalShape());
            roundedBackgroundDrawable.getPaint().setColor(mBackgroundColor);
            roundedBackgroundDrawable.setIntrinsicHeight(measuredHeight);
            roundedBackgroundDrawable.setIntrinsicWidth(measuredWidth);
            roundedBackgroundDrawable.setBounds(new Rect(0, 0, measuredWidth, measuredHeight));
        }
        if (mImageResId != 0) {
            mImageView.setBackgroundDrawable(roundedBackgroundDrawable);
            mImageView.setImageResource(mImageResId);
            mImageView.setScaleType(ScaleType.CENTER_INSIDE);
        } else if (mText != null) {
            mTextView.setText(mText);
            mTextView.setBackgroundDrawable(roundedBackgroundDrawable);
            // mTextView.setPadding(0, measuredHeight / 4, 0, measuredHeight / 4);
            mTextView.setTextSize(measuredHeight / 5);
        } else if (mBitmap != null) {
            mImageView.setScaleType(ScaleType.FIT_CENTER);
            mImageView.setBackgroundDrawable(roundedBackgroundDrawable);
            mBitmap = ThumbnailUtils.extractThumbnail(mBitmap, measuredWidth, measuredHeight);
            final RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(getResources(),
                    mBitmap);
            roundedBitmapDrawable.setCornerRadius((measuredHeight + measuredWidth) / 4);
            mImageView.setImageDrawable(roundedBitmapDrawable);
        }
        resetValuesState(false);
    }

    public void setTextAndBackgroundColor(final CharSequence text, final int backgroundColor) {
        resetValuesState(true);
        while (getCurrentView() != mTextView)
            showNext();
        this.mBackgroundColor = backgroundColor;
        mText = text;
        final int height = getHeight(), width = getWidth();
        if (height != 0 && width != 0)
            drawContent(width, height);
    }

    public void setImageResource(final int imageResId, final int backgroundColor) {
        resetValuesState(true);
        while (getCurrentView() != mImageView)
            showNext();
        mImageResId = imageResId;
        this.mBackgroundColor = backgroundColor;
        final int height = getHeight(), width = getWidth();
        if (height != 0 && width != 0)
            drawContent(width, height);
    }

    public void setImageBitmap(final Bitmap bitmap) {
        setImageBitmapAndBackgroundColor(bitmap, 0);
    }

    public void setImageBitmapAndBackgroundColor(final Bitmap bitmap, final int backgroundColor) {
        resetValuesState(true);
        while (getCurrentView() != mImageView)
            showNext();
        this.mBackgroundColor = backgroundColor;
        mBitmap = bitmap;
        final int height = getHeight(), width = getWidth();
        if (height != 0 && width != 0)
            drawContent(width, height);
    }

    private void resetValuesState(final boolean alsoResetViews) {
        mBackgroundColor = mImageResId = 0;
        mBitmap = null;
        mText = null;
        if (alsoResetViews) {
            mTextView.setText(null);
            mTextView.setBackgroundDrawable(null);
            mImageView.setImageBitmap(null);
            mImageView.setBackgroundDrawable(null);
        }
    }

    public ImageView getImageView() {
        return mImageView;
    }

    public TextView getTextView() {
        return mTextView;
    }

}

3.I've found a nice library that does it, called PagerSlidingTabStrip . Didn't find an official way to style the native one, though.

Another way is to look at Google's sample which is available right within Android-Studio, and is called "SlidingTabLayout". It shows how it's done.

EDIT: a better library for #3 is here, called "PagerSlidingTabStrip" too.

Riel answered 25/12, 2014 at 8:23 Comment(0)
T
1

You can do the following:

  1. On the leftmost side of your RecyclerView, creates a TextView that will hold the letter index;
  2. On the top of the Recycler view (in the layout that wrappes it) place a TextView in order to cover the one you created in step 1, this will be the sticky one;
  3. Add a OnScrollListener in your RecyclerView. On method onScrolled (), set the TextView created in step 2 for the reference text taken from firstVisibleRow. Until here you shall have a stiky index, without the effects of transition;
  4. To add the fade in/out transition effect, develop a logic that checks if the item previous of the currentFirstVisibleItem is the last of the previous letter list, or if the secondVisibleItem is the first one of the new letter. Based on these information make the sticky index visible/invisible and the row index the opposed, adding in this last the alpha effect.

       if (recyclerView != null) {
        View firstVisibleView = recyclerView.getChildAt(0);
        View secondVisibleView = recyclerView.getChildAt(1);
    
        TextView firstRowIndex = (TextView) firstVisibleView.findViewById(R.id.sticky_row_index);
        TextView secondRowIndex = (TextView) secondVisibleView.findViewById(R.id.sticky_row_index);
    
        int visibleRange = recyclerView.getChildCount();
        int actual = recyclerView.getChildPosition(firstVisibleView);
        int next = actual + 1;
        int previous = actual - 1;
        int last = actual + visibleRange;
    
        // RESET STICKY LETTER INDEX
        stickyIndex.setText(String.valueOf(getIndexContext(firstRowIndex)).toUpperCase());
        stickyIndex.setVisibility(TextView.VISIBLE);
    
        if (dy > 0) {
            // USER SCROLLING DOWN THE RecyclerView
            if (next <= last) {
                if (isHeader(firstRowIndex, secondRowIndex)) {
                    stickyIndex.setVisibility(TextView.INVISIBLE);
                    firstRowIndex.setVisibility(TextView.VISIBLE);
                    firstRowIndex.setAlpha(1 - (Math.abs(firstVisibleView.getY()) / firstRowIndex.getHeight()));
                    secondRowIndex.setVisibility(TextView.VISIBLE);
                } else {
                    firstRowIndex.setVisibility(TextView.INVISIBLE);
                    stickyIndex.setVisibility(TextView.VISIBLE);
                }
            }
        } else {
            // USER IS SCROLLING UP THE RecyclerVIew
            if (next <= last) {
                // RESET FIRST ROW STATE
                firstRowIndex.setVisibility(TextView.INVISIBLE);
    
                if ((isHeader(firstRowIndex, secondRowIndex) || (getIndexContext(firstRowIndex) != getIndexContext(secondRowIndex))) && isHeader(firstRowIndex, secondRowIndex)) {
                    stickyIndex.setVisibility(TextView.INVISIBLE);
                    firstRowIndex.setVisibility(TextView.VISIBLE);
                    firstRowIndex.setAlpha(1 - (Math.abs(firstVisibleView.getY()) / firstRowIndex.getHeight()));
                    secondRowIndex.setVisibility(TextView.VISIBLE);
                } else {
                    secondRowIndex.setVisibility(TextView.INVISIBLE);
                }
            }
        }
    
        if (stickyIndex.getVisibility() == TextView.VISIBLE) {
            firstRowIndex.setVisibility(TextView.INVISIBLE);
        }
    }
    

I have developed a component that does the above logic, it can be found here: https://github.com/edsilfer/sticky-index

Travesty answered 6/6, 2015 at 5:50 Comment(2)
Your demo app has a lot of bugs please fix itStridor
Here it works, can you explicitly say which errors you have foundTravesty
P
1

I answering so late, but I hope my answer will help to someone. I also had such a task. I have been looking for answers and examples of sticky header, but for recyclerView. I found the best and simplest solution in the article "Sticky Header For RecyclerView" of Saber Solooki.

enter image description here

Based on this example, I made my contact module for my application, it is very simple.

enter image description here

Premillennial answered 10/10, 2019 at 10:41 Comment(0)
P
0

Well the source code of the app is always a good place to start

Propel answered 23/12, 2014 at 15:2 Comment(5)
This is very nice, but it's really something that takes a long time, which is sadly not something I have. Have you looked at the code there before? Can you please point me to the exact places that each of the things I wrote are implemented?Riel
I can point you to the adapter android.googlesource.com/platform/packages/apps/ContactsCommon/…Propel
I'm not sure this helps. I don't see any of what I've written there. :(Riel
Well inside you have bindSectionHeaderAndDivider, and if you go one level up in the hierarhy to ContactListAdapter you will find the method in question. The rest is up to you.Propel
I think I've found a solution regarding the listView itself. Now all that's left is the PagerTitleStripRiel
A
0

To achieve something similar to the contact list of phone, This is the BEST solution I came across!

This will also work with the custom list as Images may need to be there with the name. Above solutions only have the list or array of String which may not everyone need!

Antefix answered 5/9, 2020 at 8:49 Comment(1)
Seems buggy: github.com/timusus/RecyclerView-FastScroll/issues/97Riel

© 2022 - 2024 — McMap. All rights reserved.