天天看點

Android仿QQ5.0側滑菜單ResideMenu源碼分析

轉自:http://blog.csdn.net/cym492224103/article/details/39177275

androidresidemenu

github:https://github.com/specialcyci/androidresidemenu  csdn:http://download.csdn.net/detail/cym492224103/7887801

Android仿QQ5.0側滑菜單ResideMenu源碼分析

先看看如何使用:

把項目源碼下載下傳下來導入工程,可以看到

Android仿QQ5.0側滑菜單ResideMenu源碼分析

residemenu為引用工程,再看看如何使用這個引用工程來建構出residemenu,

1.先new一個residemenu對象

[java] view

plaincopyprint?

residemenu = new residemenu(this);  

2.設定它的背景圖檔

residemenu.setbackground(r.drawable.menu_background);  

3.綁定目前activity

residemenu.attachtoactivity(this);  

4.設定監聽

residemenu.setmenulistener(menulistener);  

可以監聽菜單打開和關閉狀态

private residemenu.onmenulistener menulistener = new residemenu.onmenulistener() {  

    @override  

    public void openmenu() {  

        toast.maketext(mcontext, "menu is opened!", toast.length_short).show();  

    }  

    public void closemenu() {  

        toast.maketext(mcontext, "menu is closed!", toast.length_short).show();  

};  

5.設定内容縮放比例(0.1~1f)

//valid scale factor is between 0.0f and 1.0f. leftmenu'width is 150dip.   

        residemenu.setscalevalue(0.6f);  

6.建立子菜單

// create menu items;  

       itemhome     = new residemenuitem(this, r.drawable.icon_home,     "home");  

       itemprofile  = new residemenuitem(this, r.drawable.icon_profile,  "profile");  

       itemcalendar = new residemenuitem(this, r.drawable.icon_calendar, "calendar");  

       itemsettings = new residemenuitem(this, r.drawable.icon_settings, "settings");  

7.設定點選事件及将剛建立的子菜單添加到側換菜單中(可以看到它是通過常量來控制子菜單的添加位置)

itemhome.setonclicklistener(this);  

        itemprofile.setonclicklistener(this);  

        itemcalendar.setonclicklistener(this);  

        itemsettings.setonclicklistener(this);  

        residemenu.addmenuitem(itemhome, residemenu.direction_left);  

        residemenu.addmenuitem(itemprofile, residemenu.direction_left);  

        residemenu.addmenuitem(itemcalendar, residemenu.direction_right);  

        residemenu.addmenuitem(itemsettings, residemenu.direction_right);  

8.設定title按鈕的點選事件,設定左右菜單的開關

// you can disable a direction by setting ->  

        // residemenu.setswipedirectiondisable(residemenu.direction_right);  

        findviewbyid(r.id.title_bar_left_menu).setonclicklistener(new view.onclicklistener() {  

            @override  

            public void onclick(view view) {  

                residemenu.openmenu(residemenu.direction_left);  

            }  

        });  

        findviewbyid(r.id.title_bar_right_menu).setonclicklistener(new view.onclicklistener() {  

                residemenu.openmenu(residemenu.direction_right);  

9.還重寫了dispatchtouchevent

@override  

    public boolean dispatchtouchevent(motionevent ev) {  

        return residemenu.dispatchtouchevent(ev);  

10.菜單關閉方法

residemenu.closemenu();  

11.屏蔽菜單方法

使用方法已經說完了,接下來,看看它的源碼,先看看源碼的項目結構。

Android仿QQ5.0側滑菜單ResideMenu源碼分析

很多人初學者都曾糾結,看源碼,如何從何看起,我個人建議從上面使用的順序看起,并且在看的時候要帶個問題去看去思考,這樣更容易了解。

上面的第一步是,建立residemenu對象,我們就看看residemenu的構造。

public residemenu(context context) {  

        super(context);  

        initviews(context);  

從上面代碼,看到構造裡面就一個初始化view,思考問題:如何初始化view及初始化了什麼view。

private void initviews(context context){  

        layoutinflater inflater = (layoutinflater)  

                context.getsystemservice(context.layout_inflater_service);  

        inflater.inflate(r.layout.residemenu, this);  

        scrollviewleftmenu = (scrollview) findviewbyid(r.id.sv_left_menu);  

        scrollviewrightmenu = (scrollview) findviewbyid(r.id.sv_right_menu);  

        imageviewshadow = (imageview) findviewbyid(r.id.iv_shadow);  

        layoutleftmenu = (linearlayout) findviewbyid(r.id.layout_left_menu);  

        layoutrightmenu = (linearlayout) findviewbyid(r.id.layout_right_menu);  

        imageviewbackground = (imageview) findviewbyid(r.id.iv_background);  

原理分析:從上面的代碼可以看到,加載了一個residemenu的布局,先看布局

<?xml version="1.0" encoding="utf-8"?>  

<framelayout xmlns:android="http://schemas.android.com/apk/res/android"  

              android:layout_width="match_parent"  

              android:layout_height="match_parent">  

    <imageview  

            android:id="@+id/iv_background"  

            android:adjustviewbounds="true"  

            android:scaletype="centercrop"  

            android:layout_width="match_parent"  

            android:layout_height="match_parent"/>  

            android:id="@+id/iv_shadow"  

            android:background="@drawable/shadow"  

            android:layout_width="fill_parent"  

            android:layout_height="fill_parent"  

            android:scaletype="fitxy"/>  

    <scrollview  

            android:id="@+id/sv_left_menu"  

            android:scrollbars="none"  

            android:paddingleft="30dp"  

            android:layout_width="150dp"  

            android:layout_height="fill_parent">  

        <linearlayout  

                android:id="@+id/layout_left_menu"  

                android:orientation="vertical"  

                android:layout_gravity="center_vertical"  

                android:layout_width="wrap_content"  

                android:layout_height="wrap_content">  

        </linearlayout>  

    </scrollview>  

            android:id="@+id/sv_right_menu"  

            android:paddingright="30dp"  

            android:layout_gravity="right">  

                android:id="@+id/layout_right_menu"  

                android:layout_height="wrap_content"  

                android:gravity="right">  

</framelayout>  

布局顯示效果

Android仿QQ5.0側滑菜單ResideMenu源碼分析

從布局檔案,以及顯示效果我們可以看到,它是一個幀布局,第一個imageview是背景,第二個imageview是.9的陰影效果的圖檔(看下面的圖),

兩個(scrollview包裹着一個linerlayout),可以從上面圖看到結構分别是左菜單和右菜單

<img src="http://img.blog.csdn.net/20140910100807704?watermark/2/text/ahr0cdovl2jsb2cuy3nkbi5uzxqvy3ltndkymji0mtaz/font/5a6l5l2t/fontsize/400/fill/i0jbqkfcma==/dissolve/70/gravity/southeast" style="font-family: arial; background-color: rgb(255, 255, 255);" alt="" />  

1.初始化布局以及布局檔案分析完畢,2.接下來是設定背景圖,初始化view的時候就已經拿到了背景控件,是以設定背景圖也是非常好實作的事情了。

public void setbackground(int imageresrouce){  

       imageviewbackground.setimageresource(imageresrouce);  

   }  

3.綁定activity,思考問題:它做了什麼?

/** 

  * use the method to set up the activity which residemenu need to show; 

  * 

  * @param activity 

  */  

 public void attachtoactivity(activity activity){  

     initvalue(activity);  

     setshadowadjustscalexbyorientation();  

     viewdecor.addview(this, 0);  

     setviewpadding();  

 }  

原理分析:綁定activity做了4件事情,分别是:

1.初始化參數:

private void initvalue(activity activity){  

    this.activity   = activity;  

    leftmenuitems   = new arraylist<residemenuitem>();  

    rightmenuitems  = new arraylist<residemenuitem>();  

    ignoredviews    = new arraylist<view>();  

    viewdecor = (viewgroup) activity.getwindow().getdecorview();  

    viewactivity = new touchdisableview(this.activity);  

    view mcontent   = viewdecor.getchildat(0);  

    viewdecor.removeviewat(0);  

    viewactivity.setcontent(mcontent);  

    addview(viewactivity);  

    viewgroup parent = (viewgroup) scrollviewleftmenu.getparent();  

    parent.removeview(scrollviewleftmenu);  

    parent.removeview(scrollviewrightmenu);  

}  

2.正對橫豎屏縮放比例進行調整

private void setshadowadjustscalexbyorientation(){  

      int orientation = getresources().getconfiguration().orientation;  

      if (orientation == configuration.orientation_landscape) {  

          shadowadjustscalex = 0.034f;  

          shadowadjustscaley = 0.12f;  

      } else if (orientation == configuration.orientation_portrait) {  

          shadowadjustscalex = 0.06f;  

          shadowadjustscaley = 0.07f;  

      }  

  }  

3.添加目前view

viewdecor.addview(this, 0);  

4.設定view邊距

     * we need the call the method before the menu show, because the 

     * padding of activity can't get at the moment of oncreateview(); 

     */  

    private void setviewpadding(){  

        this.setpadding(viewactivity.getpaddingleft(),  

                viewactivity.getpaddingtop(),  

                viewactivity.getpaddingright(),  

                viewactivity.getpaddingbottom());  

4.設定監聽,思考問題:它什麼時候調用監聽,原理分析:動畫監聽開始執行動畫掉哦那個openmenu動畫結束調用closemenu,從此我們可以想到,但它調用openmenu(int direction)和closemenu()都會設定這個監聽。

private animator.animatorlistener animationlistener = new animator.animatorlistener() {  

        @override  

        public void onanimationstart(animator animation) {  

            if (isopened()){  

                showscrollviewmenu();  

                if (menulistener != null)  

                    menulistener.openmenu();  

        }  

        public void onanimationend(animator animation) {  

            // reset the view;  

            if(isopened()){  

                viewactivity.settouchdisable(true);  

                viewactivity.setonclicklistener(viewactivityonclicklistener);  

            }else{  

                viewactivity.settouchdisable(false);  

                viewactivity.setonclicklistener(null);  

                hidescrollviewmenu();  

                    menulistener.closemenu();  

        public void onanimationcancel(animator animation) {  

        public void onanimationrepeat(animator animation) {  

    };  

5.設定内容縮放比例(0.1~1f),細心的同學會發現在當縮完成後還可以在往裡面拉到更小,有種彈性的感覺,挺有趣的。但是有些人的需求不想要有這種彈性效果,我們可以通過修改源碼修改這個彈性效果,找到gettargetscale這個方法,修改下面0.5這個數值。使用時設定了0.6的縮放比例,預設下面的彈性參數是0.5是以我們當縮完成後還可以在往裡面拉0.1的比例。

private float gettargetscale(float currentrawx){  

    float scalefloatx = ((currentrawx - lastrawx) / getscreenwidth()) * 0.75f;  

    scalefloatx = scaledirection == direction_right ? - scalefloatx : scalefloatx;  

    float targetscale = viewhelper.getscalex(viewactivity) - scalefloatx;  

    targetscale = targetscale > 1.0f ? 1.0f : targetscale;  

    targetscale = targetscale < 0.5f ? 0.5f : targetscale;  

    return targetscale;  

預設縮放比例:

//valid scale factor is between 0.0f and 1.0f.  

   private float mscalevalue = 0.5f;  

animatorset scaledown_activity = buildscaledownanimation(viewactivity, mscalevalue, mscalevalue);  

     * a helper method to build scale down animation; 

     * 

     * @param target 

     * @param targetscalex 

     * @param targetscaley 

     * @return 

    private animatorset buildscaledownanimation(view target,float targetscalex,float targetscaley){  

        animatorset scaledown = new animatorset();  

        scaledown.playtogether(  

                objectanimator.offloat(target, "scalex", targetscalex),  

                objectanimator.offloat(target, "scaley", targetscaley)  

        );  

        scaledown.setinterpolator(animationutils.loadinterpolator(activity,  

                android.r.anim.decelerate_interpolator));  

        scaledown.setduration(250);  

        return scaledown;  

6.建立子菜單,看下子菜單的構造,我們通過上面的學習,原理分析:我們可以猜測到,無非就是加載布局設定内容

public residemenuitem(context context, int icon, string title) {  

        iv_icon.setimageresource(icon);  

        tv_title.settext(title);  

    private void initviews(context context){  

        layoutinflater inflater=(layoutinflater) context.getsystemservice(context.layout_inflater_service);  

        inflater.inflate(r.layout.residemenu_item, this);  

        iv_icon = (imageview) findviewbyid(r.id.iv_icon);  

        tv_title = (textview) findviewbyid(r.id.tv_title);  

布局檔案:

[html] view

<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"  

              android:orientation="horizontal"  

              android:layout_height="wrap_content"  

              android:gravity="center_vertical"  

              android:paddingtop="30dp">  

            android:layout_width="30dp"  

            android:layout_height="30dp"  

            android:id="@+id/iv_icon"/>  

    <textview  

            android:layout_height="wrap_content"  

            android:textcolor="@android:color/white"  

            android:textsize="18sp"  

            android:layout_marginleft="10dp"  

            android:id="@+id/tv_title"/>  

</linearlayout>  

顯示效果圖:

Android仿QQ5.0側滑菜單ResideMenu源碼分析

7.子菜單添加到側換菜單中(可以看到它是通過常量來控制子菜單的添加位置)原理分析:根據不同的常量來區分添加不同菜單的子菜單

    * add a single items; 

    * 

    * @param menuitem 

    * @param direction 

    */  

   public void addmenuitem(residemenuitem menuitem, int direction){  

       if (direction == direction_left){  

           this.leftmenuitems.add(menuitem);  

           layoutleftmenu.addview(menuitem);  

       }else{  

           this.rightmenuitems.add(menuitem);  

           layoutrightmenu.addview(menuitem);  

       }  

8.設定title按鈕的點選事件,設定左右菜單的開關,原理分析:先設定了縮放方向然後在設定動畫,正如我們上面想的一樣還設定了動畫監聽。

 * show the reside menu; 

 */  

public void openmenu(int direction){  

    setscaledirection(direction);  

    isopened = true;  

    animatorset scaledown_activity = buildscaledownanimation(viewactivity, mscalevalue, mscalevalue);  

    animatorset scaledown_shadow = buildscaledownanimation(imageviewshadow,  

            mscalevalue + shadowadjustscalex, mscalevalue + shadowadjustscaley);  

    animatorset alpha_menu = buildmenuanimation(scrollviewmenu, 1.0f);  

    scaledown_shadow.addlistener(animationlistener);  

    scaledown_activity.playtogether(scaledown_shadow);  

    scaledown_activity.playtogether(alpha_menu);  

    scaledown_activity.start();  

設定縮放方向及計算x,y軸位置。

private void setscaledirection(int direction){  

    int screenwidth = getscreenwidth();  

    float pivotx;  

    float pivoty = getscreenheight() * 0.5f;  

    if (direction == direction_left){  

        scrollviewmenu = scrollviewleftmenu;  

        pivotx  = screenwidth * 1.5f;  

    }else{  

        scrollviewmenu = scrollviewrightmenu;  

        pivotx  = screenwidth * -0.5f;  

    viewhelper.setpivotx(viewactivity, pivotx);  

    viewhelper.setpivoty(viewactivity, pivoty);  

    viewhelper.setpivotx(imageviewshadow, pivotx);  

    viewhelper.setpivoty(imageviewshadow, pivoty);  

    scaledirection = direction;  

9.重寫dispatchtouchevent,問題思考:如何到根據手指滑動自動縮放

如果還不了解,dispatchtouchevent這個函數如何調用?什麼時候調用?請先看看http://blog.csdn.net/cym492224103/article/details/39179311

public boolean dispatchtouchevent(motionevent ev) {  

    float currentactivityscalex = viewhelper.getscalex(viewactivity);  

    if (currentactivityscalex == 1.0f)  

        setscaledirectionbyrawx(ev.getrawx());  

    switch (ev.getaction()){  

        case motionevent.action_down:  

            lastactiondownx = ev.getx();  

            lastactiondowny = ev.gety();  

            isinignoredview = isinignoredview(ev) && !isopened();  

            pressedstate    = pressed_down;  

            break;  

        case motionevent.action_move:  

            if (isinignoredview || isindisabledirection(scaledirection))  

                break;  

            if(pressedstate != pressed_down &&  

                    pressedstate != pressed_move_horizantal)  

            int xoffset = (int) (ev.getx() - lastactiondownx);  

            int yoffset = (int) (ev.gety() - lastactiondowny);  

            if(pressedstate == pressed_down) {  

                if(yoffset > 25 || yoffset < -25) {  

                    pressedstate = pressed_move_vertical;  

                    break;  

                }  

                if(xoffset < -50 || xoffset > 50) {  

                    pressedstate = pressed_move_horizantal;  

                    ev.setaction(motionevent.action_cancel);  

            } else if(pressedstate == pressed_move_horizantal) {  

                if (currentactivityscalex < 0.95)  

                    showscrollviewmenu();  

                float targetscale = gettargetscale(ev.getrawx());  

                viewhelper.setscalex(viewactivity, targetscale);  

                viewhelper.setscaley(viewactivity, targetscale);  

                viewhelper.setscalex(imageviewshadow, targetscale + shadowadjustscalex);  

                viewhelper.setscaley(imageviewshadow, targetscale + shadowadjustscaley);  

                viewhelper.setalpha(scrollviewmenu, (1 - targetscale) * 2.0f);  

                lastrawx = ev.getrawx();  

                return true;  

        case motionevent.action_up:  

            if (isinignoredview) break;  

            if (pressedstate != pressed_move_horizantal) break;  

            pressedstate = pressed_done;  

                if (currentactivityscalex > 0.56f)  

                    closemenu();  

                else  

                    openmenu(scaledirection);  

                if (currentactivityscalex < 0.94f){  

                }else{  

    lastrawx = ev.getrawx();  

    return super.dispatchtouchevent(ev);  

上面代碼量有點多,看上去有點暈,接下來我們來分别從按下、移動、放開、來原理分析:

motionevent.action_down:

記錄了x,y軸的坐标點,判斷是否打開,設定了按下的狀态為pressed_down

motionevent.action_move:

拿到目前x,y減去down下記錄下來的x,y,這樣得到了移動的x,y,

然後判斷如果如果移動的x,y大于25或者小于-25就改變按下狀态為pressed_move_vertical

如果移動的x,y大于50或者小于-50就改變狀态為pressed_move_horizantal

狀态為pressed_move_horizantal就改變菜單主視圖内容以及陰影圖檔大小,在改變的同時還設定了目前菜單的透明度。

motionevent.action_up:

判斷是否菜單是否打開狀态,在擷取目前縮放的x比例,

判斷比例小于0.56f,則關閉菜單,反正開啟菜單。

看完後,我們在回去看看代碼,就會發現其實也不過如此~!

10.菜單關閉方法,同樣也設定了動畫監聽之前的想法也是成立的。

    * close the reslide menu; 

   public void closemenu(){  

       isopened = false;  

       animatorset scaleup_activity = buildscaleupanimation(viewactivity, 1.0f, 1.0f);  

       animatorset scaleup_shadow = buildscaleupanimation(imageviewshadow, 1.0f, 1.0f);  

       animatorset alpha_menu = buildmenuanimation(scrollviewmenu, 0.0f);  

       scaleup_activity.addlistener(animationlistener);  

       scaleup_activity.playtogether(scaleup_shadow);  

       scaleup_activity.playtogether(alpha_menu);  

       scaleup_activity.start();  

public void setswipedirectiondisable(int direction){  

        disabledswipedirection.add(direction);  

private boolean isindisabledirection(int direction){  

        return disabledswipedirection.contains(direction);  

原理分析:在重寫dispatchtouchevent的時候,細心的同學應該會看到,action_move下面有個判斷

if (isinignoredview || isindisabledirection(scaledirection))  

如果這個方向的菜單被屏蔽了,就滑不出來了。

最後我們會發現我們一直都沒說到touchdisableview,其實initvalue的時候就初始化了,它就是viewactivity,是我們的内容視圖。

Android仿QQ5.0側滑菜單ResideMenu源碼分析

我們來看看它做了什麼?

    protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {  

        int width = getdefaultsize(0, widthmeasurespec);  

        int height = getdefaultsize(0, heightmeasurespec);  

        setmeasureddimension(width, height);  

        final int contentwidth = getchildmeasurespec(widthmeasurespec, 0, width);  

        final int contentheight = getchildmeasurespec(heightmeasurespec, 0, height);  

        mcontent.measure(contentwidth, contentheight);  

    protected void onlayout(boolean changed, int l, int t, int r, int b) {  

        final int width = r - l;  

        final int height = b - t;  

        mcontent.layout(0, 0, width, height);  

    public boolean onintercepttouchevent(motionevent ev) {  

        return mtouchdisabled;  

    void settouchdisable(boolean disabletouch) {  

        mtouchdisabled = disabletouch;  

    boolean istouchdisabled() {  

動态設定寬高,設定事件是否傳遞下去的flag。

繼續閱讀