Тень элемента внутри ViewPager2 обрезается - PullRequest
1 голос
/ 23 апреля 2020

У меня есть активность с NavHostFragment. Этот NavHostFragment будет содержать три фрагмента, два из которых FragmentA и FragmentB. Внутри FragmentB у меня есть ViewPager2, у которого есть две страницы: PageA и PageB, обе фактически построены из одного фрагмента, FragmentC. Внутри каждого PageA и PageB у меня есть один RecyclerView.

Вот схема XML для FragmentB.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/keyline_4"
        android:clipChildren="false"
        android:clipToPadding="false"
        tools:context=".ui.NavigationActivity">

        ...

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/frag_course_view_pager"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="@dimen/keyline_4"
            android:clipChildren="false"
            ... />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/frag_course_tablayout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/keyline_4"
            ... />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Как видите, я установить root макета фрагмента clipChildren в false. Я также установил ViewPager2 clipChildren на false. Вот макет XML для PageA и PageB (т.е. FragmentC).

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewmodel"
            type="com.mobile.tugasakhir.viewmodels.course.CourseTabViewModel" />
    </data>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/frag_course_tab_rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/offset_bottom_nav_bar_padding"
        android:clipChildren="false"
        android:clipToPadding="false"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</layout>

Как вы можете видеть, он имеет только RecyclerView, и я установил clipChildren в false. Надутая компоновка XML для ViewHolder RV выглядит следующим образом:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        ...
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/component_course_container"
        android:layout_width="match_parent"
        android:layout_height="@dimen/course_card_height"
        android:background="@drawable/drawable_rounded_rect"
        android:backgroundTint="?attr/colorSurface"
        android:elevation="@dimen/elevation_0">
        ...

        ...

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Отсеченная часть является тенью от elevation вышеупомянутой ViewHolder XML компоновки. Поскольку я установил все атрибуты clipChildren от всех родителей на false, тень не должна была обрезаться, но она все еще остается. Почему это происходит? Как я могу предотвратить обрезку без изменения отступа / поля ?

Примечание: У меня также есть RecyclerView внутри FragmentA, но разница является то, что RecyclerView внутри FragmentA не является вложенным в ViewPager2. Следование методам (установка clipChildren всех родителей на false) на FragmentA позволяет элементам RecyclerView показывать их тени.

Вот изображение проблемы.

Problem


Обновление

Использование инспектора макета, похоже, внутри ViewPager2, есть больше ViewGroups (отмечены красным прямоугольник). Мой RecyclerView с обрезанными элементами отмечен зеленым прямоугольником. Вот что показывает Инспектор макетов.

Layout Inspector

Как видно, внутри ViewPager2 есть ViewPager2$RecyclerViewImpl, а внутри него есть FrameLayout (я не создавал эти группы просмотра). Оказывается, для этих двух clipChildren установлено значение true, даже если для ViewPager2 'clipChildren установлено значение false. Я могу нацелиться на ViewPager2$RecyclerViewImpl внутри моего FragmentB следующим образом.

(viewPager.getChildAt(0) as ViewGroup).clipChildren = false

Затем я попытался нацелиться на FrameLayout, используя аналогичный метод.

((viewPager.getChildAt(0) as ViewGroup).getChildAt(0) as ViewGroup).clipChildren = false

Однако я получил ошибка о том, что второй getChildAt(0) возвращает null. В моем инспекторе макетов это ясно показывает, что перед моим RecyclerView есть FrameLayout. Этот FrameLayout имеет clipChildren, установленный в true. Я почти уверен, что мне нужно установить FrameLayout clipChildren на false, чтобы тени не обрезались, но я могу ошибаться.

Вот скриншоты, показывающие, что я удалось установить clipChildren из RecyclerViewImpl в false и не удалось установить clipChildren из FrameLayout в false соответственно.

Success

Failure

Или есть лучший способ отсоединить тени?

Я скрыл предварительный просмотр макета по личным причинам; это приводит к тому, что предварительный просмотр макета становится белым окном.


Обновление 2

Для тех, кто хотел бы просмотреть проблему напрямую, просто запустите приложение, которое я предоставил в это Github ссылка . Используйте Инспектора макетов, чтобы увидеть, что я вижу.

Ответы [ 3 ]

1 голос
/ 06 мая 2020

Рассмотрев вашу проблему более подробно, я считаю, что проблема заключается в ViewHolder XML, использующем Drawable в качестве фона, пытающегося симулировать тени.

Оказывается, что тени на Android не являются "настоящими тенями", а являются фальшивыми нарисованными элементами в рамках. Эти тени используют - в некоторых случаях - дополнительные отступы, добавленные каркасом; снова.

CardViews, имеют внутренний механизм для решения этой проблемы (особенно на платформах, где не было понятия повышения).

По этой причине я рекомендую вам изменить свой ViewHOlder, чтобы он содержал CardView. В этом случае это будет выглядеть следующим образом:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/component_course_container"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:backgroundTint="#efefef">

    <androidx.cardview.widget.CardView
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:cardCornerRadius="8dp"
        app:cardUseCompatPadding="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:text="Your Content Goes Here"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>

А затем ... удалите все clipChildren / CliptoPadding, et c. Это вызовет у вас больше головной боли, чем решений, и приведет к снижению производительности (теперь фреймворку нужно рисовать дополнительные элементы, которые иначе не были бы отрисованы, потому что они покрыты).

Надеюсь, это поможет вам.

Что касается FrameLayout, это «вещь» вице-президента, и я надеюсь, что Google заменит это новым FragmentContainer в последних реализациях AppCompat.

Удачи.

Вот как это выглядит сейчас:

enter image description here

1 голос
/ 07 мая 2020

Этот ответ напрямую объясняет, как мы можем нацелить clipChildren автоматически сгенерированной проблемы c FrameLayout на false; таким образом расстегивая тени предметов. Если вы не возражаете против изменения вашего текущего XML макета, посмотрите @ столь же потрясающий ответ Мартина Маркончини (используя CardView, чтобы нарисовать тень, которая не обрезается), чтобы также решить эту проблему.


Как и предполагалось, упомянутое FrameLayout является причиной того, что детей обрезают. FrameLayout создается классом FragmentStateAdapter, который расширяет класс RecyclerView.Adapter<FragmentViewHolder>. Из того, что я понял, этот FragmentViewHolder в основном работает аналогично обычному держателю вида RecyclerView. Это FragmentViewHolder s itemView, на данный момент всегда возвращает FrameLayout (что является ViewGroup). Тот факт, что держатель представления всегда будет возвращать ViewGroup, вряд ли изменится даже в будущем.


Зависимость androidx.viewpager2:viewpager2:1.0.0

Если вы используете зависимость выше, вы можете видеть, как FrameLayout создается внутри функции onCreateViewHolder класса FragmentStateAdapter (строка 160).

@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return FragmentViewHolder.create(parent);
}

Метод FragmentViewHolder.create(parent) всегда создает держатель вида FrameLayout. Это будет передано в качестве параметра (holder) в onBindViewHolder из FragmentStateAdapter. Объявление метода для FragmentViewHolder.create выглядит следующим образом.

@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
    FrameLayout container = new FrameLayout(parent.getContext());
    container.setLayoutParams(
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
    container.setId(ViewCompat.generateViewId());
    container.setSaveEnabled(false);
    return new FragmentViewHolder(container);
}

Установка атрибута clipChildren в значение false

Как мы теперь знаем, что holder будет передано как В методе onBindViewHolder метода FragmentStateAdapter мы можем переопределить onBindViewHolder (внутри вашего собственного класса адаптера, который расширяет FragmentStateAdapter), установить атрибут clipChildren holder.itemView в false и вуаля, элемент внутри RecyclerView больше не будет обрезаться.

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

class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    override fun getItemCount(): Int = 1

    override fun createFragment(position: Int): Fragment {
        return PageFragment().apply {
            arguments = Bundle()
        }
    }

    // Setting its clipChildren to false
    override fun onBindViewHolder(
        holder: FragmentViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        (holder.itemView as ViewGroup).clipChildren = false
        super.onBindViewHolder(holder, position, payloads)
    }
}

Другие родительские макеты выше иерархии, которые имеют ограничивающие рамки меньше чем необходимая область для тени, необходимо также установить clipChildren на false (а именно, родительский макет RecyclerViewImpl, автоматически сгенерированный ViewPager2 [как упомянуто в вопросе]).

0 голосов
/ 06 мая 2020

добавьте clipToPadding="false" к root ConstraintLayout в файле xml, накачанном для ViewHolder.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...