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。

相关文章
|
4月前
|
Android开发
[Android]DrawerLayout滑动菜单+NavigationView
[Android]DrawerLayout滑动菜单+NavigationView
27 0
|
4月前
|
XML Java Android开发
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
66 0
|
4月前
|
XML Java Android开发
Android Studio App开发中工具栏Toolbar、溢出菜单OverflowMenu、标签布局TabLayout的讲解及实战(实现京东App的标签导航栏,附源码)
Android Studio App开发中工具栏Toolbar、溢出菜单OverflowMenu、标签布局TabLayout的讲解及实战(实现京东App的标签导航栏,附源码)
58 0
|
9月前
|
XML Android开发 数据格式
Android上机实验-4 菜单和对话框
Android上机实验-4 菜单和对话框
108 1
|
9月前
|
XML Java 测试技术
【Android开发日常】一文弄懂桌面图标快捷菜单 & 桌面小组件
开发可以定义快捷方式,以便在应用中执行特定操作。 这些快捷方式可在受支持的启动器或助理(如 Google 助理)中显示,方便用户快速启动应用中的常见任务或推荐任务。 通过本文你还将了解一些可提升快捷方式效果的最佳做法。
|
10月前
|
数据库 Android开发
重新构建711的Android项目(一),巧妙的小屏菜单查询框架实现
重新构建711的Android项目(一),巧妙的小屏菜单查询框架实现
|
11月前
|
XML Android开发 数据格式
【Android】DrawerLayout抽屉布局的写法
抽屉布局就类似那种侧拉出来的界面。 一个页面想要实现抽屉布局很简单! 这个XML文件包含抽屉布局的代码。
92 0
|
11月前
|
XML Java 数据格式
Android_三种常用Menu菜单(附源码)
记录一下我写菜单的代码,我写了一共有三种菜单,从常用到不常用。
132 0
|
Java Android开发
Android 7.1 导航栏增加按键, 关机菜单增加休眠选项
Android 7.1 导航栏增加按键, 关机菜单增加休眠选项
123 0
Android 7.1 导航栏增加按键, 关机菜单增加休眠选项
|
XML JSON Java
Android 侧滑抽屉菜单
Android 侧滑抽屉菜单
368 1
Android 侧滑抽屉菜单