Как имитировать элементы списка ListView, как в приложении контактов Lollipop?
Фон
Google недавно опубликовал новую версию Android, в которой есть приложение для контактов, которое выглядит следующим образом:
Эта проблема
Мне нужно подражать такому списку, но я не могу понять, как это сделать хорошо.
Он состоит из 3 основных компонентов:
заголовки, которые остаются до тех пор, пока у верхнего контакта есть имя, начинающееся с этой буквы.
Круглые фотографии или круг + буква (для контактов, у которых нет фотографий)
PagerTitleStrip, который имеет равномерный интервал между элементами и пытается показать их все на экране (или разрешить прокрутку при необходимости).
Что я нашел
Существует множество сторонних библиотек, но они обрабатывают заголовки stickey, которые являются частью элементов самого listView. У них нет названия слева, но в верхней части пунктов.
Здесь левая сторона движется не так, как правая. Он может "залипать", а если нет необходимости (например, в разделе есть один элемент), он прокручивается.
Я заметил, что для круговых фотографий я могу использовать новый (?) Класс с именем " RoundedBitmapDrawableFactory" (который создает " RoundedBitmapDrawable"). Это, вероятно, позволит мне красиво обвести фотографию в круглую форму. Однако, это не будет работать для писем (используется для контактов, у которых нет фотографии), но я думаю, что я могу поместить textView сверху и установить цвет фона.
Кроме того, я заметил, что для того, чтобы хорошо использовать "RoundedBitmapDrawable" (чтобы сделать его действительно круглым), я должен предоставить ему растровое изображение квадратного размера. В противном случае он будет иметь странную форму.
Я пытался использовать " setTextSpacing", чтобы минимизировать расстояние между элементами, но, похоже, это не работает. Я также не мог найти способ стилизовать / настроить PagerTitleStrip, чтобы он был таким же, как в приложении контактов.
Я также пытался использовать " PagerTabStrip", но это также не помогло.
Вопрос
Как вы можете имитировать способ, которым Google реализовал этот экран?
Более конкретно:
Как сделать так, чтобы левая сторона вела себя как в приложении контактов?
Это лучший способ реализовать круговую фотографию? Мне действительно нужно обрезать растровое изображение в квадрат, прежде чем использовать специальное рисование? Существуют ли рекомендации по дизайну, какие цвета использовать? Есть ли более официальный способ использовать ячейку с круговым текстом?
Как сделать так, чтобы стиль PagerTitleStrip был таким же, как в приложении контактов?
Проект Github
РЕДАКТИРОВАТЬ: для № 1 и № 2, я сделал проект на Github, здесь. К сожалению, я также нашел важную ошибку, поэтому я также опубликовал об этом здесь.
3 ответа
Хорошо, мне удалось решить все вопросы, о которых я написал:
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".
Вы можете сделать следующее:
- На самой левой стороне вашего RecyclerView создает TextView, который будет содержать буквенный индекс;
- В верхней части представления Recycler (в макете, который его обертывает) поместите TextView, чтобы покрыть тот, который вы создали на шаге 1, это будет липкий;
- Добавьте OnScrollListener в свой RecyclerView. В методе onScrolled () установите TextView, созданный на шаге 2, для справочного текста, взятого из firstVisibleRow. До тех пор, пока здесь у вас не будет индекса, без последствий перехода;
Чтобы добавить эффект перехода постепенного появления / исчезновения, разработайте логику, которая проверяет, является ли предыдущий элемент элемента 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
Отвечаю так поздно, но надеюсь, что мой ответ кому-то поможет. У меня тоже была такая задача. Я искал ответы и примеры липкого заголовка, но для recyclerView. Лучшее и простое решение я нашел в статье Sabre Solooki "Sticky Header For RecyclerView".
На основе этого примера я сделал свой контактный модуль для своего приложения, он очень простой.
Ну, исходный код приложения всегда хорошее место для начала
Чтобы добиться чего-то похожего на список контактов телефона, это ЛУЧШЕЕ решение, с которым я столкнулся!
Это также будет работать с настраиваемым списком, так как изображения могут быть там с именем. Вышеупомянутые решения имеют только список или массив String, который может понадобиться не всем!