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 package android.car.ui.provider; 17 18 import android.car.app.menu.CarMenuCallbacks; 19 import android.content.Context; 20 import android.graphics.Canvas; 21 import android.os.Bundle; 22 import android.support.car.ui.PagedListView; 23 import android.support.car.ui.R; 24 import android.support.v7.widget.CardView; 25 import android.support.v7.widget.RecyclerView; 26 import android.text.TextUtils; 27 import android.util.Log; 28 import android.view.View; 29 import android.view.animation.Animation; 30 import android.view.animation.AnimationUtils; 31 import android.widget.ProgressBar; 32 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.LinkedList; 36 import java.util.List; 37 import java.util.Queue; 38 import java.util.Stack; 39 40 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.FLAG_BROWSABLE; 41 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_FLAGS; 42 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_ID; 43 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_TITLE; 44 45 /** 46 * Controls the drawer for SDK app 47 */ 48 public class DrawerController 49 implements CarDrawerLayout.DrawerListener, DrawerApiAdapter.OnItemSelectedListener, 50 CarDrawerLayout.DrawerControllerListener { 51 private static final String TAG = "CAR.UI.DrawerController"; 52 // Qualify with full package name to make it less likely there will be a collision 53 private static final String KEY_IDS = "android.support.car.ui.drawer.sdk.IDS"; 54 private static final String KEY_DRAWERSTATE = 55 "android.support.car.ui.drawer.sdk.DRAWER_STATE"; 56 private static final String KEY_TITLES = "android.support.car.ui.drawer.sdk.TITLES"; 57 private static final String KEY_ROOT = "android.support.car.ui.drawer.sdk.ROOT"; 58 private static final String KEY_ID_UNAVAILABLE_CATEGORY = "UNAVAILABLE_CATEGORY"; 59 private static final String KEY_CLICK_STACK = 60 "android.support.car.ui.drawer.sdk.CLICK_STACK"; 61 private static final String KEY_MAX_PAGES = 62 "android.support.car.ui.drawer.sdk.MAX_PAGES"; 63 private static final String KEY_IS_CAPPED = 64 "android.support.car.ui.drawer.sdk.IS_CAPPED"; 65 66 /** Drawer is in Auto dark/light mode */ 67 private static final int MODE_AUTO = 0; 68 /** Drawer is in Light mode */ 69 private static final int MODE_LIGHT = 1; 70 /** Drawer is in Dark mode */ 71 private static final int MODE_DARK = 2; 72 73 private final Stack<String> mSubscriptionIds = new Stack<>(); 74 private final Stack<CharSequence> mTitles = new Stack<>(); 75 private final SubscriptionCallbacks mSubscriptionCallbacks = new SubscriptionCallbacks(); 76 // Named to be consistent with CarDrawerFragment to make copying code easier and less error 77 // prone 78 private final CarDrawerLayout mContainer; 79 private final PagedListView mListView; 80 // private final CardView mTruncatedListCardView; 81 private final ProgressBar mProgressBar; 82 private final Context mContext; 83 private final ViewAnimationController mPlvAnimationController; 84 private final CardView mTruncatedListCardView; 85 private final Stack<Integer> mClickCountStack = new Stack<>(); 86 87 private CarMenuCallbacks mCarMenuCallbacks; 88 private DrawerApiAdapter mAdapter; 89 private int mScrimColor = CarDrawerLayout.DEFAULT_SCRIM_COLOR; 90 private boolean mIsDrawerOpen; 91 private boolean mIsDrawerAnimating; 92 private boolean mIsCapped; 93 private int mItemsNumber; 94 private int mDrawerMode; 95 private CharSequence mContentTitle; 96 private String mRootId; 97 private boolean mRestartedFromDayNightMode; 98 private CarUiEntry mUiEntry; 99 DrawerController(CarUiEntry uiEntry, View menuButton, CarDrawerLayout drawerLayout, PagedListView listView, CardView cardView)100 public DrawerController(CarUiEntry uiEntry, View menuButton, CarDrawerLayout drawerLayout, 101 PagedListView listView, CardView cardView) { 102 //mCarAppLayout = appLayout; 103 menuButton.setOnClickListener(mMenuClickListener); 104 mContainer = drawerLayout; 105 mListView = listView; 106 mUiEntry = uiEntry; 107 mTruncatedListCardView = cardView; 108 mListView.setDefaultItemDecoration(new DrawerMenuListDecoration(mListView.getContext())); 109 mProgressBar = (ProgressBar) mContainer.findViewById(R.id.progress); 110 mContext = mListView.getContext(); 111 mPlvAnimationController = new ViewAnimationController( 112 mListView, R.anim.car_list_in, R.anim.sdk_list_out, R.anim.car_list_pop_out); 113 mRootId = null; 114 115 mContainer.setDrawerListener(this); 116 mContainer.setDrawerControllerListener(this); 117 setAutoLightDarkMode(); 118 } 119 120 121 @Override onDrawerOpened(View drawerView)122 public void onDrawerOpened(View drawerView) { 123 mIsDrawerOpen = true; 124 mIsDrawerAnimating = false; 125 mUiEntry.setMenuProgress(1.0f); 126 // This can be null on day/night mode changes 127 if (mCarMenuCallbacks != null) { 128 mCarMenuCallbacks.onCarMenuOpened(); 129 } 130 } 131 132 @Override onDrawerClosed(View drawerView)133 public void onDrawerClosed(View drawerView) { 134 mIsDrawerOpen = false; 135 mIsDrawerAnimating = false; 136 clearMenu(); 137 mUiEntry.setMenuProgress(0); 138 mUiEntry.setTitle(mContentTitle); 139 // This can be null on day/night mode changes 140 if (mCarMenuCallbacks != null) { 141 mCarMenuCallbacks.onCarMenuClosed(); 142 } 143 } 144 145 @Override onDrawerStateChanged(int newState)146 public void onDrawerStateChanged(int newState) { 147 } 148 149 @Override onDrawerOpening(View drawerView)150 public void onDrawerOpening(View drawerView) { 151 mIsDrawerAnimating = true; 152 // This can be null on day/night mode changes 153 if (mCarMenuCallbacks != null) { 154 mCarMenuCallbacks.onCarMenuOpening(); 155 } 156 } 157 158 @Override onDrawerSlide(View drawerView, float slideOffset)159 public void onDrawerSlide(View drawerView, float slideOffset) { 160 mUiEntry.setMenuProgress(slideOffset); 161 } 162 163 @Override onDrawerClosing(View drawerView)164 public void onDrawerClosing(View drawerView) { 165 mIsDrawerAnimating = true; 166 // This can be null on day/night mode changes 167 if (mCarMenuCallbacks != null) { 168 mCarMenuCallbacks.onCarMenuClosing(); 169 } 170 } 171 172 @Override onItemClicked(Bundle item, int position)173 public void onItemClicked(Bundle item, int position) { 174 // Don't allow selection while animating 175 if (mPlvAnimationController.isAnimating()) { 176 return; 177 } 178 int flags = item.getInt(KEY_FLAGS); 179 String id = item.getString(KEY_ID); 180 181 // Page number is 0 index, + 1 for the actual click. 182 int clicksUsed = mListView.getPage(position) + 1; 183 mClickCountStack.push(clicksUsed); 184 mListView.setMaxPages(mListView.getMaxPages() - clicksUsed); 185 mCarMenuCallbacks.onItemClicked(id); 186 if ((flags & FLAG_BROWSABLE) != 0) { 187 if (mListView.getMaxPages() == 0) { 188 mIsCapped = true; 189 } 190 CharSequence title = item.getString(KEY_TITLE); 191 if (TextUtils.isEmpty(title)) { 192 title = mContentTitle; 193 } 194 mUiEntry.setTitleText(title); 195 mTitles.push(title); 196 if (!mSubscriptionIds.isEmpty()) { 197 mPlvAnimationController.enqueueExitAnimation(mClearAdapterRunnable); 198 } 199 mProgressBar.setVisibility(View.VISIBLE); 200 if (!mSubscriptionIds.isEmpty()) { 201 mCarMenuCallbacks.unsubscribe(mSubscriptionIds.peek(), mSubscriptionCallbacks); 202 } 203 mSubscriptionIds.push(id); 204 subscribe(id); 205 } else { 206 closeDrawer(); 207 } 208 } 209 210 @Override onItemLongClicked(Bundle item)211 public boolean onItemLongClicked(Bundle item) { 212 return mCarMenuCallbacks.onItemLongClicked(item.getString(KEY_ID)); 213 } 214 215 @Override onBack()216 public void onBack() { 217 backOrClose(); 218 } 219 220 @Override onScroll()221 public boolean onScroll() { 222 // Consume scroll event if we are animating. 223 return mPlvAnimationController.isAnimating(); 224 } 225 setTitle(CharSequence title)226 public void setTitle(CharSequence title) { 227 Log.d(TAG, "setTitle in drawer" + title); 228 if (!TextUtils.isEmpty(title)) { 229 mContentTitle = title; 230 mUiEntry.showTitle(); 231 mUiEntry.setTitleText(title); 232 } else { 233 mUiEntry.hideTitle(); 234 } 235 } 236 setRootAndCallbacks(String rootId, CarMenuCallbacks callbacks)237 public void setRootAndCallbacks(String rootId, CarMenuCallbacks callbacks) { 238 mAdapter = new DrawerApiAdapter(); 239 mAdapter.setItemSelectedListener(this); 240 mListView.setAdapter(mAdapter); 241 mCarMenuCallbacks = callbacks; 242 // HACK: Due to the handler, setRootId will be called after onRestoreState. 243 // If onRestoreState has been called, the root id will already be set. So nothing to do. 244 if (mSubscriptionIds.isEmpty()) { 245 setRootId(rootId); 246 } else { 247 subscribe(mSubscriptionIds.peek()); 248 openDrawer(); 249 } 250 } 251 saveState(Bundle out)252 public void saveState(Bundle out) { 253 out.putStringArray(KEY_IDS, mSubscriptionIds.toArray(new String[mSubscriptionIds.size()])); 254 out.putStringArray(KEY_TITLES, mTitles.toArray(new String[mTitles.size()])); 255 out.putString(KEY_ROOT, mRootId); 256 out.putBoolean(KEY_DRAWERSTATE, mIsDrawerOpen); 257 out.putIntegerArrayList(KEY_CLICK_STACK, new ArrayList<Integer>(mClickCountStack)); 258 out.putBoolean(KEY_IS_CAPPED, mIsCapped); 259 out.putInt(KEY_MAX_PAGES, mListView.getMaxPages()); 260 } 261 restoreState(Bundle in)262 public void restoreState(Bundle in) { 263 if (in != null) { 264 // Restore subscribed CarMenu ids 265 String[] ids = in.getStringArray(KEY_IDS); 266 mSubscriptionIds.clear(); 267 if (ids != null) { 268 mSubscriptionIds.addAll(Arrays.asList(ids)); 269 } 270 // Restore drawer titles if there are any 271 String[] titles = in.getStringArray(KEY_TITLES); 272 mTitles.clear(); 273 if (titles != null) { 274 mTitles.addAll(Arrays.asList(titles)); 275 } 276 if (!mTitles.isEmpty()) { 277 mUiEntry.setTitleText(mTitles.peek()); 278 } 279 mRootId = in.getString(KEY_ROOT); 280 mIsDrawerOpen = in.getBoolean(KEY_DRAWERSTATE); 281 ArrayList<Integer> clickCount = in.getIntegerArrayList(KEY_CLICK_STACK); 282 mClickCountStack.clear(); 283 if (clickCount != null) { 284 mClickCountStack.addAll(clickCount); 285 } 286 mIsCapped = in.getBoolean(KEY_IS_CAPPED); 287 mListView.setMaxPages(in.getInt(KEY_MAX_PAGES)); 288 if (!mRestartedFromDayNightMode && mIsDrawerOpen) { 289 closeDrawer(); 290 } 291 } 292 } 293 setScrimColor(int color)294 public void setScrimColor(int color) { 295 mScrimColor = color; 296 mContainer.setScrimColor(color); 297 updateViewFaders(); 298 } 299 setAutoLightDarkMode()300 public void setAutoLightDarkMode() { 301 mDrawerMode = MODE_AUTO; 302 mContainer.setAutoDayNightMode(); 303 updateViewFaders(); 304 } 305 setLightMode()306 public void setLightMode() { 307 mDrawerMode = MODE_LIGHT; 308 mContainer.setLightMode(); 309 updateViewFaders(); 310 } 311 setDarkMode()312 public void setDarkMode() { 313 mDrawerMode = MODE_DARK; 314 mContainer.setDarkMode(); 315 updateViewFaders(); 316 } 317 openDrawer()318 public void openDrawer() { 319 // If we have no root, then we can't open the drawer. 320 if (mRootId == null) { 321 return; 322 } 323 mContainer.openDrawer(); 324 } 325 closeDrawer()326 public void closeDrawer() { 327 if (mRootId == null) { 328 return; 329 } 330 mTruncatedListCardView.setVisibility(View.GONE); 331 mPlvAnimationController.stopAndClearAnimations(); 332 mContainer.closeDrawer(); 333 mUiEntry.setTitle(mContentTitle); 334 } 335 setDrawerEnabled(boolean enabled)336 public void setDrawerEnabled(boolean enabled) { 337 if (enabled) { 338 mContainer.setDrawerLockMode(CarDrawerLayout.LOCK_MODE_UNLOCKED); 339 } else { 340 mContainer.setDrawerLockMode(CarDrawerLayout.LOCK_MODE_LOCKED_CLOSED); 341 } 342 } 343 showMenu(String id, String title)344 public void showMenu(String id, String title) { 345 // The app wants to show the menu associated with the given id. Create a fake item using the 346 // given inputs and then pretend as if the user clicked on the item, so that the drawer 347 // will subscribe to that menu id, set the title appropriately, and properly handle the 348 // subscription stack. 349 Bundle bundle = new Bundle(); 350 bundle.putString(KEY_ID, id); 351 bundle.putString(KEY_TITLE, title); 352 bundle.putInt(KEY_FLAGS, FLAG_BROWSABLE); 353 onItemClicked(bundle, 0 /* position */); 354 } 355 setRootId(String rootId)356 public void setRootId(String rootId) { 357 mRootId = rootId; 358 } 359 setRestartedFromDayNightMode(boolean restarted)360 public void setRestartedFromDayNightMode(boolean restarted) { 361 mRestartedFromDayNightMode = restarted; 362 } 363 isTruncatedList()364 public boolean isTruncatedList() { 365 return mItemsNumber > mAdapter.getMaxItemsNumber(); 366 } 367 clearMenu()368 private void clearMenu() { 369 if (!mSubscriptionIds.isEmpty()) { 370 mCarMenuCallbacks.unsubscribe(mSubscriptionIds.peek(), mSubscriptionCallbacks); 371 mSubscriptionIds.clear(); 372 mTitles.clear(); 373 } 374 mListView.setVisibility(View.GONE); 375 mListView.resetMaxPages(); 376 mClickCountStack.clear(); 377 mIsCapped = false; 378 } 379 380 /** 381 * Check if the drawer is inside of a CarAppLayout and add the relevant views if it is, 382 * automagically add view faders for the correct views 383 */ updateViewFaders()384 private void updateViewFaders() { 385 mContainer.removeViewFader(mStatusViewViewFader); 386 mContainer.addViewFader(mStatusViewViewFader); 387 } 388 subscribe(String id)389 private void subscribe(String id) { 390 mProgressBar.setVisibility(View.VISIBLE); 391 mCarMenuCallbacks.subscribe(id, mSubscriptionCallbacks); 392 } 393 394 private final CarDrawerLayout.ViewFader mStatusViewViewFader = new CarDrawerLayout.ViewFader() { 395 @Override 396 public void setColor(int color) { 397 mUiEntry.setMenuButtonColor(color); 398 } 399 }; 400 backOrClose()401 private void backOrClose() { 402 if (mSubscriptionIds.size() > 1) { 403 mPlvAnimationController.enqueueBackAnimation(mClearAdapterRunnable); 404 mProgressBar.setVisibility(View.VISIBLE); 405 mCarMenuCallbacks.unsubscribe(mSubscriptionIds.pop(), 406 mSubscriptionCallbacks); 407 subscribe(mSubscriptionIds.peek()); 408 // Restore the title for this menu level. 409 mTitles.pop(); 410 CharSequence title = mTitles.peek(); 411 if (TextUtils.isEmpty(title)) { 412 title = mContentTitle; 413 } 414 mUiEntry.setTitleText(title); 415 } else { 416 closeDrawer(); 417 } 418 } 419 420 private final View.OnClickListener mMenuClickListener = new View.OnClickListener() { 421 @Override 422 public void onClick(View view) { 423 if (mIsDrawerAnimating || mCarMenuCallbacks.onMenuClicked()) { 424 return; 425 } 426 // Check if drawer has root set. 427 if (mRootId == null) { 428 return; 429 } 430 mTruncatedListCardView.setVisibility(View.GONE); 431 if (mIsDrawerOpen) { 432 if (!mClickCountStack.isEmpty()) { 433 mListView.setMaxPages(mListView.getMaxPages() + mClickCountStack.pop()); 434 } 435 mIsCapped = false; 436 backOrClose(); 437 } else { 438 mSubscriptionIds.push(mRootId); 439 mTitles.push(mContentTitle); 440 subscribe(mRootId); 441 openDrawer(); 442 } 443 } 444 }; 445 446 private final Runnable mClearAdapterRunnable = new Runnable() { 447 @Override 448 public void run() { 449 mListView.setVisibility(View.GONE); 450 } 451 }; 452 updateDayNightMode()453 public void updateDayNightMode() { 454 mContainer.findViewById(R.id.drawer).setBackgroundColor( 455 mContext.getResources().getColor(R.color.car_card)); 456 mListView.setAutoDayNightMode(); 457 switch (mDrawerMode) { 458 case MODE_AUTO: 459 setAutoLightDarkMode(); 460 break; 461 case MODE_LIGHT: 462 setLightMode(); 463 break; 464 case MODE_DARK: 465 setDarkMode(); 466 break; 467 } 468 updateViewFaders(); 469 RecyclerView rv = mListView.getRecyclerView(); 470 for (int i = 0; i < mAdapter.getItemCount(); ++i) { 471 mAdapter.setDayNightModeColors(rv.findViewHolderForAdapterPosition(i)); 472 } 473 } 474 475 private static class ViewAnimationController implements Animation.AnimationListener { 476 private final Animation mExitAnim; 477 private final Animation mEnterAnim; 478 private final Animation mBackAnim; 479 private final View mView; 480 private final Context mContext; 481 private final Queue<Animation> mQueue = new LinkedList<>(); 482 483 private Runnable mOnEnterAnimStartRunnable; 484 private Runnable mOnExitAnimCompleteRunnable; 485 486 private Animation mCurrentAnimation; 487 ViewAnimationController(View view, int enter, int exit, int back)488 public ViewAnimationController(View view, int enter, int exit, int back) { 489 mView = view; 490 mContext = view.getContext(); 491 492 mEnterAnim = AnimationUtils.loadAnimation(mContext, enter); 493 mExitAnim = AnimationUtils.loadAnimation(mContext, exit); 494 mBackAnim = AnimationUtils.loadAnimation(mContext, back); 495 496 mExitAnim.setAnimationListener(this); 497 mEnterAnim.setAnimationListener(this); 498 mBackAnim.setAnimationListener(this); 499 } 500 501 @Override onAnimationStart(Animation animation)502 public void onAnimationStart(Animation animation) { 503 if (animation == mEnterAnim && mOnEnterAnimStartRunnable != null) { 504 mOnEnterAnimStartRunnable.run(); 505 mOnEnterAnimStartRunnable = null; 506 } 507 } 508 509 @Override onAnimationEnd(Animation animation)510 public void onAnimationEnd(Animation animation) { 511 if ((animation == mExitAnim || animation == mBackAnim) 512 && mOnExitAnimCompleteRunnable != null) { 513 mOnExitAnimCompleteRunnable.run(); 514 mOnExitAnimCompleteRunnable = null; 515 } 516 Animation nextAnimation = mQueue.poll(); 517 if (nextAnimation != null) { 518 mCurrentAnimation = animation; 519 mView.startAnimation(nextAnimation); 520 } else { 521 mCurrentAnimation = null; 522 } 523 } 524 525 @Override onAnimationRepeat(Animation animation)526 public void onAnimationRepeat(Animation animation) { 527 528 } 529 enqueueEnterAnimation(Runnable r)530 public void enqueueEnterAnimation(Runnable r) { 531 if (r != null) { 532 mOnEnterAnimStartRunnable = r; 533 } 534 enqueueAnimation(mEnterAnim); 535 } 536 enqueueExitAnimation(Runnable r)537 public void enqueueExitAnimation(Runnable r) { 538 // If the view isn't visible, don't play the exit animation. 539 // It will cause flicker. 540 if (mView.getVisibility() != View.VISIBLE) { 541 return; 542 } 543 if (r != null) { 544 mOnExitAnimCompleteRunnable = r; 545 } 546 enqueueAnimation(mExitAnim); 547 } 548 enqueueBackAnimation(Runnable r)549 public void enqueueBackAnimation(Runnable r) { 550 // If the view isn't visible, don't play the back animation. 551 if (mView.getVisibility() != View.VISIBLE) { 552 return; 553 } 554 if (r != null) { 555 mOnExitAnimCompleteRunnable = r; 556 } 557 enqueueAnimation(mBackAnim); 558 } 559 stopAndClearAnimations()560 public synchronized void stopAndClearAnimations() { 561 if (mExitAnim.hasStarted()) { 562 mExitAnim.cancel(); 563 } 564 565 if (mEnterAnim.hasStarted()) { 566 mEnterAnim.cancel(); 567 } 568 569 mQueue.clear(); 570 mCurrentAnimation = null; 571 } 572 isAnimating()573 public boolean isAnimating() { 574 return mCurrentAnimation != null; 575 } 576 enqueueAnimation(final Animation animation)577 private synchronized void enqueueAnimation(final Animation animation) { 578 if (mQueue.contains(animation)) { 579 return; 580 } 581 if (mCurrentAnimation != null) { 582 mQueue.add(animation); 583 } else { 584 mCurrentAnimation = animation; 585 mView.startAnimation(animation); 586 } 587 } 588 } 589 590 private class SubscriptionCallbacks extends android.car.app.menu.SubscriptionCallbacks { 591 private final Object mItemLock = new Object(); 592 private volatile List<Bundle> mItems; 593 594 @Override onChildrenLoaded(String parentId, final List<Bundle> items)595 public void onChildrenLoaded(String parentId, final List<Bundle> items) { 596 if (mSubscriptionIds.isEmpty() || parentId.equals(mSubscriptionIds.peek())) { 597 // Add unavailable category explanation at the first item of menu. 598 if (mIsCapped) { 599 Bundle extra = new Bundle(); 600 extra.putString(KEY_ID, KEY_ID_UNAVAILABLE_CATEGORY); 601 items.add(0, extra); 602 } 603 mItems = items; 604 mItemsNumber = mItems.size(); 605 mProgressBar.setVisibility(View.GONE); 606 mPlvAnimationController.enqueueEnterAnimation(new Runnable() { 607 @Override 608 public void run() { 609 synchronized (mItemLock) { 610 mAdapter.setItems(mItems, mIsCapped); 611 mListView.setVisibility(View.VISIBLE); 612 mItems = null; 613 } 614 mListView.scrollToPosition(mAdapter.getFirstItemIndex()); 615 } 616 }); 617 } 618 } 619 620 @Override onError(String id)621 public void onError(String id) { 622 // TODO: do something useful here. 623 } 624 625 @Override onChildChanged(String parentId, Bundle bundle)626 public void onChildChanged(String parentId, Bundle bundle) { 627 if (!mSubscriptionIds.isEmpty() && parentId.equals(mSubscriptionIds.peek())) { 628 // List is still animating, so adapter hasn't been updated. Update the list that 629 // needs to be set. 630 String id = bundle.getString(KEY_ID); 631 synchronized (mItemLock) { 632 if (mItems != null) { 633 for (Bundle item : mItems) { 634 if (item.getString(KEY_ID).equals(id)) { 635 item.putAll(bundle); 636 break; 637 } 638 } 639 return; 640 } 641 } 642 RecyclerView rv = mListView.getRecyclerView(); 643 RecyclerView.ViewHolder holder = rv.findViewHolderForItemId(id.hashCode()); 644 mAdapter.onChildChanged(holder, bundle); 645 } 646 } 647 } 648 649 private class DrawerMenuListDecoration extends PagedListView.Decoration { 650 DrawerMenuListDecoration(Context context)651 public DrawerMenuListDecoration(Context context) { 652 super(context); 653 } 654 655 @Override onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)656 public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { 657 if (mAdapter != null && mAdapter.isEmptyPlaceholder()) { 658 return; 659 } 660 super.onDrawOver(c, parent, state); 661 } 662 } 663 } 664