从源码分析design包中的侧滑导航栏(Naviogation Drawer )的实现

我们知道google 的android 的design 包中新增了许多的新特性和比较炫的布局的封装,而且通过新版本的androidstudio我们可以直接直接创建这些带功能的工程。极其的方便。看一下有哪些:
这里写图片描述
可以看到样式非常的多,非常方便。其中Naviogation Drawer 就是我们今天需要描述的。
今天我们针对侧滑的这个design通过源码简单分析一下之间的关联和实现的方式。首先我们来看一下gif图:
这里写图片描述
嗯就是这个效果。接下来我们看看谷歌到底帮我们做了哪些操作呢。看代码:
先看Activity 中的代码:

package com.szh.designsimple;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.view.View;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        //这句话创建toolbar右侧菜单的关键 只有这样才会执行onCreateOptionsMenu,才会创建菜单
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });

        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        //创建左侧toolbar按钮并且关联导航栏
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this, drawer, toolbar, R.string.action_settings, R.string.navigation_drawer_close);
        drawer.setDrawerListener(toggle);
        toggle.syncState();

//        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
//        navigationView.setNavigationItemSelectedListener(this);
    }

    @Override
    public void onBackPressed() {
    //返回按钮的监听
        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        if (drawer.isDrawerOpen(GravityCompat.START)) {
            drawer.closeDrawer(GravityCompat.START);
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    //创建菜单
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    //菜单中item的选中事件
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @SuppressWarnings("StatementWithEmptyBody")
    @Override
    public boolean onNavigationItemSelected(MenuItem item) {
    //导航栏菜单的选中事件
        // Handle navigation view item clicks here.
        int id = item.getItemId();

        if (id == R.id.nav_camera) {
            // Handle the camera action
        } else if (id == R.id.nav_gallery) {

        } else if (id == R.id.nav_slideshow) {

        } else if (id == R.id.nav_manage) {

        } else if (id == R.id.nav_share) {

        } else if (id == R.id.nav_send) {

        }

        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        drawer.closeDrawer(GravityCompat.END);
        return true;
    }
}

接下来是 activity 中的xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include
        layout="@layout/app_bar_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />


    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/nav_header_main"
        app:menu="@menu/activity_main_drawer" />

</android.support.v4.widget.DrawerLayout>

也非常简单:其中用到了include引用,那我们也看下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.szh.designsimple.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

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

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

可以看到这里面就是有关于toolbar的定义。
到这里关键性的生成代码我们已经看完了。最主要的我们知道
1.顶部的toolbar 这个我们在布局里看到了。
2.左侧的导航栏。这个我们也知道在mian 布局里有个 android.support.design.widget.NavigationView 这个控件很明显是design包中的导航栏。
看到这里我们还有疑问:toolbar 的左侧导航按钮和右侧的菜单是哪来的呢?这些我都在activity 代码中写了注释可以看一下。就那几行代码。这个可以理解。但是还有个很大的问题左侧导航按钮点击后弹出侧导航的事件在哪呢?的确,这部分我们就看这些生成的代码是无法找到的。我们可以想一下关键部分:
1.首先代码中用到了ActionBarDrawerToggle 这个,构造这个时候需要好几个参数呢。其中有两个我们很关注。一个是toolbar一个是drawerlayout 这两个参数很明显我们看下源码‘:

/**
      如果我们允许开发者自定义动画的话,在将来我们将会开放这个构造方法 。所以说目前我们只能用默认的
     * In the future, we can make this constructor public if we want to let developers customize
     * the
     * animation.
     */
    ActionBarDrawerToggle(Activity activity, Toolbar toolbar, DrawerLayout drawerLayout,
            DrawerArrowDrawable slider, @StringRes int openDrawerContentDescRes,
            @StringRes int closeDrawerContentDescRes) {
        if (toolbar != null) {
            mActivityImpl = new ToolbarCompatDelegate(toolbar);
            toolbar.setNavigationOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mDrawerIndicatorEnabled) {
                        toggle();
                    } else if (mToolbarNavigationClickListener != null) {
                        mToolbarNavigationClickListener.onClick(v);
                    }
                }
            });
        } else if (activity instanceof DelegateProvider) { // Allow the Activity to provide an impl
            mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mActivityImpl = new JellybeanMr2Delegate(activity);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            mActivityImpl = new HoneycombDelegate(activity);
        } else {
            mActivityImpl = new DummyDelegate(activity);
        }

        mDrawerLayout = drawerLayout;
        mOpenDrawerContentDescRes = openDrawerContentDescRes;
        mCloseDrawerContentDescRes = closeDrawerContentDescRes;
        if (slider == null) {
            mSlider = new DrawerArrowDrawable(mActivityImpl.getActionBarThemedContext());
        } else {
            mSlider = slider;
        }

        mHomeAsUpIndicator = getThemeUpIndicator();
    }

先看到第10行,首先判断了toolbar 是否为空,这里我们明显不为空。12行开始给这个toolbar 设置了setNavigationOnClickListener这个很明显就是左侧导航栏的点击监听事件。是不是一下就找到了。好先不急,那我们再看看他是如何实现的呢。继续看源码15行做了判断,这个mDrawerIndicatorEnabled是个boolean 类型。并且在源码中我们可以看到默认就是true,除非我们手动设置过不然就是true。所以接下来执行的就是16行的 toggle()。那么 toggle里面做了什么呢?我们继续看源码:

void toggle() {
        int drawerLockMode = mDrawerLayout.getDrawerLockMode(GravityCompat.START);
        if (mDrawerLayout.isDrawerVisible(GravityCompat.START)
                && (drawerLockMode != DrawerLayout.LOCK_MODE_LOCKED_OPEN)) {
            mDrawerLayout.closeDrawer(GravityCompat.START);
        } else if (drawerLockMode != DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
            mDrawerLayout.openDrawer(GravityCompat.START);
        }
    }

可以看出很简单只做两种操作一个是 mDrawerLayout.closeDrawer(GravityCompat.START)另一个是 mDrawerLayout.openDrawer(GravityCompat.START)很明显就是显示和关闭两个操作。其中的mDrawerLayout其实就是我们一开始构造方法传入的根布局。那我们接着看是如何打开的呢?继续源码:

  /**
     * Open the specified drawer by animating it out of view.
     *
     * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right.
     *                GravityCompat.START or GravityCompat.END may also be used.
     */
    public void openDrawer(@EdgeGravity int gravity) {
        openDrawer(gravity, true);
    }

    /**
     * Open the specified drawer.
     *
     * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right.
     *                GravityCompat.START or GravityCompat.END may also be used.
     * @param animate Whether opening of the drawer should be animated.
     */
    public void openDrawer(@EdgeGravity int gravity, boolean animate) {
        final View drawerView = findDrawerWithGravity(gravity);
        if (drawerView == null) {
            throw new IllegalArgumentException("No drawer view found with gravity "
                    + gravityToString(gravity));
        }
        openDrawer(drawerView, animate);
    }

看到19行 这里会获得一个view。那么这个view 怎么来的呢。并且我们看到如果这个view为null 会抛出异常。那么看下怎么来的:

 /**
     * @param gravity the gravity of the child to return. If specified as a
     *            relative value, it will be resolved according to the current
     *            layout direction.
     * @return the drawer with the specified gravity
     */
    View findDrawerWithGravity(int gravity) {
        final int absHorizGravity = GravityCompat.getAbsoluteGravity(
                gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK;
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final int childAbsGravity = getDrawerViewAbsoluteGravity(child);
            if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) {
                return child;
            }
        }
        return null;
    }

很明显15行返回是一个子view ,并且这个子view的条件是 他的Garvity属性是 和根布局的gravity 是否相等。相等的就返回:根布局对应的是这句中的参数 tools:opendrawer=”left” 和子布局中 android:layout_gravity=”left” 这个中的参数保持一致的view 才会被返回,但是如果我有多个view都设置了这个属性呢?通过我的代码的调试发现如果有多个是会报错的。因为只允许一个。如果这个控件不是NavigationView 的话。也不会出现toolbar上的导航图片,但是控件是在的并且能点击,但是没效果。我们可以看一下:
这里写图片描述
很明显左上角图标没了。但是效果是在的。所以说图标还是和这个控件相关联的。
好了找到这个view 了。接下来是看下如何展看的呢。接着看源码:

  /**
     * Open the specified drawer view.
     *
     * @param drawerView Drawer view to open
     * @param animate Whether opening of the drawer should be animated.
     */
    public void openDrawer(View drawerView, boolean animate) {
        if (!isDrawerView(drawerView)) {
            throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
        }

        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        if (mFirstLayout) {
            lp.onScreen = 1.f;
            lp.openState = LayoutParams.FLAG_IS_OPENED;

            updateChildrenImportantForAccessibility(drawerView, true);
        } else if (animate) {
            lp.openState |= LayoutParams.FLAG_IS_OPENING;

            if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
                mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop());
            } else {
                mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(),
                        drawerView.getTop());
            }
        } else {
            moveDrawerToOffset(drawerView, 1.f);
            updateDrawerState(lp.gravity, STATE_IDLE, drawerView);
            drawerView.setVisibility(VISIBLE);
        }
        invalidate();
    }

我们看到第8行首先判断是不是一个drawerView 不是我们直接抛出异常。接下来一些判断如果animate为true 也就是有动画需要,这个是默认的就是true 那我们就根据控件的滑动方向,是像左平滑的滑动还是右平滑的滑动。然后 invalidate();重新绘制页面,这样就出来最终的效果。所以整个过程我们也分析完了。我们知道怎么关联起来的了。所以这个根布局很关键。也就是android.support.v4.widget.DrawerLayout 所有的处理逻辑都是在这个控件中完成的。有兴趣的同学可以详细看下这个源码。
分析到这里我们可以简单做一下扩充:实现左滑和右滑分别从左边和右边弹出菜单栏。先上效果图:
这里写图片描述

可以看出我们随着我们的手势能够成功的弹出菜单。
接下来我们看下代码实现:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action=ev.getAction();
        switch(action){
            case MotionEvent.ACTION_DOWN:
                startX=(int)ev.getX();
                break;

            case MotionEvent.ACTION_MOVE:

                break;

            case MotionEvent.ACTION_UP:
                int screenwidth=getWindowWidth()/3;
                if(ev.getX()-startX>=screenwidth){
                    drawer.openDrawer(GravityCompat.START);
                }else if(startX-ev.getX()>=screenwidth){
                    drawer.openDrawer(GravityCompat.END);
                }
                break;

            case MotionEvent.ACTION_CANCEL:

                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    private int getWindowWidth(){
        WindowManager wm = this.getWindowManager();

        return  wm.getDefaultDisplay().getWidth();
    }

首先activity中需要重写滑动事件计算按下和抬起收拾的X轴的位移差,并且和屏幕的宽度做比较,当大于三分之一时我们直接通过 drawer.openDrawer(GravityCompat.START);代码实现弹出的效果。因为我们分析源码得知,弹出效果就是这句代码的实现。
接下来我们在main。xml中添加一个右滑出来的NavigationView:

<android.support.design.widget.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="right"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/nav_header_main"
        app:menu="@menu/activity_main_drawer" />

好了到这我们扩充的功能也实现了。当然我们还能实现更多的扩展。这里就不详细描述了。大家可以自由发挥。
下面是项目源码有需要的可以看一下:https://github.com/MrHangVIP/DesignSimple.git

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值