DrawerLayout

时间:2021-09-17 07:08:34
 
 
一、概述
 
    DrawerLayout是官方提供的侧滑菜单,相比SliddingMenu,它更加轻量级。默认情况下,DrawerLayout可以设置左侧或者右侧滑出菜单。如下,
 

xml布局:

  1. <!--
  2. <!-- A DrawerLayout is intended to be used as the top-level content view using match_parent for both width and height to consume the full space available. -->
  3. <com.sys.app.uikit.drawerlayoutplus.DrawerLayoutPlus xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:id="@+id/drawer_layout"
  5. android:layout_width= "match_parent"
  6. android:layout_height= "match_parent" >
  7. <!--
  8. As the main content view, the view below consumes the entire
  9. space available using match_parent in both dimensions.
  10. -->
  11. <FrameLayout
  12. android:id="@+id/content_frame"
  13. android:layout_width="match_parent"
  14. android:layout_height="match_parent" />
  15. <!--
  16. android:layout_gravity="start" tells DrawerLayout to treat
  17. this as a sliding drawer on the left side for left-to-right
  18. languages and on the right side for right-to-left languages.
  19. The drawer is given a fixed width in dp and extends the full height of
  20. the container. A solid background is used for contrast
  21. with the content view.
  22. -->
  23. <!-- Left drawer -->
  24. <ListView
  25. android:id="@+id/left_drawer"
  26. android:layout_width="240dp"
  27. android:layout_height="match_parent"
  28. android:layout_gravity="left"
  29. android:background="#111"
  30. android:choiceMode="singleChoice"
  31. android:divider="@android:color/transparent"
  32. android:dividerHeight="0dp" />
  33. <!-- Right drawer -->
  34. <ListView
  35. android:id="@+id/right_drawer"
  36. android:layout_width="match_parent"
  37. android:layout_height="match_parent"
  38. android:layout_gravity="right"
  39. android:choiceMode="singleChoice" />
  40. </com.sys.app.uikit.drawerlayoutplus.DrawerLayoutPlus>
Activity代码:
  1. package com.sys.app.uikit.drawerlayoutplus;
  2. import java.util.Locale;
  3. import android.app.Activity;
  4. import android.app.Fragment;
  5. import android.app.FragmentManager;
  6. import android.app.SearchManager;
  7. import android.content.Intent;
  8. import android.content.res.Configuration;
  9. import android.os.Bundle;
  10. import android.support.v4.view.GravityCompat;
  11. import android.view.LayoutInflater;
  12. import android.view.Menu;
  13. import android.view.MenuInflater;
  14. import android.view.MenuItem;
  15. import android.view.View;
  16. import android.view.ViewGroup;
  17. import android.widget.AdapterView;
  18. import android.widget.ArrayAdapter;
  19. import android.widget.ImageView;
  20. import android.widget.ListView;
  21. import android.widget.Toast;
  22. public class MainActivity extends Activity {
  23. private DrawerLayoutPlus mDrawerLayout;
  24. private ListView mLeftDrawerList, mRightDrawerList;
  25. private ActionBarDrawerToggle mDrawerToggle;
  26. private CharSequence mDrawerTitle;
  27. private CharSequence mTitle;
  28. private String[] mPlanetTitles;
  29. @Override
  30. protected void onCreate(Bundle savedInstanceState) {
  31. super.onCreate(savedInstanceState);
  32. setContentView(R.layout.activity_main);
  33. mTitle = mDrawerTitle = getTitle();
  34. mPlanetTitles = getResources().getStringArray(R.array.planets_array);
  35. mDrawerLayout = (DrawerLayoutPlus) findViewById(R.id.drawer_layout);
  36. mLeftDrawerList = (ListView) findViewById(R.id.left_drawer);
  37. mRightDrawerList = (ListView) findViewById(R.id.right_drawer);
  38. // set a custom shadow that overlays the main content when the drawer
  39. // opens
  40. mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
  41. // set up the drawer's list view with items and click listener
  42. mLeftDrawerList.setAdapter(new ArrayAdapter<String>(this, R.layout.drawer_list_item, mPlanetTitles));
  43. mLeftDrawerList.setOnItemClickListener(new DrawerItemClickListener());
  44. mRightDrawerList.setAdapter(new ArrayAdapter<String>(this, R.layout.drawer_list_item, mPlanetTitles));
  45. mRightDrawerList.setOnItemClickListener(new DrawerItemClickListener());
  46. // enable ActionBar app icon to behave as action to toggle nav drawer
  47. getActionBar().setDisplayHomeAsUpEnabled(true);
  48. getActionBar().setHomeButtonEnabled(true);
  49. // ActionBarDrawerToggle ties together the the proper interactions
  50. // between the sliding drawer and the action bar app icon
  51. mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */
  52. mDrawerLayout, /* DrawerLayout object */
  53. R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */
  54. R.string.drawer_open, /* "open drawer" description for accessibility */
  55. R.string.drawer_close /* "close drawer" description for accessibility */
  56. ) {
  57. public void onDrawerClosed(View view) {
  58. getActionBar().setTitle(mTitle);
  59. invalidateOptionsMenu(); // creates call to
  60. // onPrepareOptionsMenu()
  61. }
  62. public void onDrawerOpened(View drawerView) {
  63. getActionBar().setTitle(mDrawerTitle);
  64. invalidateOptionsMenu(); // creates call to
  65. // onPrepareOptionsMenu()
  66. }
  67. };
  68. mDrawerLayout.setDrawerListener(mDrawerToggle);
  69. if (savedInstanceState == null) {
  70. selectItem(0);
  71. }
  72. }
  73. @Override
  74. public boolean onCreateOptionsMenu(Menu menu) {
  75. MenuInflater inflater = getMenuInflater();
  76. inflater.inflate(R.menu.main, menu);
  77. return super.onCreateOptionsMenu(menu);
  78. }
  79. /* Called whenever we call invalidateOptionsMenu() */
  80. @Override
  81. public boolean onPrepareOptionsMenu(Menu menu) {
  82. // If the nav drawer is open, hide action items related to the content
  83. // view
  84. boolean drawerLeftOpen = mDrawerLayout.isDrawerOpen(mLeftDrawerList);
  85. boolean drawerRightOpen = mDrawerLayout.isDrawerOpen(mRightDrawerList);
  86. menu.findItem(R.id.action_websearch).setVisible(!(drawerLeftOpen && drawerRightOpen));
  87. return super.onPrepareOptionsMenu(menu);
  88. }
  89. @Override
  90. public boolean onOptionsItemSelected(MenuItem item) {
  91. // The action bar home/up action should open or close the drawer.
  92. // ActionBarDrawerToggle will take care of this.
  93. if (mDrawerToggle.onOptionsItemSelected(item)) {
  94. return true;
  95. }
  96. // Handle action buttons
  97. switch (item.getItemId()) {
  98. case R.id.action_websearch:
  99. // create intent to perform web search for this planet
  100. Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
  101. intent.putExtra(SearchManager.QUERY, getActionBar().getTitle());
  102. // catch event that there's no activity to handle intent
  103. if (intent.resolveActivity(getPackageManager()) != null) {
  104. startActivity(intent);
  105. } else {
  106. Toast.makeText(this, R.string.app_not_available, Toast.LENGTH_LONG).show();
  107. }
  108. return true;
  109. default:
  110. return super.onOptionsItemSelected(item);
  111. }
  112. }
  113. /* The click listner for ListView in the navigation drawer */
  114. private class DrawerItemClickListener implements ListView.OnItemClickListener {
  115. @Override
  116. public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
  117. selectItem(position);
  118. }
  119. }
  120. private void selectItem(int position) {
  121. // update the main content by replacing fragments
  122. Fragment fragment = new PlanetFragment();
  123. Bundle args = new Bundle();
  124. args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position);
  125. fragment.setArguments(args);
  126. FragmentManager fragmentManager = getFragmentManager();
  127. fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();
  128. // update selected item and title, then close the drawer
  129. mLeftDrawerList.setItemChecked(position, true);
  130. mRightDrawerList.setItemChecked(position, true);
  131. setTitle(mPlanetTitles[position]);
  132. mDrawerLayout.closeDrawer(mLeftDrawerList);
  133. mDrawerLayout.closeDrawer(mRightDrawerList);
  134. }
  135. @Override
  136. public void setTitle(CharSequence title) {
  137. mTitle = title;
  138. getActionBar().setTitle(mTitle);
  139. }
  140. /**
  141. * When using the ActionBarDrawerToggle, you must call it during
  142. * onPostCreate() and onConfigurationChanged()...
  143. */
  144. @Override
  145. protected void onPostCreate(Bundle savedInstanceState) {
  146. super.onPostCreate(savedInstanceState);
  147. // Sync the toggle state after onRestoreInstanceState has occurred.
  148. mDrawerToggle.syncState();
  149. }
  150. @Override
  151. public void onConfigurationChanged(Configuration newConfig) {
  152. super.onConfigurationChanged(newConfig);
  153. // Pass any configuration change to the drawer toggls
  154. mDrawerToggle.onConfigurationChanged(newConfig);
  155. }
  156. /**
  157. * Fragment that appears in the "content_frame", shows a planet
  158. */
  159. public static class PlanetFragment extends Fragment {
  160. public static final String ARG_PLANET_NUMBER = "planet_number";
  161. public PlanetFragment() {
  162. // Empty constructor required for fragment subclasses
  163. }
  164. @Override
  165. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  166. View rootView = inflater.inflate(R.layout.fragment_planet, container, false);
  167. int i = getArguments().getInt(ARG_PLANET_NUMBER);
  168. String planet = getResources().getStringArray(R.array.planets_array)[i];
  169. int imageId = getResources().getIdentifier(planet.toLowerCase(Locale.getDefault()), "drawable",
  170. getActivity().getPackageName());
  171. ((ImageView) rootView.findViewById(R.id.image)).setImageResource(imageId);
  172. getActivity().setTitle(planet);
  173. return rootView;
  174. }
  175. }
  176. }
效果如图所示:
DrawerLayoutDrawerLayout
图-1
  如果换一个需求,是从下面弹出菜单,那是用系统的DrawerLayout是做不到的,不过可以通过修改其源码来达到目的。
 
二、分析
    DrawerLayout作为一个父类容器,可以包含子View,而子View又可以分为内容区域和菜单区域。内容区域占据整个屏幕大小,菜单区域默认在屏幕内侧。DrawerLayout继承于ViewGroup,必然需要重写onLayout()方法,那么在设置子View位置的时候,内容区域默认直接显示出来,菜单区域默认隐藏在屏幕内侧。DrawerLayout的onLayout()代码如下:
  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. mInLayout = true ;
  4. final int width = r - l;
  5. final int childCount = getChildCount();
  6. for (int i = 0; i < childCount; i++) {
  7. final View child = getChildAt(i);
  8. if (child.getVisibility() == GONE) {
  9. continue;
  10. }
  11. final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  12. if (isContentView(child)) {
  13. child.layout(lp. leftMargin, lp.topMargin, lp.leftMargin + child.getMeasuredWidth(),
  14. lp. topMargin + child.getMeasuredHeight());
  15. } else { // Drawer, if it wasn't onMeasure would have thrown an
  16. // exception.
  17. final int childWidth = child.getMeasuredWidth();
  18. final int childHeight = child.getMeasuredHeight();
  19. int childLeft;
  20. final float newOffset;
  21. if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
  22. childLeft = -childWidth + ( int) (childWidth * lp.onScreen);
  23. newOffset = ( float) (childWidth + childLeft) / childWidth;
  24. } else { // Right; onMeasure checked for us.
  25. childLeft = width - ( int) (childWidth * lp.onScreen);
  26. newOffset = ( float) (width - childLeft) / childWidth;
  27. }
  28. final boolean changeOffset = newOffset != lp.onScreen;
  29. final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK ;
  30. switch (vgrav) {
  31. default:
  32. case Gravity.TOP : {
  33. child.layout(childLeft, lp.topMargin, childLeft + childWidth, lp.topMargin + childHeight);
  34. break;
  35. }
  36. case Gravity.BOTTOM : {
  37. final int height = b - t;
  38. child.layout(childLeft, height - lp.bottomMargin - child.getMeasuredHeight(), childLeft + childWidth,
  39. height - lp.bottomMargin);
  40. break;
  41. }
  42. case Gravity.CENTER_VERTICAL : {
  43. final int height = b - t;
  44. int childTop = (height - childHeight) / 2;
  45. // Offset for margins. If things don't fit right because of
  46. // bad measurement before, oh well.
  47. if (childTop < lp.topMargin ) {
  48. childTop = lp. topMargin;
  49. } else if (childTop + childHeight > height - lp.bottomMargin ) {
  50. childTop = height - lp.bottomMargin - childHeight;
  51. }
  52. child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
  53. break;
  54. }
  55. }
  56. if (changeOffset) {
  57. setDrawerViewOffset(child, newOffset);
  58. }
  59. final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE ;
  60. if (child.getVisibility() != newVisibility) {
  61. child.setVisibility(newVisibility);
  62. }
  63. }
  64. }
  65. mInLayout = false ;
  66. mFirstLayout = false ;
  67. }
  在onLayout方法中可以通过参数获取之前测量DrawerLayout的宽度和高度,然后获取DrawerLayout里面子View的个数,通过一个for循环来设置子View的位置。可以看到先是设置内容区域的位置,然后是菜单区域,后者通过设置offset来达到隐藏菜单的目的。所以这个地方是关键,如果想要在屏幕下方弹出菜单,那么就需要修改菜单的显示位置,让它置于屏幕下方。
  菜单View位置更改好,接着是手势操作的部分。在DrawerLayout中,是通过ViewDragHelper这个类来实现View的拖拽。首先它声明了左右菜单的帮助类和回调,
  1. private final ViewDragHelper mLeftDragger ;
  2. private final ViewDragHelper mRightDragger ;
  3. private final ViewDragCallback mLeftCallback ;
  4. private final ViewDragCallback mRightCallback ;
 以及左右锁和阴影,
  1. private int mLockModeLeft ;
  2. private int mLockModeRight ;
  3. private Drawable mShadowLeft ;
  4. private Drawable mShadowRight ;
 在构造方法里面初始化了帮助类和回调,
  1. mLeftCallback = new ViewDragCallback(Gravity.LEFT );
  2. mRightCallback = new ViewDragCallback(Gravity.RIGHT );
  3. mLeftDragger = ViewDragHelper.create( this , TOUCH_SLOP_SENSITIVITY , mLeftCallback );
  4. mLeftDragger .setEdgeTrackingEnabled(ViewDragHelper. EDGE_LEFT);
  5. mLeftDragger .setMinVelocity(minVel);
  6. mLeftCallback .setDragger(mLeftDragger );
  7. mRightDragger = ViewDragHelper.create( this , TOUCH_SLOP_SENSITIVITY , mRightCallback );
  8. mRightDragger .setEdgeTrackingEnabled(ViewDragHelper. EDGE_RIGHT);
  9. mRightDragger .setMinVelocity(minVel);
  10. mRightCallback .setDragger(mRightDragger );
 在设置阴影效果,进行一个左右的判断
  1. public void setDrawerShadow(Drawable shadowDrawable, @EdgeGravity int gravity) {
  2. /*
  3. * TODO Someone someday might want to set more complex drawables here.
  4. * They're probably nuts, but we might want to consider registering
  5. * callbacks, setting states, etc. properly.
  6. */
  7. final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection( this));
  8. if ((absGravity & Gravity. LEFT) == Gravity. LEFT) {
  9. mShadowLeft = shadowDrawable;
  10. invalidate();
  11. }
  12. if ((absGravity & Gravity. RIGHT) == Gravity.RIGHT ) {
  13. mShadowRight = shadowDrawable;
  14. invalidate();
  15. }
  16. }
  还有菜单锁,
  1. public void setDrawerLockMode(@LockMode int lockMode) {
  2. setDrawerLockMode(lockMode, Gravity. LEFT );
  3. setDrawerLockMode(lockMode, Gravity. RIGHT );
  4. }
  也是进行左右的判断,
  1. public void setDrawerLockMode(@LockMode int lockMode, @EdgeGravity int edgeGravity) {
  2. final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity, ViewCompat.getLayoutDirection( this));
  3. if (absGravity == Gravity. LEFT) {
  4. mLockModeLeft = lockMode;
  5. } else if (absGravity == Gravity. RIGHT) {
  6. mLockModeRight = lockMode;
  7. }
  8. if (lockMode != LOCK_MODE_UNLOCKED) {
  9. // Cancel interaction in progress
  10. final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger ;
  11. helper.cancel();
  12. }
  13. switch (lockMode) {
  14. case LOCK_MODE_LOCKED_OPEN :
  15. final View toOpen = findDrawerWithGravity(absGravity);
  16. if (toOpen != null) {
  17. openDrawer(toOpen);
  18. }
  19. break ;
  20. case LOCK_MODE_LOCKED_CLOSED :
  21. final View toClose = findDrawerWithGravity(absGravity);
  22. if (toClose != null) {
  23. closeDrawer(toClose);
  24. }
  25. break ;
  26. // default: do nothing
  27. }
  28. }
  不光是上面的方法,DrawerLayout中还有很多方法也都是这样。都是对Gravity的判断,判断是左侧菜单还是右侧,所以如果要改成从底部弹出菜单,那么把相应的值替换为Gravity.BOTTOM即可。当然还要注意在替换的过程中的一些逻辑问题,以免有纰漏。比如offset之前是左右,现在要改成上下,因此数值要重新计算。还有变量的命名等,之前是left或者right,现在改为bottom等等。
  修改后运行效果如下:
DrawerLayoutDrawerLayout