Android侧滑栏(一)可缩放可一起移动的侧滑栏

news2025/2/26 5:32:39

在实际的各类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地址了。

三、实现效果

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/862923.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

为c语言安装easyx图形库

按照图上的步骤&#xff0c;安装easyx图形库。 接下来看代码&#xff1a; #include<easyx.h> #include<stdio.h> #define width 800 #define height 600int main() {initgraph(width, height); // 初始化窗口&#xff08;宽度&#xff0c; 高度&#xff09;…

OpenHarmony社区运营报告(2023年7月)

本月快讯 • 2023年7月28日-29日&#xff0c;全球软件质量&效能大会&#xff08;简称“QECon”&#xff09;圆满举行&#xff0c;OpenAtom OpenHarmony&#xff08;简称“OpenHarmony”&#xff09;以“优质高效测试助力OpenHarmony北向应用生态赋能”为主题&#xff0c;以…

Java项目作业~ 通过html+Servlet+MyBatis,完成站点信息的添加功能

需求&#xff1a; 通过htmlServletMyBatis&#xff0c;完成站点信息的添加功能。 以下是站点表的建表语句&#xff1a; CREATE TABLE websites (id int(11) NOT NULL AUTO_INCREMENT,name char(20) NOT NULL DEFAULT COMMENT 站点名称,url varchar(255) NOT NULL DEFAULT ,…

目标识别模型两种部署形态图

目标检测预训练模型基于新数据进行微调&#xff08;训练&#xff09;之后&#xff0c;得到一个权重文件。 在日常工业、车载等需求环境下&#xff0c;需要在嵌入式移动端的软件系统中调用该模型文件进行推断测试&#xff0c;软件系统追求性能经常使用C/C进行编码实现&#xff…

Apipost接口自动化中关联关系如何配置

在接口自动化测试中&#xff0c;接口之间可能存在依赖关系&#xff0c;即某些接口的执行需要先完成其他接口的执行。为了确保测试用例的正确执行&#xff0c;我们需要在配置测试用例时考虑接口之间的依赖关系。在编写测试用例时&#xff0c;需要明确每个接口的功能和输入输出参…

注册亚马逊买家账号需要什么资料

注册亚马逊买家账号通常需要以下基本资料&#xff1a; 1、邮箱&#xff1a;您需要一个有效的邮箱&#xff0c;用于注册账号和接收与账户相关的通知。 2、密码&#xff1a;选择一个安全的密码&#xff0c;以确保您的账号信息安全。 3、姓名&#xff1a;提供您的全名或常用的姓…

高忆管理:今年来尚未有公司递表,香港SPAC市场为何“熄火”?

香港SPAC上市准则敞开之后&#xff0c;从第一家公司上市到现在已经有1年多的时刻。&#xff08;【深度】王石、李宁、卫哲争相发起建立SPAC&#xff0c;香港版“上市盲盒”会火吗&#xff1f; 界面新闻了解到&#xff0c;今年以来&#xff0c;香港SPAC并没有新动态&#xff0c;…

Linux 文件查看命令

一、cat命令 1.cat文件名&#xff0c;查看文件内容&#xff1a; 例如&#xff0c;查看main.c文件的内容&#xff1a; 2.cat < 文件名&#xff0c;往文件中写入数据&#xff0c; Ctrld是结束输入 例如&#xff0c;向文件a.txt中写入数据&#xff1a; 查看刚刚写入a.txt的…

linux网络编程-libevent

libevent介绍 1 事件驱动, 高性能, 轻量级, 专注于网络 2 源代码精炼, 易读 3 跨平台 4 支持多种I/O多路复用技术, 如epoll select poll等 5 支持I/O和信号等事件 1.libevent的安装 登录官方网站: http://libevent.org, 查看相关信息 libevent源码下载主要分2个大版本&…

Linux中使用split切割文件,按行或者文件大小切割

环境中有5G大小的文件1千多万行&#xff0c;需要按行数切割&#xff0c;使用linux中的split工具可快速实现。 示例&#xff1a;测试文件造的是100万行&#xff0c;按行数切割&#xff1a; split -d -l 80000 test.txt qiege --additional-suffix.txt -d表示切割后的文件按照…

Dex文件混淆(一):BlackObfuscator

Dex文件混淆(一)&#xff1a;BlackObfuscator 首发地址:http://zhuoyue360.com/crack/105.html 文章目录 Dex文件混淆(一)&#xff1a;BlackObfuscator1. 前言2.小试牛刀3. 参考学习1. dex2jar源码简析2. BlackObfuscator简析1. 控制流平坦化1. 控制流平坦化基本介绍 2. Dex解析…

职场新星:Java面试干货让你笑傲求职路(三)

职场新星&#xff1a;Java面试干货让你笑傲求职路 1、token 为什么存放在 redis 中&#xff1f;2、索引的底层原理是什么&#xff1f;3、Spring IOC和AOP的原理4、接口和抽象类有什么共同点和区别&#xff1f;5、为什么要使用线程池&#xff1f;直接new个线程不好吗&#xff1f…

C语言函数详解(2)

目录 函数的声明和定义 函数声明 函数定义 函数递归 什么是递归 递归的两个必要条件 练习1 练习2 练习3 练习4 函数的声明和定义 函数声明 1. 告诉编译器有一个函数叫什么&#xff0c;参数是什么&#xff0c;返回类型是什么。但是具体是不是存在&#xff0c;函数声明决定…

一键部署 Umami 统计个人网站访问数据

谈到网站统计&#xff0c;大家第一时间想到的肯定是 Google Analytics。然而&#xff0c;我们都知道 Google Analytics 会收集所有用户的信息&#xff0c;对数据没有任何控制和隐私保护。 Google Analytics 收集的指标实在是太多了&#xff0c;有很多都是不必要的&#xff0c;…

我被一位美女程序员夸了!

见字如面&#xff0c;我是军哥&#xff01; 昨天晚上&#xff0c;一位美女程序员&#xff08;为什么说是美女&#xff0c;因为我有她的简历照片&#xff09;读者主动找我聊天&#xff0c;说之前买我的《技术人核心能力》课程终于看完了&#xff0c;受益匪浅&#xff0c;并且有些…

【QT】 QSS样式表设计一文了解

很高兴在雪易的CSDN遇见你 &#xff0c;给你糖糖 欢迎大家加入雪易社区-CSDN社区云 前言 本文分享QT界面设计中的QSS样式技术&#xff0c;主要从**、**和**方面展开&#xff0c;希望对各位小伙伴有所帮助&#xff01;学会了QSS样式设计&#xff0c;就可以开动你的审美&#…

【腾讯云 Cloud Studio 实战训练营】基于Python实现的快速抽奖系统

文章目录 ⭐️ Cloud Studio - 简介&#x1f31f; 操作步骤&#x1f31f; 注册Cloud Studio&#x1f31f; 创建工作空间&#x1f31f; 启动对应的开发环境 ⭐️ 抽奖系统项目介绍⭐️ 抽奖系统代码结构图⭐️ 项目基础类 - 文件检查&#x1f31f; base.py 基础类文件检查示例如…

【踩坑系列记录 】Anaconda环境将torch由cpu换成gpu

概要 很早前做过深度学习&#xff0c;配环境之类的坑由于没记录都记不清了。这段时间开始做深度学习的项目&#xff0c;于是用Anaconda给项目创建了一个环境&#xff0c;其他的环境配置很顺利&#xff0c;就是到了安装pytorch时&#xff0c;我用pytorch官网的代码一直下载的是…

法学领域的技术创新点

文章目录 一、中国法研杯-2019方案分享1 相似案件检索——法律文书的相似判断方案1 -冠军方案2——三等奖 2 裁判文书论辩挖掘 二、中国法研杯2018总述Overview of CAIL2018: Legal Judgment Prediction Competition 三、中国法研杯2022-任务概述事件检测文书校对类案检索司法摘…

对留学生来说,ChatGPT究竟是福是祸?

ChatGPT&#xff0c;“出道即顶流”。教师们防ChatGPT如洪水猛兽&#xff0c;学生们使用起来依然肆无忌惮。抄袭、作弊、代写……围绕着ChatGPT的争议不绝于耳。其堪比人类的流畅写作技能和逻辑思维&#xff0c;更引发一些人的担忧&#xff1a;ChatGPT会终结大学申请文书时代吗…