Android自定义HorizontalScrollView实现侧滑菜单效果

最终效果

自定义HorizontalScrollView实现侧滑菜单效果

具体实现

布局文件
  • 自定义属性
    <!--自定义内容宽度-->
   <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的学习还是很有帮助的。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值