先看效果:
導航抽屜:
- 導航抽屜一般顯示在屏幕最左側,默認情況下是隱藏的,當用戶手紙從邊緣向另一個滑動的時候,會出現一個隱藏的面板,當點擊面板外部或者原先方向滑動的時候,抽屜就消失。
- 很多 app 都有類似的需求,最經典的是 qq個人信息欄的滑動,后來 github 上開源出了民間的控件 SlideMenu。后來被Google 收錄進 support-v4包里面,命名為 DrawerLayout。
- NavigationView:是谷歌在側滑的 MaterialDesign 的一種規范,所以提出了一個新的控件,用來規范側滑的基本樣式。
- 使用 Eclipse 的同學在使用 NavigationView 的時候記得同時引用 RecyclerView 哦,不然會報錯,NavigationView的內部使用了 RecyclerView。
用法:
在創建項目的時候直接選擇 Navigation Drawer Activity 即可,之后我們便可以看到如下布局文件(直接手寫以下文件也行)
<?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>
效果如下:
最外層是一個 DrawerLayout,包含了兩個子 View。第一個 include 引用的 layout 為主頁內容區域。第二個NavigationView 為側滑區域View。
layout_gravity可以設置為 start 或者 end,分別對應的是從左邊滑出和從右邊滑出。
NavigationView 有兩個 app 屬性,分別是 app:headerLayout和 app:menu。前者是用于控制頭布局,查看資源文件 nav-header-main 可以看到:
查看 menu文件 activity-main-drawer我們可以看到如下代碼
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_camera"
android:icon="@drawable/ic_menu_camera"
android:title="Import" />
<item
android:id="@+id/nav_gallery"
android:icon="@drawable/ic_menu_gallery"
android:title="Gallery" />
<item
android:id="@+id/nav_slideshow"
android:icon="@drawable/ic_menu_slideshow"
android:title="Slideshow" />
<item
android:id="@+id/nav_manage"
android:icon="@drawable/ic_menu_manage"
android:title="Tools" />
</group>
<item android:title="Communicate">
<menu>
<item
android:id="@+id/nav_share"
android:icon="@drawable/ic_menu_share"
android:title="Share" />
<item
android:id="@+id/nav_send"
android:icon="@drawable/ic_menu_send"
android:title="Send" />
</menu>
</item>
</menu>
對應了側滑欄目的菜單。
Activity 里面的代碼
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
//使用 toolbar 替換 actionbar,不然 onCreateOptionsMenu無法生效到 toolbar 上
setSupportActionBar(toolbar);
//給 toolbar 設置導航剪頭,并綁定 DrawLayout,在滑動的時候執行動畫
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.setDrawerListener(toggle);
toggle.syncState();
//給NavigationView的菜單設置點擊事件,
//點擊事件處理之后調用drawer.closeDrawer(GravityCompat.START)關閉菜單
//onBackPressed()方法里面可以判斷 drawer.isDrawerOpen()來判斷執行動作
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
Tips:
- 如果在 xml 里面寫了toolbar,又在 activity 里面setSupportActionBar(toolbar);要記得給主題設置 android:theme="@style/AppTheme.NoActionBar"
- 如果想要 NavigationView 在 Toolbar 下方,可以在 DrawerLayout外層再包裹一個 LinearLayout,并且添加 Toolabr 節點即可
- Toolbar上不顯示Home旋轉開關按鈕,上文有注釋,刪除ActionBarDrawerToggle相關代碼即可。
- 不使用NavigationView,使用DrawerLayout+其他布局。很簡單,把上文中布局文件里面的 DrawerLayout 節點里面的 NavigationView替換成任意 View 或者 ViewGroup。
- fitsSystemWindows:控制控件是否填充狀態欄的位置,false 為不填充。
源碼分析
----NavigationView----
這是從 design 包的 Value 文件里面拷貝出來的自定義屬性,屬性命名很規范,我就不一個一個解釋了。
<declare-styleable name="NavigationView">
<attr name="android:background"/>
<attr name="android:fitsSystemWindows"/>
<attr name="android:maxWidth"/>
<attr name="elevation"/>
<attr format="reference" name="menu"/>
<attr format="color" name="itemIconTint"/>
<attr format="color" name="itemTextColor"/>
<attr format="reference" name="itemBackground"/>
<attr format="reference" name="itemTextAppearance"/>
<attr format="reference" name="headerLayout"/>
</declare-styleable>
NavigationView 繼承自 FrameLayout,然后透過配置headerLayout和menu來為其設置頭布局和菜單列表,接下來,我們就來看看源碼實現。
首先看構造方法
public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
// Custom attributes
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.NavigationView, defStyleAttr,
R.style.Widget_Design_NavigationView);
...//省略部分代碼
if (a.hasValue(R.styleable.NavigationView_menu)) {
inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
}
if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
}
a.recycle();
}
我們可以看到,如果attrs屬性包含 headerLayout 屬性和 menu 屬性,則會去加載。
inflateMenu(int resId)
public void inflateMenu(int resId) {
mPresenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, mMenu);
mPresenter.setUpdateSuspended(false);
mPresenter.updateMenuView(false);
}
這個方法很簡單,mPresenter.setUpdateSuspended()為防錯處理,暫時不用太糾結;
然后就是getMenuInflater().inflate(resId, mMenu)去解析menu 的 xml 屬性;
mPresenter.updateMenuView(false);這句話調用了更新 MenuView,追進去看代碼
@Override
public void updateMenuView(boolean cleared) {
if (mAdapter != null) {
mAdapter.update();
}
}
如果mAdapter不為 null,那么就更新mAdapter;根據代碼經驗,這個mAdapter一般是給 ListView 或者 RecyclerView 用的,查看了一下mAdapter這個類,果然繼承自 RecyclerView.Adapter.
然后我們再看mAdapter 的update()方法
public void update() {
prepareMenuItems();
notifyDataSetChanged();
}
這里調用了兩個方法,第二個方法我就不說了,看不懂的出門左拐。繼續追prepareMenuItems()
/**
* Flattens the visible menu items of {@link #mMenu} into {@link #mItems},
* while inserting separators between items when necessary.
*/
private void prepareMenuItems() {}
這個方法是 mAdapter 里面的一個私有方法,看方法說明,我們就能知道這個方法就是將mMenu里面的數據轉換成 mAdapter 需要的 NavigationMenuItem 數據,然后再走 Update 方法里面的notifyDataSetChanged()方法將數據刷新到界面上。
inflateHeaderView(@LayoutRes int res)
public View inflateHeaderView(@LayoutRes int res) {
return mPresenter.inflateHeaderView(res);
}
不多說了,直接追mPresenter.inflateHeaderView(res);
public View inflateHeaderView(@LayoutRes int res) {
View view = mLayoutInflater.inflate(res, mHeaderLayout, false);
addHeaderView(view);
return view;
}
public void addHeaderView(@NonNull View view) {
mHeaderLayout.addView(view);
// The padding on top should be cleared.
mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom());
}
直接調用LayoutInflater去inflate一個 LayoutRes 文件得到一個 view,然后添加進 mHeaderLayout里面,源碼里面方法注釋都懶得寫,我也不過多贅述了。
好,NavigationView 核心代碼分析完畢。
----DrawerLayout----
看 activity_main.xml的布局文件我們可以知道,DrawerLayout是 ContentView 和 NavigationView 的父節點,然后根據命名,我們可以大膽的猜測,DrawerLayout就是處理側滑效果的。汗。。。。。。其實就是一個處理側滑的 view
先看類注釋說明吧,看不懂直接看后面的翻譯~~
/**
* DrawerLayout acts as a top-level container for window content that allows for
* interactive "drawer" views to be pulled out from one or both vertical edges of the window.
*
* <p>Drawer positioning and layout is controlled using the <code>android:layout_gravity</code>
* attribute on child views corresponding to which side of the view you want the drawer
* to emerge from: left or right (or start/end on platform versions that support layout direction.)
* Note that you can only have one drawer view for each vertical edge of the window. If your
* layout configures more than one drawer view per vertical edge of the window, an exception will
* be thrown at runtime.
* </p>
*
* <p>To use a DrawerLayout, position your primary content view as the first child with
* width and height of <code>match_parent</code> and no <code>layout_gravity></code>.
* Add drawers as child views after the main content view and set the <code>layout_gravity</code>
* appropriately. Drawers commonly use <code>match_parent</code> for height with a fixed width.</p>
*
* <p>{@link DrawerListener} can be used to monitor the state and motion of drawer views.
* Avoid performing expensive operations such as layout during animation as it can cause
* stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state.
* {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.</p>
*
* <p>As per the <a href="{@docRoot}design/patterns/navigation-drawer.html">Android Design
* guide</a>, any drawers positioned to the left/start should
* always contain content for navigating around the application, whereas any drawers
* positioned to the right/end should always contain actions to take on the current content.
* This preserves the same navigation left, actions right structure present in the Action Bar
* and elsewhere.</p>
*
* <p>For more information about how to use DrawerLayout, read <a
* href="{@docRoot}training/implementing-navigation/nav-drawer.html">Creating a Navigation
* Drawer</a>.</p>
*/
類注釋說明很長,我用我三級的蹩腳英語結合翻譯工具給大家簡單翻譯一下
- 可以作為一個從左右兩邊拉出抽屜效果的頂層容器
- 抽屜的位置取決于 layout_gravity屬性。注意:每個垂直邊最多只能有一個抽屜,否則會在運行的時候拋出異常
- 使用 DrawerLayout 的時候,主要的內容 view 必須放在第一個位置,寬和高為 match_parent 并且不能有 layout_gravity 屬性;抽屜 view 設置在主內容 view 之后并且必須設置 layout_gravity,抽屜 view 的高度為 match_parent,寬度設為固定值。
- DrawerListener可以用來監聽抽屜的狀態和滑動,避免在滑動過程中執行高消耗的行為,STATE_IDLE狀態下可以進行性能消耗比較大的動作。
接下來,我們來看 DrawerLayout 是怎么來控制抽屜滑動的。
在 DrawerLayout 的構造方法里面,我找到了一個熟悉的類--ViewDragHelper,熟悉 ViewDragHelper 這個類的童鞋看到這里可以不用往下看了,對,沒錯,DrawerLayout 的內部實現就是基于 ViewDragHelper。
mLeftCallback = new ViewDragCallback(Gravity.LEFT);
mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback);
mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
mLeftDragger.setMinVelocity(minVel);
mLeftCallback.setDragger(mLeftDragger);
這里是控制左邊抽屜拖動的關鍵代碼,與之相同的還有右邊抽屜的處理。
這里就是用了 ViewDragHelper 這個類來處理 contentView 的觸摸滑動來拖動抽屜。
這里,我就簡單講一下ViewDragHelper這個類吧
/**
* ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
* of useful operations and state tracking for allowing a user to drag and reposition
* views within their parent ViewGroup.
*/
ViewDragHelper是一個編寫自定義 ViewGroup 的實用類,它提供一個用于追蹤view拖動事件的參數。
翻譯得有點拗口,簡單點就是在 ViewGroup 里面監聽一個 View 的拖動。
ViewGroup 的使用很簡單,就三步
1.調用靜態方法create(ViewGroup forParent, float sensitivity, Callback cb)創建實力,第一個參數傳 ViewGroup 本身,第二個參數是拖動的敏感度,一般用1F 即可,第三個參數后文單獨說。
2.在 onTouch 和 onInterceptTouchEvent 方法里面做如下處理
@Override
public boolean onInterceptTouchEvent(MotionEvent event)
{
return mDragger.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
mDragger.processTouchEvent(event);
return true;
}
3.實現 ViewDragHelper.Callback類,
/**
* Called when the drag state changes. See the <code>STATE_*</code> constants
* for more information.
* 當ViewDragHelper狀態發生變化時回調(IDLE,DRAGGING,SETTING[自動滾動時])
* @param state The new drag state
*
* @see #STATE_IDLE
* @see #STATE_DRAGGING
* @see #STATE_SETTLING
*/
public void onViewDragStateChanged(int state) {}
/**
* Called when the captured view's position changes as the result of a drag or settle.
* 當captureview的位置發生改變時回調
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
/**
* Called when a child view is captured for dragging or settling. The ID of the pointer
* currently dragging the captured view is supplied. If activePointerId is
* identified as {@link #INVALID_POINTER} the capture is programmatic instead of
* pointer-initiated.
* 當captureview被捕獲時回調
* @param capturedChild Child view that was captured
* @param activePointerId Pointer id tracking the child capture
*/
public void onViewCaptured(View capturedChild, int activePointerId) {}
/**
* Called when the child view is no longer being actively dragged.
* The fling velocity is also supplied, if relevant. The velocity values may
* be clamped to system minimums or maximums.
*
* <p>Calling code may decide to fling or otherwise release the view to let it
* settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
* one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
* and the view capture will not fully end until it comes to a complete stop.
* If neither of these methods is invoked before <code>onViewReleased</code> returns,
* the view will stop in place and the ViewDragHelper will return to
* {@link #STATE_IDLE}.</p>
* 手指釋放的時候回調
* @param releasedChild The captured child view now being released
* @param xvel X velocity of the pointer as it left the screen in pixels per second.
* @param yvel Y velocity of the pointer as it left the screen in pixels per second.
*/
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
/**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
* 當觸摸到邊界時回調。
* @param edgeFlags A combination of edge flags describing the edge(s) currently touched
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/**
* Called when the given edge may become locked. This can happen if an edge drag
* was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
* was called. This method should return true to lock this edge or false to leave it
* unlocked. The default behavior is to leave edges unlocked.
* true的時候會鎖住當前的邊界,false則unLock。
* @param edgeFlags A combination of edge flags describing the edge(s) locked
* @return true to lock the edge, false to leave it unlocked
*/
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
* Called when the user has started a deliberate drag away from one
* of the subscribed edges in the parent view while no child view is currently captured.
* 在邊界拖動時回調
* @param edgeFlags A combination of edge flags describing the edge(s) dragged
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
/**
* Called to determine the Z-order of child views.
* 這個沒看懂,沒用過
* @param index the ordered position to query for
* @return index of the view that should be ordered at position <code>index</code>
*/
public int getOrderedChildIndex(int index) {
return index;
}
/**
* Return the magnitude of a draggable child view's horizontal range of motion in pixels.
* This method should return 0 for views that cannot move horizontally.
* 獲取目標 view 水平方向拖動的距離
* @param child Child view to check
* @return range of horizontal motion in pixels
*/
public int getViewHorizontalDragRange(View child) {
return 0;
}
/**
* Return the magnitude of a draggable child view's vertical range of motion in pixels.
* This method should return 0 for views that cannot move vertically.
* 獲取目標 view 垂直方向拖動的距離
* @param child Child view to check
* @return range of vertical motion in pixels
*/
public int getViewVerticalDragRange(View child) {
return 0;
}
/**
* Called when the user's input indicates that they want to capture the given child view
* with the pointer indicated by pointerId. The callback should return true if the user
* is permitted to drag the given view with the indicated pointer.
*
* <p>ViewDragHelper may call this method multiple times for the same view even if
* the view is already captured; this indicates that a new pointer is trying to take
* control of the view.</p>
*
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
* 如果返回 true,則捕獲該 view 的拖動事件。通常寫法 return child == targeView;
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/
public abstract boolean tryCaptureView(View child, int pointerId);
/**
* Restrict the motion of the dragged child view along the horizontal axis.
* The default implementation does not allow horizontal motion; the extending
* class must override this method and provide the desired clamping.
* 控制 child移動的水平邊界
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
/**
* Restrict the motion of the dragged child view along the vertical axis.
* The default implementation does not allow vertical motion; the extending
* class must override this method and provide the desired clamping.
* 控制 child 移動的垂直邊界
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
就到這里吧,ViewDragHelper 的用法其實很簡單,DrawerLayout 里面也是這三個步驟,追過加了一些邏輯處理而已,詳細用法可以參看鴻洋大神的 blog 《Android ViewDragHelper完全解析 自定義ViewGroup神器》,看完之后再回過頭來自己去捋一捋 DrawerLayout 里面的邏輯