NavigationMenuPresenter.java revision 30d42dc19655e637644adc3846025027240bf0af
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.view.menu.MenuBuilder; 32import android.support.v7.view.menu.MenuItemImpl; 33import android.support.v7.view.menu.MenuPresenter; 34import android.support.v7.view.menu.MenuView; 35import android.support.v7.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 mPaddingSeparator = res.getDimensionPixelOffset( 89 R.dimen.design_navigation_separator_vertical_padding); 90 } 91 92 @Override 93 public MenuView getMenuView(ViewGroup root) { 94 if (mMenuView == null) { 95 mMenuView = (NavigationMenuView) mLayoutInflater.inflate( 96 R.layout.design_navigation_menu, root, false); 97 if (mAdapter == null) { 98 mAdapter = new NavigationMenuAdapter(); 99 } 100 mHeaderLayout = (LinearLayout) mLayoutInflater 101 .inflate(R.layout.design_navigation_item_header, 102 mMenuView, false); 103 mMenuView.setAdapter(mAdapter); 104 } 105 return mMenuView; 106 } 107 108 @Override 109 public void updateMenuView(boolean cleared) { 110 if (mAdapter != null) { 111 mAdapter.update(); 112 } 113 } 114 115 @Override 116 public void setCallback(Callback cb) { 117 mCallback = cb; 118 } 119 120 @Override 121 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 122 return false; 123 } 124 125 @Override 126 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 127 if (mCallback != null) { 128 mCallback.onCloseMenu(menu, allMenusAreClosing); 129 } 130 } 131 132 @Override 133 public boolean flagActionItems() { 134 return false; 135 } 136 137 @Override 138 public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { 139 return false; 140 } 141 142 @Override 143 public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { 144 return false; 145 } 146 147 @Override 148 public int getId() { 149 return mId; 150 } 151 152 public void setId(int id) { 153 mId = id; 154 } 155 156 @Override 157 public Parcelable onSaveInstanceState() { 158 Bundle state = new Bundle(); 159 if (mMenuView != null) { 160 SparseArray<Parcelable> hierarchy = new SparseArray<>(); 161 mMenuView.saveHierarchyState(hierarchy); 162 state.putSparseParcelableArray(STATE_HIERARCHY, hierarchy); 163 } 164 if (mAdapter != null) { 165 state.putBundle(STATE_ADAPTER, mAdapter.createInstanceState()); 166 } 167 return state; 168 } 169 170 @Override 171 public void onRestoreInstanceState(Parcelable parcelable) { 172 Bundle state = (Bundle) parcelable; 173 SparseArray<Parcelable> hierarchy = state.getSparseParcelableArray(STATE_HIERARCHY); 174 if (hierarchy != null) { 175 mMenuView.restoreHierarchyState(hierarchy); 176 } 177 Bundle adapterState = state.getBundle(STATE_ADAPTER); 178 if (adapterState != null) { 179 mAdapter.restoreInstanceState(adapterState); 180 } 181 } 182 183 public void setCheckedItem(MenuItemImpl item) { 184 mAdapter.setCheckedItem(item); 185 } 186 187 public View inflateHeaderView(@LayoutRes int res) { 188 View view = mLayoutInflater.inflate(res, mHeaderLayout, false); 189 addHeaderView(view); 190 return view; 191 } 192 193 public void addHeaderView(@NonNull View view) { 194 mHeaderLayout.addView(view); 195 // The padding on top should be cleared. 196 mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom()); 197 } 198 199 public void removeHeaderView(@NonNull View view) { 200 mHeaderLayout.removeView(view); 201 if (mHeaderLayout.getChildCount() == 0) { 202 mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom()); 203 } 204 } 205 206 public int getHeaderCount() { 207 return mHeaderLayout.getChildCount(); 208 } 209 210 public View getHeaderView(int index) { 211 return mHeaderLayout.getChildAt(index); 212 } 213 214 @Nullable 215 public ColorStateList getItemTintList() { 216 return mIconTintList; 217 } 218 219 public void setItemIconTintList(@Nullable ColorStateList tint) { 220 mIconTintList = tint; 221 updateMenuView(false); 222 } 223 224 @Nullable 225 public ColorStateList getItemTextColor() { 226 return mTextColor; 227 } 228 229 public void setItemTextColor(@Nullable ColorStateList textColor) { 230 mTextColor = textColor; 231 updateMenuView(false); 232 } 233 234 public void setItemTextAppearance(@StyleRes int resId) { 235 mTextAppearance = resId; 236 mTextAppearanceSet = true; 237 updateMenuView(false); 238 } 239 240 public Drawable getItemBackground() { 241 return mItemBackground; 242 } 243 244 public void setItemBackground(Drawable itemBackground) { 245 mItemBackground = itemBackground; 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 ColorDrawable mTransparentIcon; 338 private boolean mUpdateSuspended; 339 340 NavigationMenuAdapter() { 341 prepareMenuItems(); 342 } 343 344 @Override 345 public long getItemId(int position) { 346 return position; 347 } 348 349 @Override 350 public int getItemCount() { 351 return mItems.size(); 352 } 353 354 @Override 355 public int getItemViewType(int position) { 356 NavigationMenuItem item = mItems.get(position); 357 if (item instanceof NavigationMenuSeparatorItem) { 358 return VIEW_TYPE_SEPARATOR; 359 } else if (item instanceof NavigationMenuHeaderItem) { 360 return VIEW_TYPE_HEADER; 361 } else if (item instanceof NavigationMenuTextItem) { 362 NavigationMenuTextItem textItem = (NavigationMenuTextItem) item; 363 if (textItem.getMenuItem().hasSubMenu()) { 364 return VIEW_TYPE_SUBHEADER; 365 } else { 366 return VIEW_TYPE_NORMAL; 367 } 368 } 369 throw new RuntimeException("Unknown item type."); 370 } 371 372 @Override 373 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 374 switch (viewType) { 375 case VIEW_TYPE_NORMAL: 376 return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener); 377 case VIEW_TYPE_SUBHEADER: 378 return new SubheaderViewHolder(mLayoutInflater, parent); 379 case VIEW_TYPE_SEPARATOR: 380 return new SeparatorViewHolder(mLayoutInflater, parent); 381 case VIEW_TYPE_HEADER: 382 return new HeaderViewHolder(mHeaderLayout); 383 } 384 return null; 385 } 386 387 @Override 388 public void onBindViewHolder(ViewHolder holder, int position) { 389 switch (getItemViewType(position)) { 390 case VIEW_TYPE_NORMAL: { 391 NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView; 392 itemView.setIconTintList(mIconTintList); 393 if (mTextAppearanceSet) { 394 itemView.setTextAppearance(itemView.getContext(), mTextAppearance); 395 } 396 if (mTextColor != null) { 397 itemView.setTextColor(mTextColor); 398 } 399 itemView.setBackgroundDrawable(mItemBackground != null ? 400 mItemBackground.getConstantState().newDrawable() : null); 401 NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position); 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 if (currentGroupHasIcon && item.getIcon() == null) { 503 item.setIcon(android.R.color.transparent); 504 } 505 mItems.add(new NavigationMenuTextItem(item)); 506 currentGroupId = groupId; 507 } 508 } 509 mUpdateSuspended = false; 510 } 511 512 private void appendTransparentIconIfMissing(int startIndex, int endIndex) { 513 for (int i = startIndex; i < endIndex; i++) { 514 NavigationMenuTextItem textItem = (NavigationMenuTextItem) mItems.get(i); 515 MenuItem item = textItem.getMenuItem(); 516 if (item.getIcon() == null) { 517 if (mTransparentIcon == null) { 518 mTransparentIcon = new ColorDrawable(android.R.color.transparent); 519 } 520 item.setIcon(mTransparentIcon); 521 } 522 } 523 } 524 525 public void setCheckedItem(MenuItemImpl checkedItem) { 526 if (mCheckedItem == checkedItem || !checkedItem.isCheckable()) { 527 return; 528 } 529 if (mCheckedItem != null) { 530 mCheckedItem.setChecked(false); 531 } 532 mCheckedItem = checkedItem; 533 checkedItem.setChecked(true); 534 } 535 536 public Bundle createInstanceState() { 537 Bundle state = new Bundle(); 538 if (mCheckedItem != null) { 539 state.putInt(STATE_CHECKED_ITEM, mCheckedItem.getItemId()); 540 } 541 // Store the states of the action views. 542 SparseArray<ParcelableSparseArray> actionViewStates = new SparseArray<>(); 543 for (NavigationMenuItem navigationMenuItem : mItems) { 544 if (navigationMenuItem instanceof NavigationMenuTextItem) { 545 MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem(); 546 View actionView = item != null ? item.getActionView() : null; 547 if (actionView != null) { 548 ParcelableSparseArray container = new ParcelableSparseArray(); 549 actionView.saveHierarchyState(container); 550 actionViewStates.put(item.getItemId(), container); 551 } 552 } 553 } 554 state.putSparseParcelableArray(STATE_ACTION_VIEWS, actionViewStates); 555 return state; 556 } 557 558 public void restoreInstanceState(Bundle state) { 559 int checkedItem = state.getInt(STATE_CHECKED_ITEM, 0); 560 if (checkedItem != 0) { 561 mUpdateSuspended = true; 562 for (NavigationMenuItem item : mItems) { 563 if (item instanceof NavigationMenuTextItem) { 564 MenuItemImpl menuItem = ((NavigationMenuTextItem) item).getMenuItem(); 565 if (menuItem != null && menuItem.getItemId() == checkedItem) { 566 setCheckedItem(menuItem); 567 break; 568 } 569 } 570 } 571 mUpdateSuspended = false; 572 prepareMenuItems(); 573 } 574 // Restore the states of the action views. 575 SparseArray<ParcelableSparseArray> actionViewStates = state 576 .getSparseParcelableArray(STATE_ACTION_VIEWS); 577 for (NavigationMenuItem navigationMenuItem : mItems) { 578 if (navigationMenuItem instanceof NavigationMenuTextItem) { 579 MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem(); 580 View actionView = item != null ? item.getActionView() : null; 581 if (actionView != null) { 582 actionView.restoreHierarchyState(actionViewStates.get(item.getItemId())); 583 } 584 } 585 } 586 } 587 588 public void setUpdateSuspended(boolean updateSuspended) { 589 mUpdateSuspended = updateSuspended; 590 } 591 592 } 593 594 /** 595 * Unified data model for all sorts of navigation menu items. 596 */ 597 private interface NavigationMenuItem { 598 } 599 600 /** 601 * Normal or subheader items. 602 */ 603 private static class NavigationMenuTextItem implements NavigationMenuItem { 604 605 private final MenuItemImpl mMenuItem; 606 607 private NavigationMenuTextItem(MenuItemImpl item) { 608 mMenuItem = item; 609 } 610 611 public MenuItemImpl getMenuItem() { 612 return mMenuItem; 613 } 614 615 } 616 617 /** 618 * Separator items. 619 */ 620 private static class NavigationMenuSeparatorItem implements NavigationMenuItem { 621 622 private final int mPaddingTop; 623 624 private final int mPaddingBottom; 625 626 public NavigationMenuSeparatorItem(int paddingTop, int paddingBottom) { 627 mPaddingTop = paddingTop; 628 mPaddingBottom = paddingBottom; 629 } 630 631 public int getPaddingTop() { 632 return mPaddingTop; 633 } 634 635 public int getPaddingBottom() { 636 return mPaddingBottom; 637 } 638 639 } 640 641 /** 642 * Header (not subheader) items. 643 */ 644 private static class NavigationMenuHeaderItem implements NavigationMenuItem { 645 // The actual content is hold by NavigationMenuPresenter#mHeaderLayout. 646 } 647 648} 649