在实际的各类App开发中,经常会需要做一个左侧的侧滑栏,类似于QQ这种。
今天这篇文章总结下自己在开发中遇到的这类可以跟随移动且可以缩放的侧滑栏。
一、实现原理
使用 HorizontalScrollView 实现一个水平方向的可滑动的View,左布局为侧滑栏,右布局为自己的主页内容。
来看下android的官方解释,我用谷歌翻译了:
用户可以滚动的视图层次结构的布局容器,允许其大于物理显示。 HorizontalScrollView 是一种 FrameLayout,这意味着您应该在其中放置一个包含要滚动的全部内容的子级;这个子级本身可能是一个具有复杂对象层次结构的布局管理器。经常使用的子级是水平方向的 LinearLayout,它呈现用户可以滚动的顶级项目的水平数组。
TextView 类还负责自己的滚动,因此不需要 HorizontalScrollView,但将两者结合使用可以在更大的容器中实现文本视图的效果。
HorizontalScrollView 仅支持水平滚动。对于垂直滚动,请使用 ScrollView 或 ListView。
属性
关键点:
1、用户可以滚动的视图层次结构的布局容器,允许其大于物理显示。证明就像我们平时的用到的垂直方向的scrollView嵌套几个列表一样。
2、HorizontalScrollView 是一种 FrameLayout,这意味着您应该在其中放置一个包含要滚动的全部内容的子级;这个代表你需要在HorizontalScrollView先放一个总布局,再在这个布局里放左右布局内容。
二、实现过程
第一步:xml布局
<?xml version="1.0" encoding="utf-8"?>
<wanwan.and.lx.lxslideviewdemo.SlideMenuLayout
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"
android:id="@+id/sliding"
android:background="@mipmap/img_bg"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical"
>
<RelativeLayout
android:id="@+id/sidebarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/sidebar_image_app_icon"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="106dp"
android:scaleType="fitXY"
android:src="@mipmap/ic_launcher" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/slide_item_privacy"
android:layout_width="200dp"
android:layout_height="57dp"
android:layout_below="@id/sidebar_image_app_icon"
android:layout_marginTop="30dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/set_privacy_lock_img"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="32dp"
android:src="@mipmap/privacy"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="13.7dp"
android:text="Privacy Policy"
android:textColor="@color/black"
android:textSize="17sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/set_privacy_lock_img"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/slide_item_share"
android:layout_width="200dp"
android:layout_height="57dp"
android:layout_below="@id/slide_item_privacy"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/set_share_lock_img"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="32dp"
android:src="@mipmap/share"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="13.7dp"
android:text="Share"
android:textColor="@color/black"
android:textSize="17sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/set_share_lock_img"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/slide_item_update"
android:layout_width="200dp"
android:layout_height="57dp"
android:layout_below="@id/slide_item_share"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/set_update_lock_img"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="32dp"
android:src="@mipmap/update"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="13.7dp"
android:text="Update"
android:textColor="@color/black"
android:textSize="17sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/set_update_lock_img"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:background="@color/white"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_centerInParent="true"
android:textStyle="bold"
android:textSize="18sp"
android:textColor="@color/black"
android:layout_height="wrap_content"
android:text="这是主页"/>
<androidx.appcompat.widget.AppCompatImageView
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/set"
android:layout_centerHorizontal="true"
android:layout_below="@id/text"
android:id="@+id/set"/>
</RelativeLayout>
</LinearLayout>
</wanwan.and.lx.lxslideviewdemo.SlideMenuLayout>
1.SlideMenuLayout其实就是HorizontalScrollView,这是个自定义控件,待会儿代码附上。
2.可以看到SlideMenuLayout只有一个子View,为LinearLayout,LinearLayout它是全屏且水平布局,且有两个子布局,分为左右。
第二步:自定义控件HorizontalScrollView
class SlideMenuLayout : HorizontalScrollView {
/**
* 当菜单页显示时,右侧内容页显示宽度
*/
private var menuRightWidth = 0
private lateinit var menuView: View
private lateinit var contentView: View
/**
* 用于处理飞速滑动
*/
private var gestureDetector: GestureDetector
var isMenuOpen: Boolean = false
private var btn: AppCompatImageView? = null
/**
* 是否进行事件拦截
*/
private var isIntercept = false
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
) {
gestureDetector = GestureDetector(getContext(), GestureDetectorListener())
}
//用于处理飞速滑动
inner class GestureDetectorListener : SimpleOnGestureListener() {
override fun onFling(
e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float
): Boolean {
//屏蔽向右滑动
if (e2.x - e1.x > 30) {
Log.e("TAG", "right, right, go go go --->")
return true
}
if (abs(velocityY) > abs(velocityX)) {
return false
}
if (isMenuOpen) {
if (velocityX < 0) {
closeMenu()
return true
}
} else {
if (velocityX > 0) {
openMenu()
return true
}
}
return super.onFling(e1, e2, velocityX, velocityY)
}
}
/**
* 此方法在布局加载完毕时调用
*/
override fun onFinishInflate() {
super.onFinishInflate()
val linearLayout: LinearLayout = getChildAt(0) as LinearLayout
val childCount = linearLayout.childCount
if (childCount != 2) {
throw IllegalArgumentException("LinearLayout child size must be 2!")
}
menuView = linearLayout.getChildAt(0)
val menuLayoutParams = menuView.layoutParams
menuLayoutParams.width = getScreenWidth() - menuRightWidth - SizeUtils.dp2px(100f)
menuView.layoutParams = menuLayoutParams
//设置右侧内容页宽度为屏幕宽度
contentView = linearLayout.getChildAt(1)
linearLayout.removeView(contentView)
val contentRelativeLayout = RelativeLayout(context)
contentRelativeLayout.addView(contentView)
val contentLayoutParams = contentView.layoutParams
contentLayoutParams.width = getScreenWidth()
contentRelativeLayout.layoutParams = contentLayoutParams
linearLayout.addView(contentRelativeLayout)
btn = contentView.findViewById(R.id.set)
btn?.setOnClickListener {
openMenu()
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
//默认情况下,应该全部展示内容页,关闭左侧菜单页
scrollTo(menuView.measuredWidth, 0)
}
/**
* 获取当前屏幕的宽度
*/
private fun getScreenWidth(): Int {
return resources.displayMetrics.widthPixels
}
/**
* 重写该方法,用于处理缩放和透明度效果
*/
override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
super.onScrollChanged(l, t, oldl, oldt)
//滚动的时候,不停的回调 l 从屏幕宽度变化到 0
val scale = 1 - l * 1f / menuView.measuredWidth //scale 从 0 到1
//处理菜单页缩放和透明度
menuView.pivotX = menuView.measuredWidth * 1f
menuView.pivotY = menuView.measuredHeight / 2f
menuView.scaleX = 0.5f + scale * 0.5f
menuView.scaleY = 0.5f + scale * 0.5f
menuView.alpha = 0.25f + 0.75f * scale
//处理内容页缩放 缩放到0.7f
contentView.pivotX = 0f
contentView.pivotY = contentView.measuredHeight / 2f
contentView.scaleX = 0.7f + (1 - scale) * 0.3f
contentView.scaleY = 0.7f + (1 - scale) * 0.3f
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_MOVE) {
return false
}
if (isMenuOpen) {
//如果点击事件落在内容页,则进行拦截并关闭菜单页
if (ev.x > menuView.measuredWidth) {
//进行事件拦截,不触发button点击事件
isIntercept = true
return true
} else {
isIntercept = false
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(ev: MotionEvent): Boolean {
//当执行快速滑动时,后续不再执行
if (ev.action == MotionEvent.ACTION_MOVE) {
return false
}
if (gestureDetector.onTouchEvent(ev)) {
return gestureDetector.onTouchEvent(ev)
}
when (ev.action) {
MotionEvent.ACTION_UP -> {
if (isIntercept) {
closeMenu()
return true
}
//当手指抬起时,判断左侧菜单栏应该展示开始关闭
//判断逻辑:当滚动x > 屏幕一半是,菜单栏隐藏,否则展开
// if (mScrollX > getScreenWidth() / 2) {
// closeMenu()
// } else {
// openMenu()
// }
// return false
}
}
return super.onTouchEvent(ev)
}
/**
* 打开菜单
*/
fun openMenu() {
smoothScrollTo(0, 0)
isMenuOpen = true
}
override fun dispatchTouchEvent(me: MotionEvent?): Boolean {
if (me != null) {
this.gestureDetector.onTouchEvent(me)
}
return super.dispatchTouchEvent(me)
}
/**
* 关闭菜单
*/
fun closeMenu() {
smoothScrollTo(menuView.measuredWidth, 0)
isMenuOpen = false
}
}
关键点:
1、所谓的控制左右滑动,用到的是 smoothScrollTo()方法
2、使用SimpleOnGestureListener来进行手势监听,并且把onTouchEvent和dispatchTouchEvent的部分事件交由其处理。使用重写的onFling方法,进行各类手势处理,由于需求原因,上面的代码我禁止掉了右滑,可根据自己实际需求进行开发。
3、使用onInterceptTouchEvent拦截部分事件。
4、重写onScrollChanged监听滑动时,对左右布局进行缩放,这样会显得更流畅些。
5、重写onFinishInflate方法,拿到左右子布局,对其设置layoutparam属性,点击事件,赋值等等。
tips:menuLayoutParams.width = getScreenWidth() - menuRightWidth - SizeUtils.dp2px(100f)
这个就是设置左侧侧滑栏宽度,因为一般都不会全屏,所以会拿屏幕宽度去减去自己想要的值来展示右侧主页的内容,这儿可以给menuRightWidth设定一个值,不过我需求固定了,就直接在这儿减去了SizeUtils.dp2px(100f)。
第三步:代码调用:
class MainActivity : AppCompatActivity() {
private var setImg: AppCompatImageView? = null
private var sliding: SlideMenuLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setImg = findViewById(R.id.set)
sliding = findViewById(R.id.sliding)
setImg?.setOnClickListener {
sliding?.openMenu()
}
}
}
代码全在这儿了,我就不贴github地址了。
三、实现效果