Android 左滑or右滑抽屉菜单

简介:

概述

本篇只是个示例,理解本篇博客后,可实现仿QQ5.0侧滑,左右两侧滑动菜单。再加上各种缩放,平移特效。DuangDuang的。本篇效果如下:

效果图

实现步骤

  1. 因为需要水平滑动,所以继承HorizontalScrollView
  2. 本Domo分为两个部分mMainLayout和mRightLayout。在onMeasure初始化这两部分的宽度
  3. 在onTouchEvent中判断是否完全展示,拦截当前触摸事件
  4. 前三步已经实现最简单的滑动布局,最关键的是第四步。mMainLayout跟随手势不断滑动,实现抽屉菜单。
  5. 自己再加各种特效。

开启侧滑之旅

package com.example.chouti;

import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;

public class SlidingRightView extends HorizontalScrollView {
    /**
     * 主布局界面
     */
    private ViewGroup mMainLayout;
    /**
     * 侧滑界面
     */
    private ViewGroup mRightLayout;
    /**
     * 侧滑界面宽度
     */
    private int mRightLayoutWidth;
    /**
     * 侧滑界面距离屏幕左边的距离
     */
    private int mRightLayoutMarginLeft = 200; //px
    /**
     * 是否展示侧滑
     */
    private boolean isOpen;

    private int mScreenWidth;
    private int mScreenHeight;

    private Context mContext;


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

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

    public SlidingRightView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView(context);
    }

    private void initView(Context context) {
        mContext = context;
        getScreenWidthAndHeight();
    }

    private void getScreenWidthAndHeight() {
        WindowManager wm = (WindowManager) mContext.getSystemService(mContext.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;
        mScreenHeight = outMetrics.heightPixels;
        Log.i("TAG", "mScreenWidth=" + mScreenWidth + "mScreenHeight=" + mScreenHeight);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        LinearLayout v = (LinearLayout) this.getChildAt(0);
        mMainLayout = (ViewGroup) v.getChildAt(0);
        mRightLayout = (ViewGroup) v.getChildAt(1);

        mMainLayout.getLayoutParams().width = mScreenWidth;
        mRightLayout.getLayoutParams().width = mRightLayoutWidth = mScreenWidth - mRightLayoutMarginLeft;
        Log.i("TAG", " mRightLayout.getLayoutParams().width=" + mRightLayout.getLayoutParams().width);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_UP:
                // scrollX为水平滚动条滚动的宽度
                int scrollX = getScrollX();
                if (scrollX >= mRightLayoutWidth / 2) {
                    this.smoothScrollTo(mRightLayoutWidth, 0);
                    isOpen = true;
                } else {
                    this.smoothScrollTo(0, 0);
                    isOpen = false;
                }
                // 拦截事件
                return true;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 抽屉开关
     */
    public void toggleRightLayout() {
        if (isOpen) {
            closeRightLayout();
            isOpen = false;
        } else {
            openRightLayout();
            isOpen = true;
        }
    }

    /**
     * 打开抽屉
     */
    public void openRightLayout() {
        if (isOpen) {
            return;
        }
        this.smoothScrollTo(mRightLayoutWidth, 0);
        isOpen = true;
    }

    /**
     * 关闭抽屉
     */
    public void closeRightLayout() {
        if (!isOpen) {
            return;
        }
        this.smoothScrollTo(0, 0);
        isOpen = false;
    }


}

上文是前三步工程,完成上面三步之后我们已经有一个基础的滑动布局了。下面对上段代码进行解析。可以看到我们首先获取了屏幕的宽高。在onMeasure中我们为SlingRightView唯一childView的第一个childView和第二个childView宽赋值。在onLayout中默认不显示右侧布局。在onTouchEvent中判断滑动的宽度,如果大于右侧布局宽度的一半,则完全展示右侧布局。否则,不展示右侧布局。拦截当前事件。自定义View流程和拦截点击事件不太了解的同学可以看下我的另外两篇博客自定义View总结 Android 事件传递机制

关键的第四步

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        float scale = l * 1.0f / mRightLayoutWidth;// 0.0~1.0

        // 将mMainLayout平移l(第一个参数) 实现抽屉式侧滑菜单
        // 这是个相对运动的过程 一定要注意理解
        mMainLayout.setTranslationX(mRightLayoutWidth * scale);

    }

两行代码实现抽屉式菜单。 第一个参数l为HorizontalScrollView偏移的长度(其实和getScrollX等价) 初始化时没有滑动,此时l=0。这里使用了属性动画。因为是右边布局滑动,主布局需要跟随手势滑动,滑动距离等于右侧布局的宽度。主布局看起来就像是一直“固定”在页面一样,其实是动态的滑动。mMainLayout.setTranslationX(mRightLayoutWidth * scale);抽屉的精髓就是这行代码,一定要理解这个相对运动

布局文件示例:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.chouti.SlidingRightView
        android:id="@+id/sliding"
        android:layout_width="wrap_content"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <LinearLayout
                android:id="@+id/mainLayout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#ff0000"
                android:orientation="vertical">

                <Button
                    android:id="@+id/open"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="open"
                    android:textAllCaps="false" />

                <Button
                    android:id="@+id/close"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="close"
                    android:textAllCaps="false" />

                <Button
                    android:id="@+id/toggle"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="toggle"
                    android:textAllCaps="false" />
            </LinearLayout>

            <LinearLayout
                android:id="@+id/rightLayout"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:background="#00ff00"
                android:orientation="vertical">

                <Button
                    android:id="@+id/test"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:textAllCaps="false"
                    android:text="test" />

            </LinearLayout>
        </LinearLayout>
    </com.example.chouti.SlidingRightView>
</LinearLayout>

MainActivity调用

package com.example.chouti;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.Toast;

public class MainActivity extends Activity {

    private SlidingRightView slidingRightView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        slidingRightView = (SlidingRightView) findViewById(R.id.sliding);

        findViewById(R.id.open).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               slidingRightView.openRightLayout();
            }
        });

        findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                slidingRightView.closeRightLayout();
            }
        });

        findViewById(R.id.toggle).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                slidingRightView.toggleRightLayout();
            }
        });

        findViewById(R.id.test).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "click test", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

DuangDuang特效加起来

想要实现更酷炫的特效当然要用到动画。比如将抽屉进行缩放啦,透明度的变化了。具体看项目需求,下面是个简单的示例。

        // 为mRightLayout设置缩放和透明度的变化
        mRightLayout.setScaleX(mRightLayoutScale);
        mRightLayout.setScaleY(mRightLayoutScale);
        mRightLayout.setAlpha(mRightLayoutAlpha);

右滑抽屉式菜单到此已经完毕,感谢耐心读完。再次重申:这只是个Demo。现在假设需要左滑抽屉、左右滑抽屉、甚至更变态的各种滑,如果你有思路那么本篇博客的目的达到了。如果没有,建议重新阅读本篇博客。

第二次更新


左滑菜单

思路和右滑一样,直接贴代码了

/**
 * Created by Administrator on 2016/4/2.
 */
public class SlidingRightView extends HorizontalScrollView {
    /**
     * 左侧布局界面
     */
    private ViewGroup mLeftLayout;
    /**
     * 主界面
     */
    private ViewGroup mMainLayout;
    /**
     * 侧滑界面宽度
     */
    private int mLeftLayoutWidth;
    /**
     * 侧滑界面距离屏幕右边的距离
     */
    private int mLeftLayoutMarginRight = 200; //px
    /**
     * 是否展示侧滑
     */
    private boolean isOpen;
    private int mScreenWidth;
    private int mScreenHeight;

    private Context mContext;


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

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

    public SlidingRightView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView(context);
    }

    private void initView(Context context) {
        mContext = context;
        getScreenWidthAndHeight();
    }

    private void getScreenWidthAndHeight() {
        WindowManager wm = (WindowManager) mContext.getSystemService(mContext.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;
        mScreenHeight = outMetrics.heightPixels;
        Log.i("TAG", "mScreenWidth=" + mScreenWidth + "mScreenHeight=" + mScreenHeight);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        LinearLayout v = (LinearLayout) this.getChildAt(0);
        mLeftLayout = (ViewGroup) v.getChildAt(0);
        mMainLayout = (ViewGroup) v.getChildAt(1);
        mLeftLayout.getLayoutParams().width = mLeftLayoutWidth = mScreenWidth - mLeftLayoutMarginRight;
        mMainLayout.getLayoutParams().width = mScreenWidth;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

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

        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_UP:
                // scrollX为水平滚动条滚动的宽度
                int scrollX = getScrollX();
                if (scrollX >= mLeftLayoutWidth / 2) {
                    this.smoothScrollTo(mLeftLayoutWidth, 0);
                    isOpen = false;
                } else {
                    this.smoothScrollTo(0, 0);
                    isOpen = true;
                }
                // 拦截事件
                return true;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 抽屉开关
     */
    public void toggleRightLayout() {
        if (isOpen) {
            closeRightLayout();
            isOpen = false;
        } else {
            openRightLayout();
            isOpen = true;
        }
    }

    /**
     * 打开抽屉
     */
    public void openRightLayout() {
        if (isOpen) {
            return;
        }
        this.smoothScrollTo(0, 0);
        isOpen = true;
    }

    /**
     * 关闭抽屉
     */
    public void closeRightLayout() {
        if (!isOpen) {
            return;
        }
        this.smoothScrollTo(mLeftLayoutWidth, 0);
        isOpen = false;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        float scale = l * 1.0f / mLeftLayoutWidth;// 1.0~0.0
        mLeftLayout.setTranslationX(l);


    }
}

第三次更新

不一样的左滑

应评论要求,在第二次更新中,写了一个左滑抽屉。但是有个问题:无论滑动的是mLeftLayout还是mMainLayout,总是mMainLayout覆盖在mLeftLayout上面(右边覆盖左边)。假设我们要实现mLeftLayout覆盖在mMainLayout上面要怎么做呢?
写自定义View的时候千万不能狭隘,既然这样行不通,何不换一种写法?

package com.dyk.left;

public class LeftSlidingView extends RelativeLayout {

    private static final String TAG = "LeftSlidingView";

    /** 左侧布局界面 */
    private ViewGroup mLeftLayout;
    /** 主界面 */
    private ViewGroup mMainLayout;
    /**  侧滑界面宽度 */
    private int mLeftLayoutWidth;
    /** 侧滑界面距离屏幕右边的距离 */
    private int mLeftLayoutMarginRight = 200; // px
    /** 是否展示侧滑 */
    private boolean isOpen;
    private int mScreenWidth;
    private int mScreenHeight;

    private Context mContext;

    private Point mStartPoint;
    private Point mStopPoint;
    /** 初始位置记录 */
    private int[] location;
    private int dx;

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

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

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

    private void initView(Context context) {
        mContext = context;
        mStartPoint = new Point();
        mStopPoint = new Point();
        getScreenWidthAndHeight();
    }

    private void getScreenWidthAndHeight() {
        WindowManager wm = (WindowManager) mContext.getSystemService(mContext.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;
        mScreenHeight = outMetrics.heightPixels;
        Log.i("TAG", "mScreenWidth=" + mScreenWidth + "mScreenHeight=" + mScreenHeight);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mMainLayout = (ViewGroup) this.getChildAt(0);
        mLeftLayout = (ViewGroup) this.getChildAt(1);

        mLeftLayout.getLayoutParams().width = mLeftLayoutWidth = mScreenWidth - mLeftLayoutMarginRight;
        mMainLayout.getLayoutParams().width = mScreenWidth;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (changed) {
            mLeftLayout.setTranslationX(-mLeftLayoutWidth);// 最初隐藏mLeftLayout
        }
        super.onLayout(changed, l, t, r, b);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mStartPoint.x = (int) event.getX();
            mStartPoint.y = (int) event.getY();
            // 获取mLeftLayout初始位置
            this.location = new int[2];
            mLeftLayout.getLocationOnScreen(this.location);
            break;
        case MotionEvent.ACTION_MOVE:
            mStopPoint.x = (int) event.getX();
            mStopPoint.y = (int) event.getY();
            dx = mStopPoint.x - mStartPoint.x;
            int[] location = new int[2];
            mLeftLayout.getLocationOnScreen(location);
            // 右滑
            if (dx > 0) {
                if (this.location[0] == -(mLeftLayoutWidth - 1)) {//这里-1 是因为从0开始计算
                    setViewSlidingWidth(mLeftLayout, dx < mLeftLayoutWidth && location[0] >= -mLeftLayoutWidth ? -mLeftLayoutWidth + dx : 0);
                }
            } else {// 左滑

                if (this.location[0] == 0) {
                    setViewSlidingWidth(mLeftLayout, -dx < mLeftLayoutWidth ? dx : 0);
                }
            }

            break;
        case MotionEvent.ACTION_UP:
            int[] loc = new int[2];
            mLeftLayout.getLocationOnScreen(loc);
            if ((mLeftLayoutWidth + loc[0]) < mLeftLayoutWidth / 2) {// 不到一半
                close();
            } else {// 大于一半则显示
                open();
            }
            return true;
        }

        return true;
    }

    private void setViewSlidingWidth(View view, int width) {
        view.setTranslationX(width);
    }

    public void toggle() {
        if (isOpen) {
            close();
        } else {
            open();
        }

    }

    public void open() {
        isOpen = true;
        setViewSlidingWidth(mLeftLayout, 0);// 显示
    }

    public void close() {
        isOpen = false;
        setViewSlidingWidth(mLeftLayout, -mLeftLayoutWidth);// 隐藏
    }

}

相信看完上面的代码,读者心里已经比较明白了。但为了功底不是那么扎实的小伙伴,还请容我啰嗦一番。这里继承子RelativeLayout复写了一个ViewGroup,其中只能包含两个子ViewGroup。利用RelativeLayout自身的特点来实现mLeftLayout覆盖在mMainLayout上。然后就是一系列的移动,一定要注意好条件判断。

结束语

自定义View(ViewGroup)是一个很灵活的过程,但是万变不离其宗。重要的不是实现某一个或者某几个自定义View,而是要掌握自定义View的流程,动画和事件拦截机制。然后以此为基础,慢慢才能写出比较高级的View。

相关文章
|
3月前
|
XML API Android开发
码农之重学安卓:利用androidx.preference 快速创建一、二级设置菜单(demo)
本文介绍了如何使用androidx.preference库快速创建具有一级和二级菜单的Android设置界面的步骤和示例代码。
120 1
码农之重学安卓:利用androidx.preference 快速创建一、二级设置菜单(demo)
|
5月前
|
XML Java Android开发
34. 【Android教程】菜单:Menu
34. 【Android教程】菜单:Menu
110 2
|
5月前
|
开发工具 Android开发 开发者
Android Studio中两个让初学者崩溃菜单
Android Studio中两个让初学者崩溃菜单
54 0
|
6月前
|
XML Android开发 数据格式
android 12 添加菜单
android 12 添加菜单
41 0
|
3月前
|
API Android开发
Android使用AlertDialog实现弹出菜单
本文分享了在Android开发中使用AlertDialog实现弹出菜单的方法,并通过代码示例和错误处理,展示了如何避免因资源ID找不到导致的crash问题。
60 1
|
6月前
|
Java Android开发
Android 长按电源键弹出的GlobalActions菜单
Android 长按电源键弹出的GlobalActions菜单
162 1
|
6月前
|
Java Android开发
Android 长按桌面显示菜单的代码
Android 长按桌面显示菜单的代码
45 0
|
Android开发
Android 中选项菜单(Option menu)的用法
Android 中选项菜单(Option menu)的用法
191 0
|
6月前
|
Android开发
[Android]DrawerLayout滑动菜单+NavigationView
[Android]DrawerLayout滑动菜单+NavigationView
76 0
|
6月前
|
XML Java Android开发
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
476 0