DrawerLayout側滑菜單沉浸式分析
接着android6.0 SystemUi分析,來分析一下drawerlayout
DrawerLayout要想到達側滑菜單沉浸式,就需要在DrawerLayout布局中加入:
android:fitsSystemWindows="true"
這樣系統在向下傳遞insets時就會傳遞給DrawerLayout。
DrawerLayout在構造函數中做了一些特殊特處理:
public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { ... if (ViewCompat.getFitsSystemWindows(this)) { IMPL.configureApplyInsets(this); mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context); } ... }
IMPL.configureApplyInsets(this);
對api21以下的不做任何處理(即空方法),
對api21及以上的:
DrawerLayoutCompatApi21:
public static void configureApplyInsets(View drawerLayout) { if (drawerLayout instanceof DrawerLayoutImpl) { drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener()); drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } }
可見DrawerLayout會自動加入View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN。那么mContentRoot就不會消費。
View.setOnApplyWindowInsetsListener會使在根view向下傳遞insets時不調用view的onApplyWindowInsets,而調用監聽的onApplyWindowInsets。
static class InsetsListener implements View.OnApplyWindowInsetsListener { @Override public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { final DrawerLayoutImpl drawerLayout = (DrawerLayoutImpl) v; drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0); // 直接consume掉 return insets.consumeSystemWindowInsets(); } }
DrawerLayout:
@Override public void setChildInsets(Object insets, boolean draw) { mLastInsets = insets; mDrawStatusBarBackground = draw; setWillNotDraw(!draw && getBackground() == null); requestLayout(); }
在setChildInsets中會把insets保存下來,
用在DrawerLayout.onMeasure時使用:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... // 判斷是否要處理insets final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this); ... // 會對drawerlayout的content和drawer都應用insets for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (applyInsets) { final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection); // 如果子view設置FitsSystemWindows為true則會傳遞給子view讓其消費, // 如果是false,則會對子view設置margin值來適配systemui(statusbar等) if (ViewCompat.getFitsSystemWindows(child)) { IMPL.dispatchChildInsets(child, mLastInsets, cgrav); } else { IMPL.applyMarginInsets(lp, mLastInsets, cgrav); } } ... } }
下邊的兩個方法也是只對api21以上的有處理邏輯
IMPL.dispatchChildInsets
public static void dispatchChildInsets(View child, Object insets, int gravity) { WindowInsets wi = (WindowInsets) insets; if (gravity == Gravity.LEFT) { wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); } else if (gravity == Gravity.RIGHT) { wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); } child.dispatchApplyWindowInsets(wi); }
IMPL.applyMarginInsets
public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets, int gravity) { WindowInsets wi = (WindowInsets) insets; if (gravity == Gravity.LEFT) { wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); } else if (gravity == Gravity.RIGHT) { wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); } lp.leftMargin = wi.getSystemWindowInsetLeft(); lp.topMargin = wi.getSystemWindowInsetTop(); lp.rightMargin = wi.getSystemWindowInsetRight(); lp.bottomMargin = wi.getSystemWindowInsetBottom(); }
由上可知如果Drawerlayout的drawer是一個ListView/RecyclerView的話,設置如下屬性:
android:clipToPadding="false"
android:fitsSystemWindows="true"
就可以是抽屜view在滑出的時候達到沉浸式的效果。
NavigationView沉浸式分析
<android.support.design.widget.NavigationView android:id="@+id/navigation" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:background="@color/white" app:headerLayout="@layout/nav_header" app:menu="@menu/activity_main_drawer" />
如果在DrawerLayout中使用NavigationView的話不用加入
android:clipToPadding="false"
android:fitsSystemWindows="true"
也可以直接使用,原因肯定是內部進行了設置。
NavigationView extends ScrimInsetsFrameLayout
ScrimInsetsFrameLayout extends FrameLayout
在NavigationView 構造函數中:
ViewCompat.setFitsSystemWindows(this, a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false));
系統默認會設置fitsSystemWindows為true。
在ScrimInsetsFrameLayout 構造函數中:
ViewCompat.setOnApplyWindowInsetsListener(this, new android.support.v4.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { if (null == mInsets) { mInsets = new Rect(); } mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); onInsetsChanged(insets); setWillNotDraw(mInsets.isEmpty() || mInsetForeground == null); ViewCompat.postInvalidateOnAnimation(ScrimInsetsFrameLayout.this); return insets.consumeSystemWindowInsets(); } });
對insets的dispatch做了監聽,那么接着上邊的dispatchChildInsets,
會調用ScrimInsetsFrameLayout 監聽的onApplyWindowInsets方法。
在NavigationView .onInsetsChanged中
@Override protected void onInsetsChanged(WindowInsetsCompat insets) { mPresenter.dispatchApplyWindowInsets(insets); }
NavigationMenuPresenter會對側滑菜單中的header和menu進行初始化和創建添加view。
包含兩個view:
LinearLayout mHeaderLayout;
NavigationMenuView mMenuView;
mMenuView實際上是RecyclerView,他的布局是:
<android.support.design.internal.NavigationMenuView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/design_navigation_view" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/design_navigation_padding_bottom" android:clipToPadding="false" android:scrollbars="vertical"/>
mHeaderLayout就是mMenuView中viewType為VIEW_TYPE_HEADER的itemview。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/navigation_header_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingBottom="@dimen/design_navigation_separator_vertical_padding" />
NavigationMenuPresenter:
public void dispatchApplyWindowInsets(WindowInsetsCompat insets) { int top = insets.getSystemWindowInsetTop(); if (mPaddingTopDefault != top) { mPaddingTopDefault = top; if (mHeaderLayout.getChildCount() == 0) { mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom()); } } ViewCompat.dispatchApplyWindowInsets(mHeaderLayout, insets); }
可以看到mHeaderLayout.getChildCount() == 0的判斷肯定為false,所以就直接把insets傳遞給headerview,
而headerview也是沒有設置fitsSystemWindows,所以也不會處理,
也就是說沒有view消費insets,所以其實上邊clipToPadding可以不設置為false也行。
看起來好像並沒有對insets進行消費,那為什么還能達到沉浸效果,主要是因為DrawerLayout在初始化時就設置了可以擴展到狀態欄,但如果drawer不設置fitsystemwindow=true,就會交給DrawerLayout用margin進行處理了,所以用fitsystemwindow=true拿來讓NavigationView進行處理,NavigationView不處理就可以。