仿QQ侧滑和酷狗侧滑效果

本文介绍了如何使用自定义ViewGroup实现类似QQ和酷狗的侧滑菜单效果。通过在ViewGroup下放置两个布局,分别用于菜单和内容,并在onFinishInflate()中设置布局宽度。侧滑的实现依赖于触摸事件处理,菜单的打开和关闭状态在onLayout和onTouchEvent中调整。同时,文章提到了如何添加酷狗菜单的特效和内容布局的阴影,以及利用GestureDetector处理快速滑动。最后,讨论了在内容页面点击时如何判断并处理菜单状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天来写的是关于侧滑的,说道侧滑,我们可以想导用什么来实现呢?首先肯定是系统为我们写好的DrawerLayout来写,或者用ViewGroup再加上一些手势处理,下面我来写的是自定义view继承自我们的HorizontalScrollView。

先看一下实际的QQ效果和酷狗的实际效果


先说一下实现思路:

就是在自定义的ViewGroup下面放两个布局,一个是菜单布局,一个是内容布局,看看我们在代码中使用情况吧

activity_main:

<?xml version="1.0" encoding="utf-8"?>
<moocollege.cn.kugougouslidemenu.KuGouSlideMenu xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@mipmap/home_bg_menu"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:menuRightMargin="100dp"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <include layout="@layout/layout_menu" />

        <include layout="@layout/layout_content" />
    </LinearLayout>

</moocollege.cn.kugougouslidemenu.KuGouSlideMenu>


当然你要是以为直接这样做就可以的话,在自定义的viewGropu中不做处理,布局会乱的。想看效果的可以试试

不做任何处理的

public class KuGouSlideMenu extends HorizontalScrollView {

    public KuGouSlideMenu(Context context) {
        this(context, null);
    }

    public KuGouSlideMenu(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public KuGouSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    
    }
}

layout_menu
<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:id="@+id/layout_menu_top"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_above="@+id/layout_bottom"
        android:layout_marginTop="20dp"
        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="@mipmap/toux2" />

            <TextView
                android:id="@+id/user_name_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="16dp"
                android:text="登录/注册"
                android:textColor="#FFFFFF"
                android:textSize="18sp" />
        </LinearLayout>

        <ListView
            android:id="@+id/menu_item_lv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="40dp"
            android:divider="@null"
            android:dividerHeight="0dp" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/layout_bottom"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:gravity="center"
            android:text="设置"
            android:textColor="#FFFFFF"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:gravity="center"
            android:text="退出"
            android:textColor="#FFFFFF"
            android:textSize="18sp" />
    </LinearLayout>

</RelativeLayout>
layout_content
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/rl_content"
    android:background="#FFFFFF"
    android:orientation="vertical">

    <ImageView
        android:onClick="clickImage"
        android:layout_width="56dp"
        android:layout_height="56dp"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="20dp"
        android:src="@mipmap/toux2" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="酷狗主页内容"
        android:textColor="@color/colorAccent"
        android:textSize="30sp" />

</RelativeLayout>
上面这样布局会乱的

这就要我们在自定义view中为我们的两个布局去指定宽度,在onFinishInflate()中去处理,这个方法在解析完xml的时候会调用,

菜单的宽度 = 屏幕的宽度 - 菜单离右边屏幕的距离;

我们一进来菜单是默认关闭的,那么我们就要在onLayout中调用scroolTo(菜单宽度,0),类比我们的ScrooView来理解。

触摸事件处理,在我们的手指抬起的时候,是要关闭还是要打开呢。在这里注意我们当前滑动的距离和菜单的宽度的关系

酷狗中的菜单特效,缩放等一些效果和QQ侧滑中的内容布局的阴影我们在onScrooChanged()方法中去做,处理手势的快速滑动我们借助于系统提供的一个类GestureDetector

点击内容页面的时候我们要判断当前菜单是否关闭,如果需要关闭菜单,那么我们就要拦截子view的事件,在onInterCept中去做相应的处理,并且在拦截子view事件的时候,不要去触发我们自己的onTouc方法,具体看代码:

酷狗:

public class KuGouSlideMenu extends HorizontalScrollView {


    //左边的菜单
    private View mMenuView;
    //主页的内容
    private View mContentView;
    private Context mContext;
    //菜单的宽度
    private int mMenuWidth;
    //当前是否打开 默认一进来是关闭的
    private boolean mMenuIsOpen = false;
    private GestureDetector mGestureDetector; // 系统自带的手势处理类
    private boolean mIntercept = false;

    public KuGouSlideMenu(Context context) {
        this(context, null);
    }

    public KuGouSlideMenu(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public KuGouSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        mGestureDetector = new GestureDetector(mContext, new GestureDetectorListener());
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.KuGouSlideMenu);
        float rightMargin = array.getDimension(R.styleable.KuGouSlideMenu_menuRightMargin, ScreenUtils.dip2px(mContext, 50));
        //菜单的宽度 = 屏幕的宽度-菜单离右边的距离
        mMenuWidth = (int) (ScreenUtils.getScreenWidth(mContext) - rightMargin);
        array.recycle();
    }


    /**
     * 手势处理的监听类
     */
    private class GestureDetectorListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // 快速滑动
            // 向右快速滑动会是正的  +   向左快速滑动 是  -
            // 如果菜单是打开的   向右向左快速滑动都会回调这个方法
            if (mMenuIsOpen) {
                if (velocityX < 0) {
                    closeMenu();
                    return true;
                }
            } else {
                if (velocityX > 0) {
                    openMenu();
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * 1.这个方法在整个布局xml解析完毕走这个方法
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //获取菜单和主页内容
        //我们在这里getChildAt(0)拿的是我们布局中的LinearLayout
        ViewGroup container = (ViewGroup) getChildAt(0);
        int childCount = container.getChildCount();
        if (childCount != 2) {
            //抛运行时异常,只能放置两个子view
            throw new RuntimeException("You can only place two sub view");
        }
        //拿到我们的菜单布局
        mMenuView = container.getChildAt(0);
        //拿到我们的主页内容的布局
        mContentView = container.getChildAt(1);
        ViewGroup.LayoutParams layoutMenuParams = mMenuView.getLayoutParams();
        //指定菜单的宽度
        layoutMenuParams.width = mMenuWidth;
        //7.0
        mMenuView.setLayoutParams(layoutMenuParams);
        ViewGroup.LayoutParams layoutContentParams = mContentView.getLayoutParams();
        //指定内容的宽度 指定宽高后会重新摆放 在onLayout中
        layoutContentParams.width = ScreenUtils.getScreenWidth(mContext);
        mContentView.setLayoutParams(layoutContentParams);
    }

    //2 布局摆放 默认进来进来是关闭的
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        // 用来排放子布局的   等子View全部摆放完才能去滚动 我们一进来的时候默认是关闭菜单的
        //类比纵向的ScrollVew的来理解
        scrollTo(mMenuWidth, 0);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        mIntercept = false;
        if (mMenuIsOpen) {
            float currentX = ev.getX();
            if (currentX > mMenuWidth) {
                //关闭菜单
                closeMenu();
                //子view不响应任何事件 拦截子view的触摸事件
                //如果返回true 代表会拦截子view的触摸事件,但是会相应自己的onTouch事件
                mIntercept = true;
                return true;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

    //3 事件的拦截处理
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //获取手指滑动速率,获取手指滑动的速率,当期大于一定值就认为是快速滑动 , GestureDetector(系统提供好的类)
        //当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件(打开情况下点击内容页不会响应点击事件)
        //这里保证了手势处理类的调用
        //快速滑动了 下面的拦截事件就不要处理了、
        if (mGestureDetector.onTouchEvent(ev)) {
            return true;
        }
        //如果有拦截,则不执行自己的onTouch方法
        if (mIntercept){
            return true;
        }
        // 拦截处理事件
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_UP:
                int currentScrollX = getScrollX();
                //在这里注意currentScrollX的变化,当我们默认关闭菜单的时候去拉动ScrollView,数值在不断的变小
                if (currentScrollX < mMenuWidth / 2) {
                    //打开菜单
                    openMenu();
                } else {
                    //关闭菜单
                    closeMenu();
                }
                //确保super.onTouchEvent不会执行 这里看super.onTouchEvent源码中的fling方法
                //和smoothScrollTo的源码
                return true;
        }
        return super.onTouchEvent(ev);
    }


    //4 处理主页内容的缩放,左边的缩放和透明度的调节 这就需要不断的获取当前的滑动位置
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //l是从mMuneWidth一直变化到0
        //计算梯度值
        float scale = 1f * l / mMenuWidth; //梯度从1逐渐变为0
        //右边的缩放 最小0.7f 最大是1
        float rightScale = 0.7f + 0.3f * scale;
        //设置主页内容的缩放,默认是中心点缩放
        //设置缩放的中心点
        ViewCompat.setPivotX(mContentView, 0);
        ViewCompat.setPivotY(mContentView, mContentView.getMeasuredHeight() / 2);
        ViewCompat.setScaleX(mContentView, rightScale);
        ViewCompat.setScaleY(mContentView, rightScale);

        //设置菜单的缩放和透明度 从半透明到完全透明 0.5f到1.0f
        float menuAlpha = 0.5f + (1 - scale) * 0.5f;
        ViewCompat.setAlpha(mMenuView, menuAlpha);
        //缩放处理
        float menuScale = 0.7f + (1 - scale) * 0.3f;
        ViewCompat.setScaleX(mMenuView, menuScale);
        ViewCompat.setScaleY(mMenuView, menuScale);
        //设置平移 l*0.7f
        ViewCompat.setTranslationX(mMenuView, 0.25f * l);


    }

    /**
     * 关闭菜单
     */
    private void closeMenu() {
        smoothScrollTo(mMenuWidth, 0);
        mMenuIsOpen = false;
    }

    /**
     * 打开菜单
     */
    private void openMenu() {
        smoothScrollTo(0, 0);
        mMenuIsOpen = true;
    }
}

QQ侧滑代码:

public class QQSlider extends HorizontalScrollView {

    //左边菜单布局
    private View mLeftMenu;
    //内容布局
    private View mContentView;
    //菜单是否打开
    private boolean mMenuIsOpen;
    //是否拦截事件
    private boolean mIntercept;
    //手势处理类
    private GestureDetector mGestureDetector;
    //菜单的宽度
    private int mMenuWidth;
    private View mShadeView;
    private Context mContext;

    public QQSlider(Context context) {
        this(context,null);
    }

    public QQSlider(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public QQSlider(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQSlider);
        //菜单离右边屏幕边缘的距离
        float rightMargin = array.getDimension(R.styleable.QQSlider_qqMenuRightMargin, ScreenUtils.dip2px(mContext, 50));
        mMenuWidth = (int) (ScreenUtils.getScreenWidth(mContext) - rightMargin);
        array.recycle();
        mGestureDetector = new GestureDetector(mContext, mGestureDetectorListener);
    }

    private GestureDetector.SimpleOnGestureListener mGestureDetectorListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // 快速滑动
            // 向右快速滑动会是正的  +   向左快速滑动 是  -
            // 如果菜单是打开的   向右向左快速滑动都会回调这个方法
            if (mMenuIsOpen) {
                if (velocityX < 0) {
                    closeMenu();
                    return true;
                }
            } else {
                if (velocityX > 0) {
                    openMenu();
                    return true;
                }
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    };

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //拿到根布局
        ViewGroup rootView = (ViewGroup) getChildAt(0);
        int childCount = rootView.getChildCount();
        if (childCount != 2)
            throw new RuntimeException("You can only place two sub view in the root");
        //拿到菜单布局
        mLeftMenu= rootView.getChildAt(0);
        //指定菜单的宽度
        ViewGroup.LayoutParams mLeftMenuLayoutParams = mLeftMenu.getLayoutParams();
        mLeftMenuLayoutParams.width = mMenuWidth;
        mLeftMenu.setLayoutParams(mLeftMenuLayoutParams);

        //拿到内容布局
        mContentView= rootView.getChildAt(1);
        //指定内容的宽度
        ViewGroup.LayoutParams mContentLayoutParams = mContentView.getLayoutParams();
        //把内容布局单读提出来
        rootView.removeView(mContentView);
        //在外面套一层阴影
        RelativeLayout contentContainer = new RelativeLayout(mContext);
        contentContainer.addView(mContentView);
        mShadeView = new View(mContext);
        mShadeView.setBackgroundColor(Color.parseColor("#55000000"));
        contentContainer.addView(mShadeView);
        //把容器放回去
        mContentLayoutParams.width = ScreenUtils.getScreenWidth(mContext);

        contentContainer.setLayoutParams(mContentLayoutParams);

        rootView.addView(contentContainer);
        mShadeView.setAlpha(0.0f);






    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        scrollTo(mMenuWidth,0);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        mIntercept = false;
        if (mMenuIsOpen) {
            float currentX = ev.getX();
            if (currentX > mMenuWidth) {
                //关闭菜单
                closeMenu();
                //子view不响应任何事件 拦截子view的触摸事件
                //如果返回true 代表会拦截子view的触摸事件,但是会相应自己的onTouch事件
                mIntercept = true;
                return true;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

    //3 事件的拦截处理
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //获取手指滑动速率,获取手指滑动的速率,当期大于一定值就认为是快速滑动 , GestureDetector(系统提供好的类)
        //当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件(打开情况下点击内容页不会响应点击事件)
        //这里保证了手势处理类的调用
        //快速滑动了 下面的拦截事件就不要处理了、
        if (mGestureDetector.onTouchEvent(ev)) {
            return true;
        }
        //如果有拦截,则不执行自己的onTouch方法
        if (mIntercept){
            return true;
        }
        // 拦截处理事件
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_UP:
                int currentScrollX = getScrollX();
                //在这里注意currentScrollX的变化,当我们默认关闭菜单的时候去拉动ScrollView,数值在不断的变小
                if (currentScrollX < mMenuWidth / 2) {
                    //打开菜单
                    openMenu();
                } else {
                    //关闭菜单
                    closeMenu();
                }
                //确保super.onTouchEvent不会执行 这里看super.onTouchEvent源码中的fling方法
                //和smoothScrollTo的源码
                return true;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 关闭菜单
     */
    private void closeMenu() {
        smoothScrollTo(mMenuWidth, 0);
        mMenuIsOpen = false;
    }

    /**
     * 打开菜单
     */
    private void openMenu() {
        smoothScrollTo(0, 0);
        mMenuIsOpen = true;
    }

    //4 处理主页内容的,这就需要不断的获取当前的滑动位置
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //l是从mMuneWidth一直变化到0
        //计算梯度值
        float scale = 1f * l / mMenuWidth; //梯度从1逐渐变为0
        //控制阴影 从0变化到1
        float alphaScale = 1 -scale;
        mShadeView.setAlpha(alphaScale);

        ViewCompat.setTranslationX(mLeftMenu, 0.6f * l);

    }
}
来看一下我们自己实现的效果:


附上github地址:点击打开链接



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值