# BottomSlide **Repository Path**: bianchengyouzi/BottomSlide ## Basic Information - **Project Name**: BottomSlide - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-05-28 - **Last Updated**: 2025-04-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # BottomSlide 使用BottomSheetBehavior实现仿美团拖拽效果 > 前几天看到一片文章,文章的标题是[Android 仿美团拖拽效果](https://www.jianshu.com/p/92180b45aaf7),抱着好奇心去看了下,效果确实不错,但实现过程较为复杂。用原生的CoordinatorLayout+BottomSheetBehavior可以快速的实现这一效果,所以心血来潮打算水一篇文章。
demo地址:[https://github.com/weibindev/BottomSlide](https://github.com/weibindev/BottomSlide) `注:`本文的所有图文素材均来自[https://www.jianshu.com/p/92180b45aaf7](https://www.jianshu.com/p/92180b45aaf7) 先来看下成品效果: ![效果图.gif](https://github.com/weibindev/BottomSlide/blob/master/screenshot/8518082-48e41f9026b4db59.gif) #### 实现思路分析: 1. 界面上可以分为两个部分:顶部部分包括一系列的图片和按钮控件;底部部分是用`NestedScrollView`包裹实现的界面,并且指定 `app:layout_behavior="@string/bottom_sheet_behavior"`。界面根布局采用`CoordinatorLayout`,与`BottomSheetBehavior`包装底部部分的布局实现拖拽。 2. 当界面初始化时,`BottomSheetBehavior`以淡入的方式平滑至设定的最小高度。在`BottomSheetBehavior`拖拽过程中,通过代码改变View的`layoutParams`属性使其达到所能拖拽的最大高度。 3. 除去底部部分初始化淡入的过程,其余时间顶部部分都会发生色差值和视图偏移的变化。 #### 界面布局: `activity_main.xml` ```XML ``` `include_main_top.xml` ```XML ``` `include_main_content.xml` ```XML ``` 在` 0) { statusBarHeight = resources.getDimensionPixelSize(resourceId) } //返回按钮至屏幕顶部的高度 marginTop = imageView.height + lp.topMargin + lp.bottomMargin / 2 + statusBarHeight //返回按钮至根布局的距离 offsetDistance = lp.topMargin } ``` 获取到`marginTop`后在`BottomSheetBehavior.BottomSheetCallback()`回调监听有底部工作的事件。 `BottomSheetBehavior.BottomSheetCallback()`有两个事件 ```Java //在拖动时调用 void onSlide (View bottomSheet, float slideOffset) //在改变状态时调用 void onStateChanged (View bottomSheet, int newState) ``` 只要底部进行拖拽,其状态就会发生变化,所以在`onStateChanged (View bottomSheet, int newState)`做处理 ```Kotlin behavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { val layoutParams = bottomSheet.layoutParams //如果控件本身的Height值就小于返回按钮的高度,就不用做处理 if (bottomSheet.height > heightPixels - marginTop) { //屏幕高度减去marinTop作为控件的Height layoutParams.height = heightPixels - marginTop bottomSheet.layoutParams = layoutParams } } override fun onSlide(bottomSheet: View, slideOffset: Float) { } }) ``` 相应的顶部色差值和偏移值的变化在另一个回调事件`void onSlide (View bottomSheet, float slideOffset) `中处理。 ```Kotlin behavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { } override fun onSlide(bottomSheet: View, slideOffset: Float) { var distance: Float = 0F; /** * slideOffset为底部的新偏移量,值在[-1,1]范围内。当BottomSheetBehavior处于折叠(STATE_COLLAPSED)和 * 展开(STATE_EXPANDED)状态之间时,它的值始终在[0,1]范围内,向上移动趋近于1,向下区间于0。[-1,0]处于 * 隐藏状态(STATE_HIDDEN)和折叠状态(STATE_COLLAPSED)之间。 */ //这里的BottomSheetBehavior初始化完成后,界面设置始终可见,所以不用考虑[-1,0]区间 //色差值变化->其实是遮罩的透明度变化,拖拽至最高,顶部成半透明色 maskView.alpha = slideOffset //offsetDistance是initSystem()中获得的,是返回按钮至根布局的距离 distance = offsetDistance * slideOffset //当BottomSheetBehavior由隐藏状态变为折叠状态(即gif图开始的由底部滑出至设置的最小高度) //slide在[-1,0]的区间内,不加判断会出现顶部布局向下偏移的情况。 if (distance > 0) { constraint.translationY = -distance } } }) ``` 最后还有`BottomSheetBehavior`的滑出效果:先设置`BottomSheetBehavior`的状态为隐藏,然后调用`Handler`的`postDelayed()`方法设置状态为折叠以及最小高度,当然再加一个属性动画,起到锦上添花的作用。 附上`MainActivity.kt`的全部代码 ```Kotlin /** * @author vico * @date 2019/1/23 * email: 1005078384@qq.com */ class MainActivity : AppCompatActivity() { private var heightPixels: Int = 0 private var peekHeight: Int = 0 private var marginTop: Int = 0 private var offsetDistance: Int = 0 private lateinit var mHandler: Handler companion object { const val TAG = "MainActivity.class" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //初始屏幕相关的参数 initSystem() initView() initBehavior() } private fun initBehavior() { val behavior = BottomSheetBehavior.from(nestedScrollView) behavior.isHideable = true behavior.state = BottomSheetBehavior.STATE_HIDDEN behavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { val layoutParams = bottomSheet.layoutParams //如果控件本身的Height值就小于返回按钮的高度,就不用做处理 if (bottomSheet.height > heightPixels - marginTop) { //屏幕高度减去marinTop作为控件的Height layoutParams.height = heightPixels - marginTop bottomSheet.layoutParams = layoutParams } } override fun onSlide(bottomSheet: View, slideOffset: Float) { var distance: Float = 0F; /** * slideOffset为底部的新偏移量,值在[-1,1]范围内。当BottomSheetBehavior处于折叠(STATE_COLLAPSED)和 * 展开(STATE_EXPANDED)状态之间时,它的值始终在[0,1]范围内,向上移动趋近于1,向下区间于0。[-1,0]处于 * 隐藏状态(STATE_HIDDEN)和折叠状态(STATE_COLLAPSED)之间。 */ //这里的BottomSheetBehavior初始化完成后,界面设置始终可见,所以不用考虑[-1,0]区间 //色差值变化->其实是遮罩的透明度变化,拖拽至最高,顶部成半透明色 maskView.alpha = slideOffset //offsetDistance是initSystem()中获得的,是返回按钮至根布局的距离 distance = offsetDistance * slideOffset //当BottomSheetBehavior由隐藏状态变为折叠状态(即gif图开始的由底部滑出至设置的最小高度) //slide在[-1,0]的区间内,不加判断会出现顶部布局向下偏移的情况。 if (distance > 0) { constraint.translationY = -distance } Log.i( TAG, String.format( "slideOffset -->>> %s bottomSheet.getHeight() -->>> %s heightPixels -->>> %s", slideOffset, bottomSheet.height, heightPixels ) ) Log.i(TAG, String.format("distance -->>> %s", distance)) } }) mHandler.postDelayed({ behavior.isHideable = false behavior.state = BottomSheetBehavior.STATE_COLLAPSED behavior.peekHeight = peekHeight ObjectAnimator.ofFloat(nestedScrollView, "alpha", 0f, 1f).setDuration(500).start() }, 200) } private fun initView() { tabLayout.tabMode = TabLayout.MODE_SCROLLABLE tabLayout.addTab(tabLayout.newTab().setText("费用说明")) tabLayout.addTab(tabLayout.newTab().setText("预定须知")) tabLayout.addTab(tabLayout.newTab().setText("退款政策")) tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab) { } override fun onTabUnselected(tab: TabLayout.Tab) { } override fun onTabSelected(tab: TabLayout.Tab) { when (tab.position) { 0 -> frameLayout.setBackgroundColor(Color.parseColor("#ff0000")) 1 -> frameLayout.setBackgroundColor(Color.parseColor("#0000ff")) 2 -> frameLayout.setBackgroundColor(Color.parseColor("#00ff00")) } } }) imageView.setOnClickListener { finish() } imageView2.setOnClickListener { Toast.makeText(this, "转发", Toast.LENGTH_SHORT).show() } imageView3.setOnClickListener { Toast.makeText(this, "收藏", Toast.LENGTH_SHORT).show() } mHandler = Handler() } private fun initSystem() { //获取屏幕高度 heightPixels = resources.displayMetrics.heightPixels Log.i(TAG, "heightPixels: $heightPixels") val behaviorHeight = DensityUtils.px2dp(this, (heightPixels / 2).toFloat()) peekHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, behaviorHeight, resources.displayMetrics).toInt() Log.i(TAG, "peekHeight: $peekHeight") imageView.post { val lp = imageView.layoutParams as ConstraintLayout.LayoutParams //获取状态栏高度 var statusBarHeight = 0 val resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android") if (resourceId > 0) { statusBarHeight = resources.getDimensionPixelSize(resourceId) } //返回按钮至屏幕顶部的高度 marginTop = imageView.height + lp.topMargin + lp.bottomMargin / 2 + statusBarHeight //返回按钮至根布局的距离 offsetDistance = lp.topMargin } } } ``` #### 最后: 文章demo地址:[https://github.com/weibindev/BottomSlide](https://github.com/weibindev/BottomSlide) demo引用素材: [Android 仿美团拖拽效果](https://www.jianshu.com/p/92180b45aaf7) 参考文章: [Material Design系列-严振杰](https://blog.csdn.net/yanzhenjie1003/article/details/51946749)