Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 452
android developer Avatar asked Dec 23 '14 13:12

android developer


2 Answers

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.

like image 195
android developer Avatar answered Oct 29 '22 21:10

android developer


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

like image 39
E. Fernandes Avatar answered Oct 29 '22 19:10

E. Fernandes