Because the effect of IOS version and Android version is not consistent, I saw the Android implementation is a little more complex, so I first implemented the IOS way, the effect of the general function effect are all there, the details are not polished, after all, just demo. In order to explore the sliding processing.

Same old rule. Let’s see what it looks like.

Do not know after seeing the effect, you still have not seen the interest of 😄😄😄

Implementation approach

Upon seeing this folding effect, my first feeling is to use BottomSheetBehavior, and of course I also use this one this time.

For BottomSheetBehavior, I intend to write a separate article and explain the Behavior under use and customization related. 😄 mainly has not studied too deeply at present, said too much easy exposure o(╥﹏╥) O


      
<org.fireking.laboratory.qingtingfm.detail_ios.widget.BottomSheetRootFrameLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.fireking.laboratory.qingtingfm.detail_ios.QtDetailsIosActivity">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:id="@+id/flAudioInfo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/sp_audio_bg"
        android:orientation="vertical">

        <View
            android:id="@+id/vStatusBar"
            android:layout_width="match_parent"
            android:layout_height="25dp" />

        <org.fireking.laboratory.qingtingfm.detail_ios.widget.QtFmAppbar
            android:id="@+id/fmAppbar"
            android:layout_width="match_parent"
            android:layout_height="44dp" />

        <org.fireking.laboratory.qingtingfm.detail_ios.widget.QtFmContent
            android:id="@+id/qtFmContent"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </androidx.appcompat.widget.LinearLayoutCompat>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:visibility="gone">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/bottomSheetLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/sp_round_12"
            android:orientation="vertical"
            app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

            <net.lucode.hackware.magicindicator.MagicIndicator
                android:id="@+id/magicIndicator"
                android:layout_width="match_parent"
                android:layout_height="50dp" />

            <View
                android:layout_width="match_parent"
                android:layout_height="0.7 dp"
                android:background="#EEEEEE" />

            <androidx.viewpager.widget.ViewPager
                android:id="@+id/vPager"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:nestedScrollingEnabled="false" />
        </androidx.appcompat.widget.LinearLayoutCompat>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</org.fireking.laboratory.qingtingfm.detail_ios.widget.BottomSheetRootFrameLayout>
Copy the code

Main do is used to calculate in BottomSheetRootFrameLayout BottomSheetBehavior largest open by default state should show how many distance, the distance can’t write dead, because need to be calculated according to the content of the request to the dynamic, So the onMeasure is overwritten to dynamically calculate an actual height based on content changes and set it to the peekHeight method of the BottomSheetBehavior.

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
     super.onMeasure(widthMeasureSpec, heightMeasureSpec) bottomSheetLayout? .also {val newBottomSheetHeight =
             (measuredHeight - dip2px(44F) - StatusBarUtil.getStatusBarHeight(context)).toInt()
         if(bottomSheetHeight ! = newBottomSheetHeight) { it.layoutParams.height = newBottomSheetHeight bottomSheetHeight = newBottomSheetHeight }valnewPeekHeight = (measuredHeight - qtFmContent!! .measuredHeight - dip2px(44F) - StatusBarUtil.getStatusBarHeight(
                 context
             )).toInt()
         if(currentPeekHeight ! = newPeekHeight && ! isFinal) {val mBottomSheetBehavior = BottomSheetBehavior.from(it)
             mBottomSheetBehavior.peekHeight = newPeekHeight
             currentPeekHeight = newPeekHeight
             // After setting the height, perform a secondary calculation to ensure that the layout is synchronized
             super.onMeasure(widthMeasureSpec, heightMeasureSpec)
             isFinal = true}}}Copy the code

After completing the calculation Settings above, you will find that the BottomSheetBehavior can be used normally without filling SmartRefreshLayout and RecyclerView, and basically meets the folding requirements, and the folding display height meets the actual requirements.

The show hide control for the title bar can be handled directly with ‘BottomSheetBehavior#onStateChanged

val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout)
   bottomSheetBehavior.setBottomSheetCallback(object :
       BottomSheetBehavior.BottomSheetCallback() {
       override fun onStateChanged(bottomSheet: View, newState: Int) {
           if (newState == BottomSheetBehavior.STATE_EXPANDED) {
               fmAppbar.showTitle()
           } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
               fmAppbar.hideTitle()
           }
       }

       override fun onSlide(bottomSheet: View, slideOffset: Float){}})Copy the code

At this point, BottomBehavior is also linked to the title bar, well, it looks like there’s an animation for the title bar change. Arrange ~


      
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:id="@+id/ctlTransitionPanel"
    android:layout_width="match_parent"
    android:layout_height="44dp"
    android:animateLayoutChanges="true"
    tools:background="#CC297F87">

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/ivBackIcon"
        android:layout_width="28dp"
        android:layout_height="28dp"
        android:layout_marginStart="16dp"
        android:src="@drawable/ic_back_white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.LinearLayoutCompat
        android:id="@+id/llAppbarTransitionContainer"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:visibility="gone"
        app:layout_constraintEnd_toStartOf="@+id/ivWhiteWechat"
        app:layout_constraintStart_toEndOf="@+id/ivBackIcon">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tvTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:text="Romance of the Sui and Tang Dynasties"
            android:textColor="@android:color/white"
            android:textSize="18dp" />

        <View
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />

        <org.fireking.basic.textview.widget.ShapeTextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:paddingStart="16dp"
            android:paddingTop="4dp"
            android:paddingEnd="16dp"
            android:paddingBottom="4dp"
            android:text="Collection"
            android:textColor="@android:color/white"
            app:stv_corners="12dp"
            app:stv_solid="#FF2442" />
    </androidx.appcompat.widget.LinearLayoutCompat>

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/ivWhiteWechat"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:layout_marginEnd="16dp"
        android:src="@drawable/ic_wechat_white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/ivWhiteMore"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/ivWhiteMore"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:layout_marginEnd="16dp"
        android:src="@drawable/ic_more_white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

QtFmAppbar set animation effects, here using LayoutTransition to achieve animation, can be searched under the usage, relatively simple, but not more explanation

class QtFmAppbar @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {

    private var viewBinding: QtfmAppbarBinding? = null

    init {
        viewBinding = QtfmAppbarBinding.inflate(LayoutInflater.from(context), this.true)

        val transitionHeight =
            TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 44F, resources.displayMetrics)

        valtransition = LayoutTransition() viewBinding? .apply { ctlTransitionPanel.layoutTransition = transition }val appearingAnimator =
            ObjectAnimator.ofFloat(null."translationY", transitionHeight, 0F)
                .setDuration(transition.getDuration(LayoutTransition.APPEARING))
        transition.setAnimator(LayoutTransition.APPEARING, appearingAnimator)

        val disAppearingAnimator =
            ObjectAnimator.ofFloat(null."translationY".0F. transitionHeight) .setDuration(transition.getDuration(LayoutTransition.APPEARING)) transition.setAnimator(LayoutTransition.DISAPPEARING, disAppearingAnimator) }fun showTitle(a){ viewBinding? .apply { llAppbarTransitionContainer.visibility = View.VISIBLE } }fun hideTitle(a){ viewBinding? .apply { llAppbarTransitionContainer.visibility = View.GONE } } }Copy the code

Now we have the title bar animation as well.

The only thing left to do is to set up two fragments in the Viewpager and place the corresponding list data separately.

Everything is in order. Nice~

But when there are no problems, we find problems.

When SmartRefreshLayout and RecyclerView are set, there is a conflict in the sliding of the BottomSheetBehavior, which cannot slide smoothly. Moreover, when sliding later, only the RecyclerView list below moves again, instead of the whole sliding.

After reading the BottomSheetBehavior source code, we found that it also handles Nestedscrolling, so the problem is clear at a glance. In fact, we don’t need SmartRefreshLayout to handle Nestedscrolling. Just deliver it directly to BottomSheetBehavior and handle it by yourself. The short answer way is to use requestDisallowInterceptTouchEvent = false to handle SmartRefresh the superclass can directly, but looked at, found that inheritance SmartRefreshLayout, intercept is not very convenient for events. The srlEnableNestedScrolling attribute is used to process whether or not to intercept. So just set SmartRefeshLayout app:srlEnableRefresh=”false”.

After a brief attempt, the dilemma was solved. Nice~

Finally, I would like to explain that the layout is folded and expanded when clicked. This is relatively simple, which is achieved by using the state of BottomSheetBehavior.

The BottomSheetBehavior has the following states


  /** The bottom sheet is dragging. */
  public static final int STATE_DRAGGING = 1;

  /** The bottom sheet is settling. */
  public static final int STATE_SETTLING = 2;

  /** The bottom sheet is expanded. */
  public static final int STATE_EXPANDED = 3;

  /** The bottom sheet is collapsed. */
  public static final int STATE_COLLAPSED = 4;

  /** The bottom sheet is hidden. */
  public static final int STATE_HIDDEN = 5;
Copy the code

When we click on it, if we want to fold it, we just set it to hide.

 if (it) {
     bottomSheetBehavior.isHideable = false
     bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
 } else {
     bottomSheetBehavior.isHideable = true
     bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
 }
Copy the code

Here all the explanation is completed, the core code is also written out, I believe that if you look carefully, you can also achieve the corresponding effect.

It’s late at night, goodbye!