Как имитировать липкие элементы listView, как в приложении контактов Lollipop?

Фон

Google недавно опубликовал новую версию Android, в которой есть контактное приложение, которое выглядит следующим образом:

введите здесь описание изображения

Эта проблема

Мне нужно имитировать такой список, но я не могу понять, как это сделать хорошо.

Он состоит из 3 основных компонентов:

  1. липкие заголовки, которые остаются до тех пор, пока у верхнего контакта есть имя, начинающееся с этой буквы.

  2. Круглые фотографии или кружок+буква (для контактов, у которых нет фотографий)

  3. PagerTitleStrip, который имеет одинаковый интервал между элементами и пытается показать их все на экране (или разрешить прокрутку, если это необходимо).

Что я нашел

  1. Существует множество сторонних библиотек, но они обрабатывают липкие заголовки, которые являются частью элементов самого listView. У них не заголовок слева, а сверху элементов.

    Здесь левая сторона движется иначе, чем правая. Он может залипать, и если нет необходимости залипать (потому что в разделе один пункт, например) он прокручивается.

  2. Я заметил, что для круглых фотографий я могу использовать новый (?) класс с именем RoundedBitmapDrawableFactory (который создает RoundedBitmapDrawable ). Это, вероятно, позволит мне красиво поместить фотографию в круглую форму. Однако это не сработает для писем (используется для контактов, у которых нет фотографии), но я думаю, что могу поместить textView сверху и установить цвет фона.

    Кроме того, я заметил, что для того, чтобы правильно использовать RoundedBitmapDrawable (чтобы сделать его действительно круглым), я должен предоставить ему растровое изображение квадратного размера. В противном случае он будет иметь странную форму.

  3. Я пытался использовать setTextSpacing< /strong>, чтобы минимизировать пространство между элементами, но, похоже, это не работает. Я также не смог найти способ стилизовать/настроить PagerTitleStrip так, чтобы он был похож на приложение для контактов.

    Я также пытался использовать PagerTabStrip< /a> , но это тоже не помогло.

Вопрос

Как вы можете имитировать способ, которым Google реализовал этот экран?

Более конкретно:

  1. Как сделать так, чтобы левая сторона вела себя как в приложении для контактов?

  2. Это лучший способ реализовать круговое фото? Мне действительно нужно обрезать растровое изображение в квадрат, прежде чем использовать специальный drawable? Существуют ли какие-либо рекомендации по дизайну, какие цвета использовать? Есть ли более официальный способ использования ячейки кругового текста?

  3. Как вы стилизуете PagerTitleStrip, чтобы он выглядел так же, как в приложении для контактов?


Гитхаб проект

РЕДАКТИРОВАТЬ: для № 1 и № 2 я сделал проект на Github, здесь< /а>. К сожалению, я также обнаружил важную ошибку, поэтому я также опубликовал о ней здесь .


person android developer    schedule 23.12.2014    source источник
comment
Я также должен разработать первую часть вашего вопроса в своем приложении. Может быть, вы можете взглянуть на класс PinnedHeaderListView - android.googlesource.com/platform/packages/apps/ContactsCommon/   -  person androidGuy    schedule 24.12.2014
comment
@androidGuy Нашел решение всех этих проблем, хотя ни одна из них не является официальной...   -  person android developer    schedule 25.12.2014
comment
Для будущих читателей вопроса: я недавно разветвил репозиторий липких заголовков для Android и сделал его липким боковым индексом, как указано выше. См. sticky-alphabet-index на GitHub для кода/реализации.   -  person james    schedule 29.01.2018


Ответы (5)


хорошо, мне удалось решить все проблемы, о которых я писал:

1. Я изменил способ работы сторонней библиотеки (не помню, откуда взял библиотеку, но этот очень похож) , изменив макет каждой строки, чтобы заголовок был слева от содержимого сам. Это всего лишь XML-файл макета, и все готово. Возможно, я опубликую хорошую библиотеку для обоих этих решений.

2. Это вид, который я сделал. Это не официальная реализация (не нашел), поэтому я сделал что-то сам. Это может быть более эффективным, но, по крайней мере, его довольно легко понять, а также достаточно гибко:

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. Я нашел хорошую библиотеку, которая это делает, под названием PagerSlidingTabStrip. Однако не нашел официального способа стилизовать родной.

Другой способ — посмотреть образец Google, который доступен прямо в Android-Studio и называется «SlidingTabLayout». Он показывает, как это делается.

РЕДАКТИРОВАТЬ: лучшей библиотекой для № 3 является здесь, также называемая "PagerSlidingTabStrip".

person android developer    schedule 25.12.2014

Вы можете сделать следующее:

  1. В самой левой части вашего RecyclerView создается TextView, который будет содержать буквенный индекс;
  2. В верхней части представления Recycler (в макете, который его обертывает) поместите TextView, чтобы покрыть тот, который вы создали на шаге 1, он будет липким;
  3. Добавьте OnScrollListener в свой RecyclerView. В методе onScrolled() установите TextView, созданный на шаге 2, для ссылочного текста, взятого из firstVisibleRow. До сих пор у вас будет липкий индекс без эффектов перехода;
  4. Чтобы добавить эффект постепенного появления/исчезновения, разработайте логику, которая проверяет, является ли элемент, предшествующий currentFirstVisibleItem, последним в списке предыдущих букв, или является ли secondVisibleItem первым в новой букве. Основываясь на этой информации, сделайте липкий индекс видимым/невидимым, а индекс строки противоположным, добавив к последнему альфа-эффект.

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

Я разработал компонент, который выполняет описанную выше логику, его можно найти здесь: https://github.com/edsilfer/sticky-index

person E. Fernandes    schedule 06.06.2015
comment
В вашем демонстрационном приложении много ошибок, исправьте их. - person Khalid ElSayed; 30.12.2015
comment
Вот это работает, можете ли вы явно сказать, какие ошибки вы нашли - person E. Fernandes; 30.12.2015

Я отвечаю так поздно, но надеюсь, что мой ответ поможет кому-то. У меня тоже была такая задача. Я искал ответы и примеры липкого заголовка, но для recyclerView. Лучшее и простое решение я нашел в статье "Sticky Header For RecyclerView " Сэйбер Солооки.

введите здесь описание изображения

На основе этого примера я сделал свой контактный модуль для своего приложения, он очень простой.

введите здесь описание изображения

person ABDUAHAD    schedule 10.10.2019

Что ж, исходный код приложения — это всегда хорошее место для начала.

person Aleks Nine    schedule 23.12.2014
comment
Это очень приятно, но это действительно то, что требует много времени, чего, к сожалению, у меня нет. Вы смотрели на код там раньше? Не могли бы вы указать мне точные места, где реализована каждая из вещей, которые я написал? - person android developer; 23.12.2014
comment
Я не уверен, что это помогает. Я не вижу ничего из того, что я там написал. :( - person android developer; 23.12.2014
comment
Ну, внутри у вас есть bindSectionHeaderAndDivider, и если вы подниметесь на один уровень вверх по иерархии до ContactListAdapter, вы найдете рассматриваемый метод. Остальное зависит от тебя. - person Aleks Nine; 23.12.2014
comment
Я думаю, что нашел решение относительно самого listView. Теперь все, что осталось, это PagerTitleStrip - person android developer; 24.12.2014

Чтобы добиться чего-то похожего на список контактов телефона, это является ЛУЧШИМ решением. наткнулся!

Это также будет работать с специальным списком, так как изображения должны быть там с именем. В приведенных выше решениях есть только список или массив строк, которые могут понадобиться не всем!

person Kishan Solanki    schedule 05.09.2020
comment
Ошибка: github.com/timusus/RecyclerView-FastScroll/issues/97 - person android developer; 05.09.2020