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