1/* 2 * Copyright (C) 2017 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 androidx.car.drawer; 18 19import android.content.Context; 20import android.content.res.Configuration; 21import android.os.Bundle; 22import android.view.Gravity; 23import android.view.MenuItem; 24import android.view.View; 25import android.view.animation.AnimationUtils; 26import android.widget.ProgressBar; 27import android.widget.TextView; 28 29import androidx.annotation.AnimRes; 30import androidx.annotation.NonNull; 31import androidx.annotation.Nullable; 32import androidx.appcompat.app.ActionBarDrawerToggle; 33import androidx.appcompat.widget.Toolbar; 34import androidx.car.R; 35import androidx.car.widget.PagedListView; 36import androidx.drawerlayout.widget.DrawerLayout; 37import androidx.recyclerview.widget.RecyclerView; 38 39import java.util.ArrayDeque; 40 41/** 42 * A controller that will handle the set up of the navigation drawer. It will hook up the 43 * necessary buttons for up navigation, as well as expose methods to allow for a drill down 44 * navigation. 45 */ 46public class CarDrawerController { 47 /** An animation for when a user navigates into a submenu. */ 48 @AnimRes 49 private static final int DRILL_DOWN_ANIM = R.anim.fade_in_trans_right_layout_anim; 50 51 /** An animation for when a user navigates up (when the back button is pressed). */ 52 @AnimRes 53 private static final int NAVIGATE_UP_ANIM = R.anim.fade_in_trans_left_layout_anim; 54 55 /** 56 * A representation of the hierarchy of navigation being displayed in the list. The ordering of 57 * this stack is the order that the user has visited each level. When the user navigates up, 58 * the adapters are popped from this list. 59 */ 60 private final ArrayDeque<CarDrawerAdapter> mAdapterStack = new ArrayDeque<>(); 61 62 private final Context mContext; 63 64 private final TextView mTitleView; 65 private final DrawerLayout mDrawerLayout; 66 private final ActionBarDrawerToggle mDrawerToggle; 67 68 private final PagedListView mDrawerList; 69 private final ProgressBar mProgressBar; 70 71 /** 72 * @deprecated Use {@link #CarDrawerController(DrawerLayout, ActionBarDrawerToggle)} instead. 73 * The {@code Toolbar} is no longer needed and will be ignored. 74 */ 75 @Deprecated 76 public CarDrawerController(@Nullable Toolbar toolbar, @NonNull DrawerLayout drawerLayout, 77 @NonNull ActionBarDrawerToggle drawerToggle) { 78 this(drawerLayout, drawerToggle); 79 } 80 81 /** 82 * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by 83 * {@code drawerLayout}. 84 * 85 * <p>The given {@code drawerLayout} should either have a child View that is inflated from 86 * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that 87 * layout. 88 * 89 * @param drawerLayout The top-level container for the window content that shows the 90 * interactive drawer. 91 * @param drawerToggle The {@link ActionBarDrawerToggle} that will open the drawer. 92 */ 93 public CarDrawerController(@NonNull DrawerLayout drawerLayout, 94 @NonNull ActionBarDrawerToggle drawerToggle) { 95 mContext = drawerLayout.getContext(); 96 mDrawerToggle = drawerToggle; 97 mDrawerLayout = drawerLayout; 98 99 mTitleView = drawerLayout.findViewById(R.id.drawer_title); 100 mDrawerList = drawerLayout.findViewById(R.id.drawer_list); 101 mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED); 102 mProgressBar = drawerLayout.findViewById(R.id.drawer_progress); 103 104 drawerLayout.findViewById(R.id.drawer_back_button).setOnClickListener(v -> { 105 if (!maybeHandleUpClick()) { 106 closeDrawer(); 107 } 108 }); 109 110 setupDrawerToggling(); 111 } 112 113 /** 114 * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of 115 * this root adapter are shown when the drawer is first opened. It is also the top-most level of 116 * navigation in the drawer. 117 * 118 * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then 119 * this method will do nothing. 120 */ 121 public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) { 122 if (rootAdapter == null) { 123 return; 124 } 125 126 // The root adapter is always the last item in the stack. 127 if (!mAdapterStack.isEmpty()) { 128 mAdapterStack.removeLast(); 129 } 130 mAdapterStack.addLast(rootAdapter); 131 setDisplayAdapter(rootAdapter); 132 } 133 134 /** 135 * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display 136 * in the navigation drawer. The title will also be updated from the adapter. 137 * 138 * <p>This switch is treated as a navigation to the next level in the drawer. Navigation away 139 * from this level will pop the given adapter off and surface contents of the previous adapter 140 * that was set via this method. If no such adapter exists, then the root adapter set by 141 * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead. 142 * 143 * @param adapter Adapter for next level of content in the drawer. 144 */ 145 public final void pushAdapter(CarDrawerAdapter adapter) { 146 mAdapterStack.peek().setTitleChangeListener(null); 147 mAdapterStack.push(adapter); 148 setDisplayAdapter(adapter); 149 runLayoutAnimation(DRILL_DOWN_ANIM); 150 } 151 152 /** Close the drawer. */ 153 public void closeDrawer() { 154 if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { 155 mDrawerLayout.closeDrawer(Gravity.LEFT); 156 } 157 } 158 159 /** Opens the drawer. */ 160 public void openDrawer() { 161 if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { 162 mDrawerLayout.openDrawer(Gravity.LEFT); 163 } 164 } 165 166 /** Sets a listener to be notified of Drawer events. */ 167 public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { 168 mDrawerLayout.addDrawerListener(listener); 169 } 170 171 /** Removes a listener to be notified of Drawer events. */ 172 public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { 173 mDrawerLayout.removeDrawerListener(listener); 174 } 175 176 /** 177 * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true}, 178 * the progress bar is displayed and the navigation list is hidden and vice versa. 179 */ 180 public void showLoadingProgressBar(boolean show) { 181 mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE); 182 mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE); 183 } 184 185 /** Scroll to given position in the list. */ 186 public void scrollToPosition(int position) { 187 mDrawerList.getRecyclerView().smoothScrollToPosition(position); 188 } 189 190 /** 191 * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this 192 * controller's internal Toolbar. 193 */ 194 private void setToolbarTitleFrom(CarDrawerAdapter adapter) { 195 mTitleView.setText(adapter.getTitle()); 196 adapter.setTitleChangeListener(mTitleView::setText); 197 } 198 199 /** 200 * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer 201 * hierarchy is properly displayed. 202 */ 203 private void setupDrawerToggling() { 204 mDrawerLayout.addDrawerListener(mDrawerToggle); 205 mDrawerLayout.addDrawerListener( 206 new DrawerLayout.DrawerListener() { 207 @Override 208 public void onDrawerSlide(View drawerView, float slideOffset) {} 209 210 @Override 211 public void onDrawerClosed(View drawerView) { 212 // If drawer is closed, revert stack/drawer to initial root state. 213 cleanupStackAndShowRoot(); 214 scrollToPosition(0); 215 } 216 217 @Override 218 public void onDrawerOpened(View drawerView) {} 219 220 @Override 221 public void onDrawerStateChanged(int newState) {} 222 }); 223 } 224 225 /** 226 * Synchronizes the display of the drawer with its linked {@link DrawerLayout}. 227 * 228 * <p>This should be called from the associated Activity's 229 * {@link androidx.appcompat.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize 230 * after teh DRawerLayout's instance state has been restored, and any other time when the 231 * state may have diverged in such a way that this controller's associated 232 * {@link ActionBarDrawerToggle} had not been notified. 233 */ 234 public void syncState() { 235 mDrawerToggle.syncState(); 236 } 237 238 /** 239 * Notify this controller that device configurations may have changed. 240 * 241 * <p>This method should be called from the associated Activity's 242 * {@code onConfigurationChanged()} method. 243 */ 244 public void onConfigurationChanged(Configuration newConfig) { 245 // Pass any configuration change to the drawer toggle. 246 mDrawerToggle.onConfigurationChanged(newConfig); 247 } 248 249 /** 250 * Sets the given adapter as the one displaying the current contents of the drawer. 251 * 252 * <p>The drawer's title will also be derived from the given adapter. 253 */ 254 private void setDisplayAdapter(CarDrawerAdapter adapter) { 255 setToolbarTitleFrom(adapter); 256 // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between 257 // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts. 258 mDrawerList.getRecyclerView().setAdapter(adapter); 259 } 260 261 /** 262 * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called 263 * when the Activity's method is called and will return {@code true} if the selection has 264 * been handled. 265 * 266 * @return {@code true} if the item processing was handled by this class. 267 */ 268 public boolean onOptionsItemSelected(MenuItem item) { 269 // Handle home-click and see if we can navigate up in the drawer. 270 if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) { 271 return true; 272 } 273 274 // DrawerToggle gets next chance to handle up-clicks (and any other clicks). 275 return mDrawerToggle.onOptionsItemSelected(item); 276 } 277 278 /** 279 * Switches to the previous level in the drawer hierarchy if the current list being displayed 280 * is not the root adapter. This is analogous to a navigate up. 281 * 282 * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise. 283 */ 284 private boolean maybeHandleUpClick() { 285 // Check if already at the root level. 286 if (mAdapterStack.size() <= 1) { 287 return false; 288 } 289 290 CarDrawerAdapter adapter = mAdapterStack.pop(); 291 adapter.setTitleChangeListener(null); 292 adapter.cleanup(); 293 setDisplayAdapter(mAdapterStack.peek()); 294 runLayoutAnimation(NAVIGATE_UP_ANIM); 295 return true; 296 } 297 298 /** Clears stack down to root adapter and switches to root adapter. */ 299 private void cleanupStackAndShowRoot() { 300 while (mAdapterStack.size() > 1) { 301 CarDrawerAdapter adapter = mAdapterStack.pop(); 302 adapter.setTitleChangeListener(null); 303 adapter.cleanup(); 304 } 305 setDisplayAdapter(mAdapterStack.peek()); 306 runLayoutAnimation(NAVIGATE_UP_ANIM); 307 } 308 309 /** 310 * Runs the given layout animation on the PagedListView. Running this animation will also 311 * refresh the contents of the list. 312 */ 313 private void runLayoutAnimation(@AnimRes int animation) { 314 RecyclerView recyclerView = mDrawerList.getRecyclerView(); 315 recyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, animation)); 316 recyclerView.getAdapter().notifyDataSetChanged(); 317 recyclerView.scheduleLayoutAnimation(); 318 } 319} 320