最终效果
具体实现
布局文件
- 自定义属性
<!--自定义内容宽度-->
<declare-styleable name="SlideMenuLayout">
<attr name="menuRightWidth" format="dimension" />
</declare-styleable>
- 布局文件
### 主布局
<?xml version="1.0" encoding="utf-8"?>
<com.crystal.view.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:background="@drawable/home_menu_bg"
app:menuRightWidth="60dp"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!--左侧菜单页-->
<include layout="@layout/layout_home_menu" />
<!--右侧内容页-->
<include layout="@layout/layout_home_content" />
</LinearLayout>
</com.crystal.view.SlideMenuLayout>
### 左侧菜单页
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="72dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/enter_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="23dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/user_head_iv"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@drawable/morentouxiang" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="22dp"
android:orientation="vertical">
<TextView
android:id="@+id/user_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:drawableRight="@drawable/user_write_paint"
android:text="请登录"
android:textColor="#c6b178"
android:textSize="18dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
android:orientation="horizontal">
<TextView
android:id="@+id/user_attention_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:text="关注 0"
android:textColor="#c6b178"
android:textSize="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:drawablePadding="10dp"
android:text="粉丝 0"
android:textColor="#c6b178"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<ListView
android:id="@+id/menu_item_lv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@null"
android:dividerHeight="0dp"
android:layout_marginTop="60dp"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="20dp"
android:text="退出"
android:textColor="#FFFFFF"
android:layout_height="wrap_content" />
</RelativeLayout>
### 右侧内容页
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="60dp"
android:layout_height="40dp"
android:text="按钮"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="主页内容" />
</RelativeLayout>
自定义SlideMenuLayout
package com.crystal.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.widget.*
import kotlin.math.abs
/**
* 自定义侧滑菜单实现
* on 2022/10/27
*/
class SlideMenuLayout : HorizontalScrollView {
/**
* 当菜单页显示时,右侧内容页显示宽度
*/
private var menuRightWidth = 0
/**
* 左侧菜单页View
*/
private lateinit var menuView: View
/**
* 右侧内容页View
*/
private lateinit var contentView: View
/**
* 右侧内容页阴影View
*/
private lateinit var shaderView: View
/**
* 用于处理飞速滑动
*/
private var gestureDetector: GestureDetector
/**
* 菜单当前是否为打开状态
*/
private var isMenuOpen: Boolean = false
/**
* 内容页按钮
*/
private var btn: Button? = 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
) {
val array = context.obtainStyledAttributes(attrs, R.styleable.SlideMenuLayout)
menuRightWidth = array.getDimension(
R.styleable.SlideMenuLayout_menuRightWidth,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 62f, resources.displayMetrics)
).toInt()
array.recycle()
gestureDetector = GestureDetector(getContext(), GestureDetectorListener())
}
//用于处理飞速滑动
private inner class GestureDetectorListener : SimpleOnGestureListener() {
override fun onFling(
e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float
): Boolean {
// velocityX 向右快速滑动是正值,向左快速滑动是负值
//判断飞速滑动方向不是水平时,不处理
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
//这里childCount应该只包含 左侧菜单页以及右侧内容页,childCount 必须为2
if (childCount != 2) {
throw IllegalArgumentException("LinearLayout child size must be 2!")
}
//设置左侧菜单页宽度为屏幕宽度 - 主页收缩时宽度
menuView = linearLayout.getChildAt(0)
val menuLayoutParams = menuView.layoutParams
menuLayoutParams.width = getScreenWidth() - menuRightWidth
menuView.layoutParams = menuLayoutParams
//设置右侧内容页宽度为屏幕宽度
contentView = linearLayout.getChildAt(1)
/**
* 实现在滑动过程中,为右侧内容页添加阴影效果
* 1.先把contentView从布局中挖出来
* 2.新建RelativeLayout 包裹 此contentView以及一层阴影View
* 3.onScrollChanged中根据scale的值动态修改shader view的alpha值
*/
linearLayout.removeView(contentView)
val contentRelativeLayout = RelativeLayout(context)
contentRelativeLayout.addView(contentView)
shaderView = View(context)
shaderView.setBackgroundColor(Color.parseColor("#55000000"))
contentRelativeLayout.addView(shaderView)
val contentLayoutParams = contentView.layoutParams
contentLayoutParams.width = getScreenWidth()
contentRelativeLayout.layoutParams = contentLayoutParams
linearLayout.addView(contentRelativeLayout)
shaderView.alpha = 0.0f
btn = contentView.findViewById(R.id.btn)
btn?.setOnClickListener {
Toast.makeText(context, "This is Button", Toast.LENGTH_SHORT).show()
}
}
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
//滑动时,根据scale动态调整shader view的alpha值
shaderView.alpha = scale
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
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 (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)
}
/**
* 打开菜单
*/
private fun openMenu() {
smoothScrollTo(0, 0)
isMenuOpen = true
}
/**
* 关闭菜单
*/
private fun closeMenu() {
smoothScrollTo(menuView.measuredWidth, 0)
isMenuOpen = false
}
}
总结
这个案例涉及到事件分发、飞速滑动、view的缩放以及透明度处理,对自定义View的学习还是很有帮助的。