闲来无事,看到蜻蜓FM的详情页面效果挺好玩的,于是乎仿一下,因为IOS版本和Android版本的效果不一致,看了下Android实现上更加复杂一点,所以先实现了IOS的方式,效果上大致的功能效果都有了,细节没有打磨,终究只是demo,为了探究滑动处理方式。
老规矩,先上图,看一下效果。
不知道看了效果之后,你还有没有看下去的兴趣😄😄😄
实现思路
一看到这种折叠效果,我第一感觉应该是使用BottomSheetBehavior
,当然我本次使用也是使用的这个。
对于BottomSheetBehavior
我打算的后续会和Behavior
单独写文章说明下使用和自定义相关的。😄主要是目前还没有研究太深,说太多容易暴露出来o(╥﹏╥)o
<?xml version="1.0" encoding="utf-8"?>
<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.7dp"
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>
BottomSheetRootFrameLayout
中主要做的事情就是用来计算BottomSheetBehavior
默认展开状态下最大应该展示多少距离,这个距离是不能写死的,因为需要根据请求到的内容来动态计算,所以这里重写了onMeasure
,来根据内容变化动态计算出来一个实际的高度并设置给BottomSheetBehavior
的peekHeight
方法。
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
}
val newPeekHeight =
(measuredHeight - qtFmContent!!.measuredHeight - dip2px(44F) - StatusBarUtil.getStatusBarHeight(
context
)).toInt()
if (currentPeekHeight != newPeekHeight && !isFinal) {
val mBottomSheetBehavior = BottomSheetBehavior.from(it)
mBottomSheetBehavior.peekHeight = newPeekHeight
currentPeekHeight = newPeekHeight
//设置高度后,进行二次计算,确保布局同步
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
isFinal = true
}
}
}
完成上面的计算设置之后,你会发现BottomSheetBehavior
在没有填充SmartRefreshLayout
和RecyclerView
的情况下,可以正常使用了,且基本满足折叠需求,而且折叠展示的高度都是满足实际需求的。
标题栏的展示隐藏控制,可以直接使用`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) {
}
})
至此BottomBehavior
也和标题栏联动起来了,嗯,好像标题栏变化还有个动画效果。安排
<?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"
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="隋唐演义"
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="收藏"
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>
QtFmAppbar
设置动画效果,这里使用LayoutTransition
实现动画,可自行检索下用法,比较简单,就不过多说明了
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)
val transition = 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() {
viewBinding?.apply {
llAppbarTransitionContainer.visibility = View.VISIBLE
}
}
fun hideTitle() {
viewBinding?.apply {
llAppbarTransitionContainer.visibility = View.GONE
}
}
}
现在标题栏的动画也有了。
剩下来就是Viewpager
里面设置两个Fragment
,分别放置对应的列表数据。
一切都井然有序。Nice~
但是当没有问题的时候,我们发现问题来了。
当设置了SmartRefreshLayout
和RecyclerView
之后,BottomSheetBehavior
的滑动出现了冲突,不能流畅滑动了,而且之后滑动的时候只是下面的RecyclerView
列表再动,并没有使整体滑动。
简单阅读了下BottomSheetBehavior
源码之后,发现他里面也处理了Nestedscrolling
,那么问题就一目了然了,其实我们根本就不需要SmartRefreshLayout
再去处理NestedScrolling
,直接交付给BottomSheetBehavior
一个人去处理就ok了。最简答的方式就是使用requestDisallowInterceptTouchEvent=false
直接让SmartRefresh
父类处理即可,但是看了下,发现继承实现SmartRefreshLayout
的话,对于事件拦截也不是很方便。这个时候发现了一个属性,srlEnableNestedScrolling
,发现使用的地方刚好就是用来处理是否拦截相关的操作。所以,直接将SmartRefeshLayout
的app:srlEnableRefresh="false"
设置即可。
简单尝试了下,困境解决了。Nice~
最后说明一下点击的时候收起和展开布局,这个相对比较简单,就是使用BottomSheetBehavior
的状态实现的。
BottomSheetBehavior
有如下几种状态
/** 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;
只需要我们点击的时候,如果想要折叠,就是设置支持hide即可。
if (it) {
bottomSheetBehavior.isHideable = false
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
} else {
bottomSheetBehavior.isHideable = true
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
到这里所有的讲解都完成了,核心代码也都写出来了,相信如果认真去看的话,你也能实现出来对应的效果。
夜深了,再见!