史上最简单粗暴实现侧滑菜单

博文出处:史上最简单粗暴实现侧滑菜单,欢迎大家关注我的博客,谢谢!

在Android开发中,相信侧滑菜单大家都不陌生吧,几乎是每个app都必备的。从早期的 SlidingMenu 再到 AndroidResideMenu 最后到Android自带的DrawerLayout,无处不体现着侧滑菜单的诱人魅力。侧滑菜单可以拓展app的内容,充分利用手机屏幕,增加程序的可玩性。既然有这么多可供选择的侧滑菜单使用,那为什么我们还要自己写呢?我觉得我们在使用侧滑菜单的时候应该要懂得其中的原理,更好的,可以自己写一个侧滑菜单来加深体会。

好了,话不多说。来看看我们所谓“史上最简单粗暴实现的侧滑菜单”的产物吧:

侧滑菜单gif

看完了上面的gif,想不想自己也写一个呢,那还等什么,一起来看看喽。

首先来说一下侧滑菜单实现的思路:侧滑菜单的布局为MenuLayout,还有主页的布局为MainLayout。MenuLayout在MainLayout的左边,当手指向右滑动的时候,MainLayout就向右滑动,同时MenuLayout跟着向右滑动,于是就显示出了侧滑菜单。以下是示意图:

侧滑菜单示意图

大概地了解思路以后,我们先来看看布局文件。

layout_slidemenu.xml(侧滑菜单的布局):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:background="@drawable/menu_bg"
    android:orientation="vertical">

    <ListView
        android:id="@+id/lv_menu"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:cacheColorHint="@null" />
</LinearLayout>

layout_activity_main.xml(主界面的布局):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#55666666"
android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@drawable/top_bar_bg"
        android:gravity="center_vertical" >

        <ImageView
            android:id="@+id/iv_menu"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_gravity="center_vertical"
            android:background="@drawable/img_menu" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:text="SlidingMenu"
            android:layout_gravity="center_vertical"
            android:textColor="#ffffff"
            android:textSize="22sp" />
    </LinearLayout>

</LinearLayout>

layout_main.xml(activity的布局),注意,主界面的布局一定要放在菜单布局的后面:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >

    <com.yuqirong.slidingmenu.view.SlidingMenu
        android:id="@+id/slideMenu1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >

        <!-- 菜单界面的布局 -->

        <include layout="@layout/layout_slidemenu" />

        <!-- 主界面的布局 -->

        <include layout="@layout/layout_activity_main" />
    </com.yuqirong.slidingmenu.view.SlidingMenu>

</RelativeLayout>

看完了布局文件,下面我们就来看看代码(以下为部分代码,并非全部):

public class SlidingMenu extends FrameLayout {

    private ViewDragHelper mdDragHelper;

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

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

    public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mdDragHelper = ViewDragHelper.create(this, callback);
    }
}

我们创建一个类名叫SlidingMenu,继承自FrameLayout,然后重写构造器。在构造器中新建了一个ViewDragHelper的对象。如果你还不知道ViewDragHelper为何物,建议你去看看鸿洋_《Android ViewDragHelper完全解析 自定义ViewGroup神器》,这里就不展开叙述了。在ViewDragHelper.create(Context context,ViewDragHelper.Callback callback)里我们传入了一个回调callback,那接下来就来看看这个callback:

Callback callback = new Callback() {

    @Override
    public boolean tryCaptureView(View view, int arg1) {
        return true;
    }

    public int getViewHorizontalDragRange(View child) {
        return menuWidth;
    }

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        if (child == mainView) {
            if (left < 0)
                return 0;
            else if (left > menuWidth)
                return menuWidth;
            else
                return left;
        } else if (child == menuView) {
            if (left > 0)
                return 0;
            else if (left > menuWidth)
                return menuWidth;
            else
                return left;
        }
        return 0;
    }

    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        if (releasedChild == mainView) {
            if (status == Status.Open) {
        // 关闭侧滑菜单
        close();
                return;
            }
            if (xvel == 0
                    && Math.abs(mainView.getLeft()) > menuWidth / 2.0f) {
        // 打开侧滑菜单
        open();
            } else if (xvel > 0) {
                open();
            } else {
                close();
            }
        } else {
            if (xvel == 0
                    && Math.abs(mainView.getLeft()) > menuWidth / 2.0f) {
        // 打开侧滑菜单
        open();
            }else if (xvel > 0) {
                open();
            } else {
        // 关闭侧滑菜单
        close();
            }
        }
    }

};

我们发现在callback中几乎完成了绝大部分的逻辑。首先在tryCaptureView(View view, int arg1)直接返回了true,因为无论在mainView(主View)还是在menuView(菜单View)都应该去捕获,而getViewHorizontalDragRange(View child)返回的应该是menuView的宽度,也就是说滑动的时候最多能滑menuWidth的距离。而menuWidth是在onFinishInflate()中得到的。至于clampViewPositionHorizontal(View child, int left, int dx)方法逻辑很简单,相信大家都看得懂。最后在onViewReleased(View releasedChild, float xvel, float yvel)方法中判断了菜单打开或关闭的逻辑,比如在菜单关闭的情况下,只要手指向右滑或是停止滑动时侧滑菜单在屏幕中的宽度大于menuWidth/2这两种情况下,侧滑菜单都是执行open()方法,其它的情况以此类推。下面就来看看open()和close()方法。

/**
 * 打开菜单
 */
public void open() {
    if (mdDragHelper.smoothSlideViewTo(mainView, menuWidth, 0)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
    preStatus = status;
    status = Status.Open;
    if (listener != null && preStatus == Status.Close) {
        listener.statusChanged(status);
    }
}

/**
 * 关闭菜单
 */
public void close() {
    if (mdDragHelper.smoothSlideViewTo(mainView, 0, 0)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
    preStatus = status;
    status = Status.Close;
    if (listener != null && preStatus == Status.Open) {
        listener.statusChanged(status);
    }
}

/**
 * 切换菜单状态
 */
public void toggle() {
    if (status == Status.Close) {
        open();
    } else {
        close();
    }
} 

@Override
public void computeScroll() {
    super.computeScroll();
    // 开始执行动画
    if (mdDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

我们发现在open()close()两个方法中都调用了ViewCompat.postInvalidateOnAnimation(this);postInvalidateOnAnimation(View view)需要重写computeScroll()来实现平滑滚动的效果,一般的写法都如上代码所示,不需要改动。再重新回到open()close()两个方法,其中的listener就是菜单开关状态的监听器,当状态改变的时候都会回调listener的statusChanged(Status status)方法。

最后的最后,别忘了在onLayout(boolean changed, int left, int top, int right, int bottom)中把menuView设置在mainView的左边。而menuView和mainView都是在onFinishInflate()中得到的。

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    if(getChildCount()!=2){
        throw new IllegalArgumentException("子view的数量必须为2个");
    }
    menuView = getChildAt(0);
    mainView = getChildAt(1);
    menuWidth = menuView.getLayoutParams().width;
}

@Override
protected void onLayout(boolean changed, int left, int top, int right,
                        int bottom) {
    menuView.layout(-menuWidth, 0, 0, menuView.getMeasuredHeight());
    mainView.layout(0, 0, right, bottom);
}

好了,讲解了这么多,差不多把SlidingMenu的代码逻辑讲解完成了。如果有什么疑问,可以在下面留言。

国际惯例,下面贴出源码下载链接:

SlidingMenu.rar

~have fun!~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容