Как предотвратить перемещение дочерней ячейки RecyclerView во время прокрутки пользователя

У меня очень длинные ячейки на моем горизонтальном RecyclerView, и я хочу, чтобы у них был заголовок, который оставался неподвижным, когда пользователь прокручивал по горизонтали.

- Recycler View (A)
-   -   Cell (parent) (B)
-   -   -   Header (C) <-- We want that to be still
-   -   -   Content (D)

Вот как это выглядит визуально:

Таким образом, я ищу способ либо:

1) Не позволяйте заголовку (C) менять положение, пока пользователь проводит пальцем по RecyclerView (A)

or

2) Прокрутите ячейку (B), как обычно, но измените положение ее дочернего элемента (C) в противоположном направлении, чтобы заголовок не двигался, хотя он движется (в направлении, противоположном родителю (B).

Вот что я пытаюсь построить:

Любые идеи?

p.s 1: я заметил много ответов SO, предлагаю использовать ItemDecoration, но все возможные ответы имеют код для реализаций VERTICAL, которые сильно отличаются от реализаций HORIZONTAL.

p.s. 2 Я создаю весь контент для просмотра программно, поэтому не буду использовать файлы макета. (Это потому, что контент будет представлять собой реагирующие представления, и я не могу создать их с файлами макета).

p.s 3: Я также заметил, что ItemDecoration — это старая тактика, а более поздние сторонние библиотеки расширяют LayoutManager.

Пожалуйста, пролейте немного света, спасибо.


person SudoPlz    schedule 26.03.2018    source источник
comment
Я использую этот ответ как решение stackoverflow.com/a/44327350/4643073 Отлично работает!   -  person Tuby    schedule 26.03.2018
comment
Это выглядит многообещающе, спасибо, чувак. Не могли бы вы подробнее рассказать о том, как я могу использовать это в своей горизонтальной реализации? Я обновил свой ответ, чтобы предоставить больше информации. Спасибо @Tuby   -  person SudoPlz    schedule 10.04.2018
comment
Добавлено как ответ   -  person Tuby    schedule 10.04.2018


Ответы (4)


Хотя можно оставить вид заголовка внутри RecyclerView и сделать его статическим, я предлагаю альтернативный подход.

Заголовок может по-прежнему отображаться внутри RecyclerView, но отображение будет вынесено наружу в верхнюю часть RecyclerView следующим образом:

- Title (C) <-- We want that to be still
- Recycler View (A)
-   -   Cell (parent) (B)
-   -   -   Content

RecyclerView.OnScrollListener будет прослушивать появление новых элементов и соответствующим образом изменять заголовок. Таким образом, по мере появления новых элементов заголовок, который является TextView, будет отображать новый заголовок. Это демонстрирует следующее.

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

(Это простая реализация для демонстрационных целей. Полное приложение будет отображать изображения пород собак и какое-то осмысленное описание.)

Вот код, который реализует этот эффект:

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private LinearLayoutManager mLayoutManager;
    private RecyclerViewAdapter mAdapter;
    private TextView mBreedNameTitle;
    private int mLastBreedTitlePosition = RecyclerView.NO_POSITION;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        List<String> breedList = createBreedList();

        // This is where the breed title is displayed.
        mBreedNameTitle = findViewById(R.id.breedNameTitle);

        // Set up the RecyclerView.
        mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
        RecyclerView recyclerView = findViewById(R.id.recyclerView);
        mAdapter = new RecyclerViewAdapter(breedList);
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setAdapter(mAdapter);

        // Add the OnScrollListener so we know when to change the breed title.
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int lastVisible = mLayoutManager.findLastVisibleItemPosition();
                if (lastVisible == RecyclerView.NO_POSITION) {
                    return;
                }
                if (lastVisible != mLastBreedTitlePosition) {
                    mBreedNameTitle.setText(mAdapter.getItems().get(lastVisible));
                    mLastBreedTitlePosition = lastVisible;
                }
            }
        });
    }

    private List<String> createBreedList() {
        List<String> breedList = new ArrayList<>();
        breedList.add("Affenpinscher");
        breedList.add("Afghan Hound");
        breedList.add("Airedale Terrier");
        breedList.add("Akita");
        breedList.add("Alaskan Malamute");
        breedList.add("American Cocker Spaniel");
        breedList.add("American Eskimo Dog (Miniature)");
        breedList.add("American Eskimo Dog (Standard)");
        breedList.add("American Eskimo Dog (Toy)");
        breedList.add("American Foxhound");
        breedList.add("American Staffordshire Terrier");
        breedList.add("American Eskimo Dog (Standard)");
        return breedList;
    }

    @SuppressWarnings("unused")
    private final static String TAG = "MainActivity";
}

class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final List<String> mItems;

    RecyclerViewAdapter(List<String> items) {
        mItems = items;
    }

    @Override
    @NonNull
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;

        view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new RecyclerViewAdapter.ItemViewHolder(view);
    }


    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        RecyclerViewAdapter.ItemViewHolder vh = (RecyclerViewAdapter.ItemViewHolder) holder;

        vh.mBreedImage.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.no_image));
        vh.mBreedName = mItems.get(position);
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    public List<String> getItems() {
        return mItems;
    }

    static class ItemViewHolder extends RecyclerView.ViewHolder {
        private ImageView mBreedImage;
        private String mBreedName;

        ItemViewHolder(View itemView) {
            super(itemView);
            mBreedImage = itemView.findViewById(R.id.breedImage);
        }
    }
}

activity_main.xml

<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/activity_horizontal_margin"
    android:orientation="vertical">

    <TextView
        android:id="@+id/breedNameTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:fontFamily="sans-serif"
        android:textColor="@android:color/black"
        android:textSize="16sp"
        tools:text="Breed name" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />
</LinearLayout>

item_layout.xml

<android.support.constraint.ConstraintLayout 
    android:id="@+id/cont_item_root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white">

    <ImageView
        android:id="@+id/breedImage"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:contentDescription="Dog breed image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="1.0"
        tools:ignore="HardcodedText" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:text="@string/large_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/breedImage"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Обновление. Вот еще один подход, который устанавливает левый отступ TextView, чтобы сделать заголовок липким. Отрицательное x-смещение TextView используется в качестве заполнения для заголовка, чтобы заставить его скользить вправо в пределах TextView и прикрепляться к левой стороне экрана.

Вот результат:

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

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private LinearLayoutManager mLayoutManager;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        List<String> breedList = createBreedList();

        // Set up the RecyclerView.
        mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
        RecyclerView recyclerView = findViewById(R.id.recyclerView);
        RecyclerViewAdapter adapter = new RecyclerViewAdapter(breedList);
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setAdapter(adapter);

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                // Pad the left of the breed name so it stays aligned with the left side of the display.
                int firstVisible = mLayoutManager.findFirstVisibleItemPosition();
                View firstView = mLayoutManager.findViewByPosition(firstVisible);
                firstView.findViewById(R.id.itemBreedName).setPadding((int) -firstView.getX(), 0, 0, 0);

                // Make sure the other breed name has zero padding because we may have changed it.
                int lastVisible = mLayoutManager.findLastVisibleItemPosition();
                View lastView = mLayoutManager.findViewByPosition(lastVisible);
                lastView.findViewById(R.id.itemBreedName).setPadding(0, 0, 0, 0);
            }
        });
    }

    private List<String> createBreedList() {
        List<String> breedList = new ArrayList<>();
        breedList.add("Affenpinscher");
        breedList.add("Afghan Hound");
        breedList.add("Airedale Terrier");
        breedList.add("Akita");
        breedList.add("Alaskan Malamute");
        breedList.add("American Cocker Spaniel");
        breedList.add("American Eskimo Dog (Miniature)");
        breedList.add("American Eskimo Dog (Standard)");
        breedList.add("American Eskimo Dog (Toy)");
        breedList.add("American Foxhound");
        breedList.add("American Staffordshire Terrier");
        breedList.add("American Eskimo Dog (Standard)");
        return breedList;
    }

    @SuppressWarnings("unused")
    private final static String TAG = "MainActivity";

}

class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final List<String> mItems;

    RecyclerViewAdapter(List<String> items) {
        mItems = items;
    }

    @Override
    @NonNull
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;

        view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new RecyclerViewAdapter.ItemViewHolder(view);
    }


    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        RecyclerViewAdapter.ItemViewHolder vh = (RecyclerViewAdapter.ItemViewHolder) holder;

        vh.mBreedImage.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.no_image));
        vh.mBreedName.setPadding(0, 0, 0, 0);
        vh.mBreedName.setText(mItems.get(position));
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    static class ItemViewHolder extends RecyclerView.ViewHolder {
        private ImageView mBreedImage;
        private TextView mBreedName;

        ItemViewHolder(View itemView) {
            super(itemView);
            mBreedImage = itemView.findViewById(R.id.breedImage);
            mBreedName = itemView.findViewById(R.id.itemBreedName);
        }
    }
}

activity_main.xml

<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/activity_horizontal_margin"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />
</LinearLayout>

item_layout.xml

<android.support.constraint.ConstraintLayout 
    android:id="@+id/cont_item_root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white">

    <TextView
        android:id="@+id/itemBreedName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:ellipsize="none"
        android:fontFamily="sans-serif"
        android:singleLine="true"
        android:textColor="@android:color/black"
        android:textSize="16sp"
        tools:text="Breed name" />

    <ImageView
        android:id="@+id/breedImage"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:contentDescription="Dog breed image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/itemBreedName"
        app:layout_constraintVertical_bias="1.0"
        tools:ignore="HardcodedText" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:text="@string/large_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/breedImage"
        app:layout_constraintTop_toBottomOf="@+id/itemBreedName" />

</android.support.constraint.ConstraintLayout>
person Cheticamp    schedule 09.04.2018
comment
Это прекрасно, я ценю время, которое вы потратили, чтобы написать это. На самом деле эта мысль пришла мне в голову, но я действительно хочу, чтобы представление заголовка выскакивало вместе с содержимым, когда пользователь прокручивает следующий/предыдущий элемент. Любые идеи? - person SudoPlz; 09.04.2018
comment
@SudoPlz Не совсем уверен, что вы хотите сделать, но я бы поставил горизонтальный RecyclerView поверх RecyclerView, который у вас есть, и заполнил его названиями, которые вы хотите. Каждый элемент верхнего RecyclerView будет шириной экрана. Когда новый элемент появляется справа внизу RecyclerView, я прокручиваю верхнюю часть, чтобы ее конец оставался выровненным с началом нового представления, которое скользит. Это можно сделать в предоставленном мной прослушивателе прокрутки. Вы захотите запретить пользователю сдвигать верхнюю часть RecyclerView с помощью касания и просто перемещать ее программно. - person Cheticamp; 09.04.2018
comment
Я обновил свой ответ, включив в него фотографии, чтобы было понятнее, не могли бы вы взглянуть? Кроме того, когда дело доходит до добавления второго RecyclerView поверх существующего RecyclerView, недостатком является то, что он не сможет прокручивать по горизонтали с точно такой же скоростью, с которой пользователь проводит пальцем, поскольку у нас нет точного контроля над прокрутить изменить координаты, не так ли? - person SudoPlz; 10.04.2018
comment
@SudoPlz Я вижу, что ты пытаешься сделать. Я думаю, что вы можете синхронизировать два RecyclerView. В Интернете есть обсуждение того, как сделать именно это. Вот один из примеров. Однако ваш пример выглядит не как вид ресайклера, а как карта, скользящая по другой карте. Я думаю, что вам нужно посмотреть на RecyclerView украшения предметов и липкие заголовки. Вот одна из реализаций, которую вы можете адаптировать к своей ситуации. - person Cheticamp; 10.04.2018
comment
Ты, мой друг, легенда, это именно то, что я ищу. Теперь я изучу ваш код более подробно. Кстати, есть ли недостатки в том, чтобы делать это таким образом вместо использования декоратора ItemDecoration? - person SudoPlz; 10.04.2018
comment
@SudoPlz Я думаю, что путь заполнения - это путь. Если у вас нет сжатых сроков, вы можете изучить подход к декорированию. Это более сложно, в то время как подход заполнения легче понять. - person Cheticamp; 10.04.2018
comment
Является ли способ ItemDecoration более производительным по сравнению с подходом заполнения? - person SudoPlz; 10.04.2018
comment
@SudoPlz Не уверен, что лучше для производительности. Я предполагаю, что метод заполнения более эффективен, но я не думаю, что это действительно имеет значение. - person Cheticamp; 10.04.2018

Надеюсь, что эта библиотека поможет: TableView

<com.evrencoskun.tableview.TableView
    android:id="@+id/content_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"

    app:column_header_height="@dimen/column_header_height"
    app:row_header_width="@dimen/row_header_width"
    app:selected_color="@color/selected_background_color"
    app:shadow_color="@color/shadow_background_color"
    app:unselected_color="@color/unselected_background_color" />
person Rajesh    schedule 12.04.2018

Я использую этот ответ как решение stackoverflow.com/a/44327350/4643073 Отлично работает!

Если вам нужен горизонтальный липкий заголовок, просто измените все, что связано с «вертикальностью», измените getY() на getX(), getTop() на getRight(), getHeight() на getWidth().

Как вы думаете, почему ItemDecoration это старая тактика? Он не устарел, он не мешает вашему адаптеру расширять какой-то конкретный класс, он работает хорошо.

person Tuby    schedule 10.04.2018
comment
Хорошо, если я использую отдельный тип представления для моего Header и другой тип представления для моего Content, тогда Header не будет иметь того же x с моим содержимым (они не будут перекрываться). В вертикальном списке это не проблема, но если вы посмотрите на изображение выше, мне нужно, чтобы заголовок был прямо над содержимым. Имеет ли это смысл? - person SudoPlz; 10.04.2018
comment
Это слишком много, чтобы попросить пример? - person SudoPlz; 11.04.2018
comment
Ну, вы можете определить свой собственный липкий макет заголовка внутри примера, который я связал, я использовал то же самое, что и в моем ViewHolder, но это не обязательно. Посмотрите на метод getHeaderLayout() внутри интерфейса - person Tuby; 11.04.2018

В итоге я сделал следующее (благодаря вдохновению Четикампа):

- Helper Header (C) <-- We now have an extra title view
- Recycler View (A)
-   -   Cell (parent) (B)
-   -   -   Header (C) <-- Plus the typical titles within our cells
-   -   -   Content

Как вы видете:

  • Теперь у нас есть вспомогательное представление заголовка, которое находится за пределами нашего RecyclerView.
  • Представления Header, которые находятся в нашем RecyclerView, продолжают двигаться, но прямо над ними мы размещаем вспомогательное представление.

Вот реальный код, чтобы увидеть, что происходит:

Открытый класс CalendarView расширяет LinearLayout { protected LinearLayoutManager mLayoutManager; защищенный HeaderView helperHeaderView; защищенный RecyclerView recyclerView;

public CalendarView(final ReactContext context) {
    super(context);


    setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    setOrientation(LinearLayout.VERTICAL);

    helperHeaderView = new HeaderView(context);
    addView(helperHeaderView);




    final DailyViewAdapter adapter = new DailyViewAdapter(context) {
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            super.onBindViewHolder(holder, position);

            // if our header is not assinged any position yet (we haven't given it any data yet)
            if (helperHeaderView.getLastPosition() == null) {
                updateHeaderData(helperHeaderView, globals.getInitialPosition()); // hydrate it
            }
        }
    };

    recyclerView = new SPRecyclerView(context) {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            if (mLayoutManager == null) {
                mLayoutManager = (LinearLayoutManager) getLayoutManager();
            }

            // the width of any header
            int headerWidth = helperHeaderView.getWidth();

            // get the position of the first visible header in the recyclerview
            int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition();

            // get a ref of the Cell that contains that header
            DayView firstView = (DayView) mLayoutManager.findViewByPosition(firstVisiblePos);

            // get the X coordinate of the first visible header
            float firstViewX = firstView.getX();



            // get the position of the last visible header in the recyclerview
            int lastVisiblePos = mLayoutManager.findLastVisibleItemPosition();

            // get a ref of the Cell that contains that header
            DayView lastView = (DayView) mLayoutManager.findViewByPosition(lastVisiblePos);

            // get the X coordinate of the last visible header
            float lastViewX = lastView.getX();


            // if the first visible position is not the one our header is set to
            if (helperHeaderView.getLastPosition() != firstVisiblePos) {
                // update the header data
                adapter.updateHeaderData(helperHeaderView, firstVisiblePos);
            }

            // if the first visible is not also the last visible (happens when there's only one Cell on screen)
            if (firstVisiblePos == lastVisiblePos) {
                // reset the X coordinates
                helperHeaderView.setX(0);
            } else { // else if there are more than one cells on screen
                // set the X of the helper header, to whatever the last visible header X was, minus the width of the header
                helperHeaderView.setX(lastViewX - headerWidth);
            }

        }
    };


    // ...
  • Все, что осталось сделать сейчас, превратить родительский макет в RelativeLayout, чтобы фактические представления перекрывались (вспомогательный вид заголовка идет прямо над видом переработчика).

  • Также вы можете поэкспериментировать с установкой альфа-канала вспомогательного представления на ноль, когда это необходимо, чтобы убедиться, что он выглядит хорошо.

Я надеюсь, что это поможет кому-то в будущем.

person SudoPlz    schedule 13.04.2018