根据内容设置 ViewPager2 的高度无法正常工作

Setting the height of the ViewPager2 based on its content is not working properly

我有一个ViewPager2 with TabLayout. I'm trying to set the height of the ViewPager2 dynamically based on its content. I have checked some related questions like and this。实际上,后者对我有帮助,但无法正常工作。当我导航到 DetailsFragment 时,我从网络获取数据如下:

DetailFragmentViewModel

   private val _advert = MutableLiveData<Resource<DetailAdvert>>()
    val advert: LiveData<Resource<DetailAdvert>> get() = _advert
    init {
        savedStateHandle.get<Int>("id")?.let {
            getAdvert(it)      
        }
}

 private fun getAdvert(id: Int) {
        _advert.value = Resource.Loading()
        viewModelScope.launch(Dispatchers.Main) {
            _advert.value = repo.advert(id)
        }
    }

我在 DetailsFragment 中观察 advert LiveData :

DetailsFragment

  mViewModel.advert.observe(viewLifecycleOwner) {
            when (it) {
                is Resource.Loading -> {
                    showProgress(true)
                }

                is Resource.Error -> {
                    showSnack(it.message!!)
                    showProgress(false)
                }
                is Resource.Success -> {
                    it.data?.let { advert ->
                        this.advert = advert
                        bindAdvert(advert)         
                    }
                    showProgress(false)
                }
            }
        }

    private fun bindAdvert(item: DetailAdvert) {
        initTabLayout(fragments, item)
}

当我成功收到数据时,我正在初始化我的 TabLayoutViewPager2,然后将 List 传递给 InfoFragment,这是第一个选项卡和描述文本到第二个选项卡 DescriptionFragment

    private fun initTabLayout(fragments: List<Fragment>, advert: DetailAdvert) {
        viewpagerAdapter =
            DetailsFragmentPagerAdapter(childFragmentManager, lifecycle, fragments, advert, this)
        binding.viewpager.adapter = viewpagerAdapter
        binding.viewpager.isUserInputEnabled = false
        //initializing utility class
        val heightAnimator = ViewPager2ViewHeightAnimator()
        heightAnimator.viewPager2 = binding.viewpager
        tabLayoutMediator = TabLayoutMediator(binding.tabs, binding.viewpager) { tab, position ->
            tab.text = titles[position]
        }
        tabLayoutMediator.attach()
    }

DetailsFragmentPagerAdapter

class DetailsFragmentPagerAdapter(
    fragmentManager: FragmentManager,
    lifecycle: Lifecycle,
    private val fragments: List<Fragment>,
    private val advert: DetailAdvert,
    private val parentFragment: Fragment
) : FragmentStateAdapter(fragmentManager, lifecycle) {

    init {
        registerFragmentTransactionCallback(object :
            FragmentStateAdapter.FragmentTransactionCallback() {
            override fun onFragmentMaxLifecyclePreUpdated(
                fragment: Fragment,
                maxLifecycleState: Lifecycle.State,
            ) = if (maxLifecycleState == Lifecycle.State.RESUMED) {
                OnPostEventListener {
                    fragment.parentFragmentManager.commitNow {
                        setPrimaryNavigationFragment(fragment)
                    }
                }
            } else {
                super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState)
            }
        })
    }

    override fun getItemCount(): Int = fragments.size

    override fun createFragment(position: Int): Fragment {
        val fragment = fragments[position]
        when (fragment) {
            is DescriptionFragment -> {
                fragment.arguments = Bundle().apply {
                    putString(DESCRIPTION_KEY, advert.text)
                }
            }
            is InfoFragment -> {
                fragment.arguments = Bundle().apply {
                    val infoList = getInfoList(advert,parentFragment.requireContext())
                    putParcelableArray(INFO_KEY, infoList.toTypedArray())
                }
            }
        }
        return fragment
    }
}

在第一个选项卡中,我只有 RecyclerView 显示列表项:

fragment_info.xml

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/info_rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:adapter="@{adapter}"
         
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

InfoFragment

@AndroidEntryPoint
class InfoFragment : Fragment(R.layout.fragment_info_layout) {

    private lateinit var binding: FragmentInfoLayoutBinding

    @Inject
    lateinit var mAdapter: InfoFragmentAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding = FragmentInfoLayoutBinding.bind(view)
        val infoList = arguments?.getParcelableArray(Constants.INFO_KEY)!!.toList() as List<Info>
        binding.adapter = mAdapter
        val dividerItemDecoration = DividerItemDecoration(binding.infoRv.context,
            LinearLayoutManager(requireContext()).orientation)
        binding.infoRv.addItemDecoration(dividerItemDecoration)
        mAdapter.submitList(infoList)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding.unbind()
    }
}

在第二个选项卡 DescriptionFragment 中,我有一个 NestedScrollView 和一个 TextViewfragment_description.xml

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

        <androidx.core.widget.NestedScrollView
            android:id="@+id/nsv_detail"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="16dp"
            android:paddingStart="24dp"
            android:paddingEnd="24dp"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:id="@+id/description_tv"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{Jsoup.parse(description).text()}"
                android:lineSpacingExtra="8dp"
                tools:text="@string/lorem" />
        </androidx.core.widget.NestedScrollView>

    </androidx.constraintlayout.widget.ConstraintLayout>

DescriptionFragment

class DescriptionFragment : Fragment(R.layout.fragment_description_layout) {

    private lateinit var binding: FragmentDescriptionLayoutBinding


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding = FragmentDescriptionLayoutBinding.bind(view)
        val description = arguments?.getString(Constants.DESCRIPTION_KEY)!!
        binding.description = description
    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding.unbind()
    }
}

最后是我的布局 DetailsFragment :

   <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/main_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:backgroundTint="@android:color/transparent">

            <com.google.android.material.appbar.CollapsingToolbarLayout
                android:id="@+id/collapsing_toolbar"
                android:layout_width="match_parent"
                android:layout_height="@dimen/view_pager_height"
                app:layout_scrollFlags="scroll|exitUntilCollapsed">

                <androidx.viewpager2.widget.ViewPager2
                    android:id="@+id/advert_images_vp"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:adapter="@{adapter}"
                    android:transitionGroup="true"
                    android:orientation="horizontal"
                    />

                <TextView
                    android:id="@+id/no_image_tv"
                    style="@style/TextAppearance.AppCompat.Medium"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="@string/no_image"
                    android:textColor="@color/gray"
                    android:visibility="invisible" />

            </com.google.android.material.appbar.CollapsingToolbarLayout>


            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tabs"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:layout_gravity="bottom"
                android:background="@color/primaryColor"
                app:tabContentStart="72dp"
                app:tabGravity="fill"
                app:tabIndicatorColor="@color/white"
                app:tabMode="fixed"
                app:tabTextAppearance="@android:style/TextAppearance.Widget.TabWidget"
                app:tabTextColor="@color/white" />

        </com.google.android.material.appbar.AppBarLayout>

        <androidx.core.widget.NestedScrollView
            android:id="@+id/nested_scroll"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <androidx.viewpager2.widget.ViewPager2
                    android:id="@+id/viewpager"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" />
                      
                   //other stuff

        </androidx.core.widget.NestedScrollView>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

正如我之前提到的,我正在使用以下 class 动态更改 ViewPager2 的高度:

ViewPager2ViewHeightAnimator

class ViewPager2ViewHeightAnimator {

    var viewPager2: ViewPager2? = null; set(value) {
        if (field != value) {
            field?.unregisterOnPageChangeCallback(onPageChangeCallback)
            field = value
            value?.registerOnPageChangeCallback(onPageChangeCallback)
        }
    }

    private val layoutManager: LinearLayoutManager? get() = (viewPager2?.getChildAt(0) as? RecyclerView)?.layoutManager as? LinearLayoutManager

    private val onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
            super.onPageScrolled(position, positionOffset, positionOffsetPixels)
            recalculate(position, positionOffset)
        }
    }

    fun recalculate(position: Int, positionOffset: Float = 0f) = layoutManager?.apply {
        val leftView = findViewByPosition(position) ?: return@apply
        val rightView = findViewByPosition(position + 1)
        val setMeasure = {
            viewPager2?.apply {
                val leftHeight = getMeasuredViewHeightFor(leftView)
                layoutParams = layoutParams.apply {
                    height = if (rightView != null) {
                        val rightHeight = getMeasuredViewHeightFor(rightView)
                        leftHeight + ((rightHeight - leftHeight) * positionOffset).toInt()
                    } else {
                        leftHeight
                    }
                }
                invalidate()
            }
        }
        val onLayoutChanged =
            ViewTreeObserver.OnGlobalLayoutListener {
                setMeasure.invoke()
            }
        leftView.viewTreeObserver.addOnGlobalLayoutListener(onLayoutChanged)
        rightView?.viewTreeObserver?.addOnGlobalLayoutListener(onLayoutChanged)
        setMeasure.invoke()
    }

    private fun getMeasuredViewHeightFor(view: View): Int {
        val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY)
        val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        view.measure(wMeasureSpec, hMeasureSpec)
        return view.measuredHeight
    }
}

但结果是:

我也曾尝试修改here中的代码,但结果是:

我认为发生这种情况是因为异步加载数据。我该如何解决?

谢谢。

好的,我不知道发生了什么,但我已经解决了我的问题。这是代码:

ViewPager2HeightAnimator

class ViewPager2HeightAnimator {

    var viewPager2: ViewPager2? = null; set(value) {
            if (field != value) {
                field?.unregisterOnPageChangeCallback(onPageChangeCallback)
                field = value
                value?.registerOnPageChangeCallback(onPageChangeCallback)
            }
        }

    private val layoutManager: LinearLayoutManager? get() = (viewPager2?.getChildAt(0) as? RecyclerView)?.layoutManager as? LinearLayoutManager

    private val onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
        override fun onPageScrolled(
            position: Int,
            positionOffset: Float,
            positionOffsetPixels: Int,
        ) {
            super.onPageScrolled(position, positionOffset, positionOffsetPixels)
            recalculate(position, positionOffset)
        }
    }

    fun recalculate(position: Int, positionOffset: Float = 0f) = layoutManager?.apply {
        val leftView = findViewByPosition(position) ?: return@apply
        val rightView = findViewByPosition(position + 1)
        val setMeasure = {
            viewPager2?.apply {
                val leftHeight = getMeasuredViewHeightFor(leftView)
                layoutParams = layoutParams.apply {
                    height = if (rightView != null) {
                        val rightHeight = getMeasuredViewHeightFor(rightView)
                        leftHeight + ((rightHeight - leftHeight) * positionOffset).toInt()
                    } else {
                        leftHeight
                    }
                }
                invalidate()
            }
        }
        val onLayoutChanged =
            ViewTreeObserver.OnGlobalLayoutListener {
                setMeasure.invoke()
            }
        leftView.viewTreeObserver.addOnGlobalLayoutListener(onLayoutChanged)
        rightView?.viewTreeObserver?.addOnGlobalLayoutListener(onLayoutChanged)
    }

    private fun getMeasuredViewHeightFor(view: View): Int {
        val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY)
        val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        view.measure(wMeasureSpec, hMeasureSpec)
        return view.measuredHeight
    }
}

然后我将 binding.infoRv.isNestedScrollingEnabled = false 添加到第一个选项卡的 OnViewCreated 方法中。虽然它有一些小的性能问题,但它按预期工作。