史上最簡單粗暴實現側滑菜單

博文出處:史上最簡單粗暴實現側滑菜單,歡迎大家關注我的博客,謝謝!

在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!~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容