NavigationMenuPresenter.java revision e7b2c26d2ffadc94dc6ee043ab12e03201e1bf95
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.design.internal; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.Resources; 22import android.graphics.drawable.ColorDrawable; 23import android.graphics.drawable.Drawable; 24import android.os.Bundle; 25import android.os.Parcelable; 26import android.support.annotation.LayoutRes; 27import android.support.annotation.NonNull; 28import android.support.annotation.Nullable; 29import android.support.annotation.StyleRes; 30import android.support.design.R; 31import android.support.v7.internal.view.menu.MenuBuilder; 32import android.support.v7.internal.view.menu.MenuItemImpl; 33import android.support.v7.internal.view.menu.MenuPresenter; 34import android.support.v7.internal.view.menu.MenuView; 35import android.support.v7.internal.view.menu.SubMenuBuilder; 36import android.support.v7.widget.RecyclerView; 37import android.util.SparseArray; 38import android.view.LayoutInflater; 39import android.view.MenuItem; 40import android.view.SubMenu; 41import android.view.View; 42import android.view.ViewGroup; 43import android.widget.LinearLayout; 44import android.widget.TextView; 45 46import java.util.ArrayList; 47 48/** 49 * @hide 50 */ 51public class NavigationMenuPresenter implements MenuPresenter { 52 53 private static final String STATE_HIERARCHY = "android:menu:list"; 54 private static final String STATE_ADAPTER = "android:menu:adapter"; 55 56 private NavigationMenuView mMenuView; 57 private LinearLayout mHeaderLayout; 58 59 private Callback mCallback; 60 private MenuBuilder mMenu; 61 private int mId; 62 63 private NavigationMenuAdapter mAdapter; 64 private LayoutInflater mLayoutInflater; 65 66 private int mTextAppearance; 67 private boolean mTextAppearanceSet; 68 private ColorStateList mTextColor; 69 private ColorStateList mIconTintList; 70 private Drawable mItemBackground; 71 72 /** 73 * Padding to be inserted at the top of the list to avoid the first menu item 74 * from being placed underneath the status bar. 75 */ 76 private int mPaddingTopDefault; 77 78 /** 79 * Padding for separators between items 80 */ 81 private int mPaddingSeparator; 82 83 @Override 84 public void initForMenu(Context context, MenuBuilder menu) { 85 mLayoutInflater = LayoutInflater.from(context); 86 mMenu = menu; 87 Resources res = context.getResources(); 88 mPaddingTopDefault = res.getDimensionPixelOffset( 89 R.dimen.design_navigation_padding_top_default); 90 mPaddingSeparator = res.getDimensionPixelOffset( 91 R.dimen.design_navigation_separator_vertical_padding); 92 } 93 94 @Override 95 public MenuView getMenuView(ViewGroup root) { 96 if (mMenuView == null) { 97 mMenuView = (NavigationMenuView) mLayoutInflater.inflate( 98 R.layout.design_navigation_menu, root, false); 99 if (mAdapter == null) { 100 mAdapter = new NavigationMenuAdapter(); 101 } 102 mHeaderLayout = (LinearLayout) mLayoutInflater 103 .inflate(R.layout.design_navigation_item_header, 104 mMenuView, false); 105 mMenuView.setAdapter(mAdapter); 106 } 107 return mMenuView; 108 } 109 110 @Override 111 public void updateMenuView(boolean cleared) { 112 if (mAdapter != null) { 113 mAdapter.update(); 114 } 115 } 116 117 @Override 118 public void setCallback(Callback cb) { 119 mCallback = cb; 120 } 121 122 @Override 123 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 124 return false; 125 } 126 127 @Override 128 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 129 if (mCallback != null) { 130 mCallback.onCloseMenu(menu, allMenusAreClosing); 131 } 132 } 133 134 @Override 135 public boolean flagActionItems() { 136 return false; 137 } 138 139 @Override 140 public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { 141 return false; 142 } 143 144 @Override 145 public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { 146 return false; 147 } 148 149 @Override 150 public int getId() { 151 return mId; 152 } 153 154 public void setId(int id) { 155 mId = id; 156 } 157 158 @Override 159 public Parcelable onSaveInstanceState() { 160 Bundle state = new Bundle(); 161 if (mMenuView != null) { 162 SparseArray<Parcelable> hierarchy = new SparseArray<>(); 163 mMenuView.saveHierarchyState(hierarchy); 164 state.putSparseParcelableArray(STATE_HIERARCHY, hierarchy); 165 } 166 if (mAdapter != null) { 167 state.putBundle(STATE_ADAPTER, mAdapter.createInstanceState()); 168 } 169 return state; 170 } 171 172 @Override 173 public void onRestoreInstanceState(Parcelable parcelable) { 174 Bundle state = (Bundle) parcelable; 175 SparseArray<Parcelable> hierarchy = state.getSparseParcelableArray(STATE_HIERARCHY); 176 if (hierarchy != null) { 177 mMenuView.restoreHierarchyState(hierarchy); 178 } 179 Bundle adapterState = state.getBundle(STATE_ADAPTER); 180 if (adapterState != null) { 181 mAdapter.restoreInstanceState(adapterState); 182 } 183 } 184 185 public void setCheckedItem(MenuItemImpl item) { 186 mAdapter.setCheckedItem(item); 187 } 188 189 public View inflateHeaderView(@LayoutRes int res) { 190 View view = mLayoutInflater.inflate(res, mHeaderLayout, false); 191 addHeaderView(view); 192 return view; 193 } 194 195 public void addHeaderView(@NonNull View view) { 196 mHeaderLayout.addView(view); 197 // The padding on top should be cleared. 198 mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom()); 199 } 200 201 public void removeHeaderView(@NonNull View view) { 202 mHeaderLayout.removeView(view); 203 if (mHeaderLayout.getChildCount() == 0) { 204 mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom()); 205 } 206 } 207 208 @Nullable 209 public ColorStateList getItemTintList() { 210 return mIconTintList; 211 } 212 213 public void setItemIconTintList(@Nullable ColorStateList tint) { 214 mIconTintList = tint; 215 updateMenuView(false); 216 } 217 218 @Nullable 219 public ColorStateList getItemTextColor() { 220 return mTextColor; 221 } 222 223 public void setItemTextColor(@Nullable ColorStateList textColor) { 224 mTextColor = textColor; 225 updateMenuView(false); 226 } 227 228 public void setItemTextAppearance(@StyleRes int resId) { 229 mTextAppearance = resId; 230 mTextAppearanceSet = true; 231 updateMenuView(false); 232 } 233 234 public Drawable getItemBackground() { 235 return mItemBackground; 236 } 237 238 public void setItemBackground(Drawable itemBackground) { 239 mItemBackground = itemBackground; 240 } 241 242 public void setUpdateSuspended(boolean updateSuspended) { 243 if (mAdapter != null) { 244 mAdapter.setUpdateSuspended(updateSuspended); 245 } 246 } 247 248 private abstract static class ViewHolder extends RecyclerView.ViewHolder { 249 250 public ViewHolder(View itemView) { 251 super(itemView); 252 } 253 254 } 255 256 private static class NormalViewHolder extends ViewHolder { 257 258 public NormalViewHolder(LayoutInflater inflater, ViewGroup parent, 259 View.OnClickListener listener) { 260 super(inflater.inflate(R.layout.design_navigation_item, parent, false)); 261 itemView.setOnClickListener(listener); 262 } 263 264 } 265 266 private static class SubheaderViewHolder extends ViewHolder { 267 268 public SubheaderViewHolder(LayoutInflater inflater, ViewGroup parent) { 269 super(inflater.inflate(R.layout.design_navigation_item_subheader, parent, false)); 270 } 271 272 } 273 274 private static class SeparatorViewHolder extends ViewHolder { 275 276 public SeparatorViewHolder(LayoutInflater inflater, ViewGroup parent) { 277 super(inflater.inflate(R.layout.design_navigation_item_separator, parent, false)); 278 } 279 280 } 281 282 private static class HeaderViewHolder extends ViewHolder { 283 284 public HeaderViewHolder(View itemView) { 285 super(itemView); 286 } 287 288 } 289 290 /** 291 * Handles click events for the menu items. The items has to be {@link NavigationMenuItemView}. 292 */ 293 private final View.OnClickListener mOnClickListener = new View.OnClickListener() { 294 295 @Override 296 public void onClick(View v) { 297 NavigationMenuItemView itemView = (NavigationMenuItemView) v; 298 setUpdateSuspended(true); 299 MenuItemImpl item = itemView.getItemData(); 300 boolean result = mMenu.performItemAction(item, NavigationMenuPresenter.this, 0); 301 if (item != null && item.isCheckable() && result) { 302 mAdapter.setCheckedItem(item); 303 } 304 setUpdateSuspended(false); 305 updateMenuView(false); 306 } 307 308 }; 309 310 private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> { 311 312 private static final String STATE_CHECKED_ITEM = "android:menu:checked"; 313 314 private static final String STATE_ACTION_VIEWS = "android:menu:action_views"; 315 private static final int VIEW_TYPE_NORMAL = 0; 316 private static final int VIEW_TYPE_SUBHEADER = 1; 317 private static final int VIEW_TYPE_SEPARATOR = 2; 318 private static final int VIEW_TYPE_HEADER = 3; 319 320 private final ArrayList<NavigationMenuItem> mItems = new ArrayList<>(); 321 private MenuItemImpl mCheckedItem; 322 private ColorDrawable mTransparentIcon; 323 private boolean mUpdateSuspended; 324 325 NavigationMenuAdapter() { 326 prepareMenuItems(); 327 } 328 329 @Override 330 public long getItemId(int position) { 331 return position; 332 } 333 334 @Override 335 public int getItemCount() { 336 return mItems.size(); 337 } 338 339 @Override 340 public int getItemViewType(int position) { 341 NavigationMenuItem item = mItems.get(position); 342 if (item instanceof NavigationMenuSeparatorItem) { 343 return VIEW_TYPE_SEPARATOR; 344 } else if (item instanceof NavigationMenuHeaderItem) { 345 return VIEW_TYPE_HEADER; 346 } else if (item instanceof NavigationMenuTextItem) { 347 NavigationMenuTextItem textItem = (NavigationMenuTextItem) item; 348 if (textItem.getMenuItem().hasSubMenu()) { 349 return VIEW_TYPE_SUBHEADER; 350 } else { 351 return VIEW_TYPE_NORMAL; 352 } 353 } 354 throw new RuntimeException("Unknown item type."); 355 } 356 357 @Override 358 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 359 switch (viewType) { 360 case VIEW_TYPE_NORMAL: 361 return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener); 362 case VIEW_TYPE_SUBHEADER: 363 return new SubheaderViewHolder(mLayoutInflater, parent); 364 case VIEW_TYPE_SEPARATOR: 365 return new SeparatorViewHolder(mLayoutInflater, parent); 366 case VIEW_TYPE_HEADER: 367 return new HeaderViewHolder(mHeaderLayout); 368 } 369 return null; 370 } 371 372 @Override 373 public void onBindViewHolder(ViewHolder holder, int position) { 374 switch (getItemViewType(position)) { 375 case VIEW_TYPE_NORMAL: { 376 NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView; 377 itemView.setIconTintList(mIconTintList); 378 if (mTextAppearanceSet) { 379 itemView.setTextAppearance(itemView.getContext(), mTextAppearance); 380 } 381 if (mTextColor != null) { 382 itemView.setTextColor(mTextColor); 383 } 384 itemView.setBackgroundDrawable(mItemBackground != null ? 385 mItemBackground.getConstantState().newDrawable() : null); 386 NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position); 387 itemView.initialize(item.getMenuItem(), 0); 388 break; 389 } 390 case VIEW_TYPE_SUBHEADER: { 391 TextView subHeader = (TextView) holder.itemView; 392 NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position); 393 subHeader.setText(item.getMenuItem().getTitle()); 394 break; 395 } 396 case VIEW_TYPE_SEPARATOR: { 397 NavigationMenuSeparatorItem item = 398 (NavigationMenuSeparatorItem) mItems.get(position); 399 holder.itemView.setPadding(0, item.getPaddingTop(), 0, 400 item.getPaddingBottom()); 401 break; 402 } 403 case VIEW_TYPE_HEADER: { 404 break; 405 } 406 } 407 408 } 409 410 @Override 411 public void onViewRecycled(ViewHolder holder) { 412 if (holder instanceof NormalViewHolder) { 413 ((NavigationMenuItemView) holder.itemView).recycle(); 414 } 415 } 416 417 public void update() { 418 prepareMenuItems(); 419 notifyDataSetChanged(); 420 } 421 422 /** 423 * Flattens the visible menu items of {@link #mMenu} into {@link #mItems}, 424 * while inserting separators between items when necessary. 425 */ 426 private void prepareMenuItems() { 427 if (mUpdateSuspended) { 428 return; 429 } 430 mUpdateSuspended = true; 431 mItems.clear(); 432 mItems.add(new NavigationMenuHeaderItem()); 433 434 int currentGroupId = -1; 435 int currentGroupStart = 0; 436 boolean currentGroupHasIcon = false; 437 for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) { 438 MenuItemImpl item = mMenu.getVisibleItems().get(i); 439 if (item.isChecked()) { 440 setCheckedItem(item); 441 } 442 if (item.isCheckable()) { 443 item.setExclusiveCheckable(false); 444 } 445 if (item.hasSubMenu()) { 446 SubMenu subMenu = item.getSubMenu(); 447 if (subMenu.hasVisibleItems()) { 448 if (i != 0) { 449 mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0)); 450 } 451 mItems.add(new NavigationMenuTextItem(item)); 452 boolean subMenuHasIcon = false; 453 int subMenuStart = mItems.size(); 454 for (int j = 0, size = subMenu.size(); j < size; j++) { 455 MenuItemImpl subMenuItem = (MenuItemImpl) subMenu.getItem(j); 456 if (subMenuItem.isVisible()) { 457 if (!subMenuHasIcon && subMenuItem.getIcon() != null) { 458 subMenuHasIcon = true; 459 } 460 if (subMenuItem.isCheckable()) { 461 subMenuItem.setExclusiveCheckable(false); 462 } 463 if (item.isChecked()) { 464 setCheckedItem(item); 465 } 466 mItems.add(new NavigationMenuTextItem(subMenuItem)); 467 } 468 } 469 if (subMenuHasIcon) { 470 appendTransparentIconIfMissing(subMenuStart, mItems.size()); 471 } 472 } 473 } else { 474 int groupId = item.getGroupId(); 475 if (groupId != currentGroupId) { // first item in group 476 currentGroupStart = mItems.size(); 477 currentGroupHasIcon = item.getIcon() != null; 478 if (i != 0) { 479 currentGroupStart++; 480 mItems.add(new NavigationMenuSeparatorItem( 481 mPaddingSeparator, mPaddingSeparator)); 482 } 483 } else if (!currentGroupHasIcon && item.getIcon() != null) { 484 currentGroupHasIcon = true; 485 appendTransparentIconIfMissing(currentGroupStart, mItems.size()); 486 } 487 if (currentGroupHasIcon && item.getIcon() == null) { 488 item.setIcon(android.R.color.transparent); 489 } 490 mItems.add(new NavigationMenuTextItem(item)); 491 currentGroupId = groupId; 492 } 493 } 494 mUpdateSuspended = false; 495 } 496 497 private void appendTransparentIconIfMissing(int startIndex, int endIndex) { 498 for (int i = startIndex; i < endIndex; i++) { 499 NavigationMenuTextItem textItem = (NavigationMenuTextItem) mItems.get(i); 500 MenuItem item = textItem.getMenuItem(); 501 if (item.getIcon() == null) { 502 if (mTransparentIcon == null) { 503 mTransparentIcon = new ColorDrawable(android.R.color.transparent); 504 } 505 item.setIcon(mTransparentIcon); 506 } 507 } 508 } 509 510 public void setCheckedItem(MenuItemImpl checkedItem) { 511 if (mCheckedItem == checkedItem || !checkedItem.isCheckable()) { 512 return; 513 } 514 if (mCheckedItem != null) { 515 mCheckedItem.setChecked(false); 516 } 517 mCheckedItem = checkedItem; 518 checkedItem.setChecked(true); 519 } 520 521 public Bundle createInstanceState() { 522 Bundle state = new Bundle(); 523 if (mCheckedItem != null) { 524 state.putInt(STATE_CHECKED_ITEM, mCheckedItem.getItemId()); 525 } 526 // Store the states of the action views. 527 SparseArray<ParcelableSparseArray> actionViewStates = new SparseArray<>(); 528 for (NavigationMenuItem navigationMenuItem : mItems) { 529 if (navigationMenuItem instanceof NavigationMenuTextItem) { 530 MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem(); 531 View actionView = item != null ? item.getActionView() : null; 532 if (actionView != null) { 533 ParcelableSparseArray container = new ParcelableSparseArray(); 534 actionView.saveHierarchyState(container); 535 actionViewStates.put(item.getItemId(), container); 536 } 537 } 538 } 539 state.putSparseParcelableArray(STATE_ACTION_VIEWS, actionViewStates); 540 return state; 541 } 542 543 public void restoreInstanceState(Bundle state) { 544 int checkedItem = state.getInt(STATE_CHECKED_ITEM, 0); 545 if (checkedItem != 0) { 546 mUpdateSuspended = true; 547 for (NavigationMenuItem item : mItems) { 548 if (item instanceof NavigationMenuTextItem) { 549 MenuItemImpl menuItem = ((NavigationMenuTextItem) item).getMenuItem(); 550 if (menuItem != null && menuItem.getItemId() == checkedItem) { 551 setCheckedItem(menuItem); 552 break; 553 } 554 } 555 } 556 mUpdateSuspended = false; 557 prepareMenuItems(); 558 } 559 // Restore the states of the action views. 560 SparseArray<ParcelableSparseArray> actionViewStates = state 561 .getSparseParcelableArray(STATE_ACTION_VIEWS); 562 for (NavigationMenuItem navigationMenuItem : mItems) { 563 if (navigationMenuItem instanceof NavigationMenuTextItem) { 564 MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem(); 565 View actionView = item != null ? item.getActionView() : null; 566 if (actionView != null) { 567 actionView.restoreHierarchyState(actionViewStates.get(item.getItemId())); 568 } 569 } 570 } 571 } 572 573 public void setUpdateSuspended(boolean updateSuspended) { 574 mUpdateSuspended = updateSuspended; 575 } 576 577 } 578 579 /** 580 * Unified data model for all sorts of navigation menu items. 581 */ 582 private interface NavigationMenuItem { 583 } 584 585 /** 586 * Normal or subheader items. 587 */ 588 private static class NavigationMenuTextItem implements NavigationMenuItem { 589 590 private final MenuItemImpl mMenuItem; 591 592 private NavigationMenuTextItem(MenuItemImpl item) { 593 mMenuItem = item; 594 } 595 596 public MenuItemImpl getMenuItem() { 597 return mMenuItem; 598 } 599 600 } 601 602 /** 603 * Separator items. 604 */ 605 private static class NavigationMenuSeparatorItem implements NavigationMenuItem { 606 607 private final int mPaddingTop; 608 609 private final int mPaddingBottom; 610 611 public NavigationMenuSeparatorItem(int paddingTop, int paddingBottom) { 612 mPaddingTop = paddingTop; 613 mPaddingBottom = paddingBottom; 614 } 615 616 public int getPaddingTop() { 617 return mPaddingTop; 618 } 619 620 public int getPaddingBottom() { 621 return mPaddingBottom; 622 } 623 624 } 625 626 /** 627 * Header (not subheader) items. 628 */ 629 private static class NavigationMenuHeaderItem implements NavigationMenuItem { 630 // The actual content is hold by NavigationMenuPresenter#mHeaderLayout. 631 } 632 633} 634