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