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 17 package com.android.car.app; 18 19 import android.content.res.Configuration; 20 import android.os.Bundle; 21 import android.support.annotation.LayoutRes; 22 import android.support.annotation.NonNull; 23 import android.support.v4.widget.DrawerLayout; 24 import android.support.v7.app.ActionBarDrawerToggle; 25 import android.support.v7.app.AppCompatActivity; 26 import android.support.v7.widget.Toolbar; 27 import android.view.Gravity; 28 import android.view.LayoutInflater; 29 import android.view.MenuItem; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.ProgressBar; 33 34 import com.android.car.stream.ui.R; 35 import com.android.car.view.PagedListView; 36 37 import java.util.Stack; 38 39 /** 40 * Common base Activity for car apps that need to present a Drawer. 41 * <p> 42 * This Activity manages the overall layout. To use it sub-classes need to: 43 * <ul> 44 * <li>Provide the root-items for the Drawer by implementing {@link #getRootAdapter()}.</li> 45 * <li>Add their main content using {@link #setMainContent(int)} or 46 * {@link #setMainContent(View)}. They can also add fragments to the main-content container by 47 * obtaining its id using {@link #getContentContainerId()}</li> 48 * </ul> 49 * This class will take care of drawer toggling and display. 50 * <p> 51 * The rootAdapter can implement nested-navigation, in its click-handling, by passing the 52 * CarDrawerAdapter for the next level to {@link #switchToAdapter(CarDrawerAdapter)}. This 53 * activity will maintain a stack of such adapters. When the user navigates up, it will pop the top 54 * adapter off and display its contents again. 55 * <p> 56 * Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a 57 * derivative. 58 * <p> 59 * NOTE: This version is based on a regular Activity unlike car-support-lib's CarDrawerActivity 60 * which is based on CarActivity. 61 */ 62 public abstract class CarDrawerActivity extends AppCompatActivity { 63 private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f; 64 65 private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>(); 66 private DrawerLayout mDrawerLayout; 67 private PagedListView mDrawerList; 68 private ProgressBar mProgressBar; 69 private View mDrawerContent; 70 private Toolbar mToolbar; 71 private ActionBarDrawerToggle mDrawerToggle; 72 73 @Override 74 protected void onCreate(Bundle savedInstanceState) { 75 super.onCreate(savedInstanceState); 76 77 setContentView(R.layout.car_drawer_activity); 78 mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout); 79 mDrawerContent = findViewById(R.id.drawer_content); 80 mDrawerList = (PagedListView)findViewById(R.id.drawer_list); 81 // Let drawer list show unlimited pages of items. 82 mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED); 83 mProgressBar = (ProgressBar)findViewById(R.id.drawer_progress); 84 85 mToolbar = (Toolbar) findViewById(R.id.main_toolbar); 86 setSupportActionBar(mToolbar); 87 88 // Init drawer adapter stack. 89 CarDrawerAdapter rootAdapter = getRootAdapter(); 90 mAdapterStack.push(rootAdapter); 91 setToolbarTitleFrom(rootAdapter); 92 mDrawerList.setAdapter(rootAdapter); 93 94 setupDrawerToggling(); 95 } 96 97 @Override 98 protected void onStop() { 99 super.onStop(); 100 mDrawerLayout.closeDrawer(Gravity.LEFT, false /* animation */); 101 } 102 103 private void setToolbarTitleFrom(CarDrawerAdapter adapter) { 104 if (adapter.getTitle() != null) { 105 mToolbar.setTitle(adapter.getTitle()); 106 } else { 107 throw new RuntimeException("CarDrawerAdapter subclass must supply title via " + 108 " setTitle()"); 109 } 110 adapter.setTitleChangeListener(mToolbar::setTitle); 111 } 112 113 /** 114 * Set main content to display in this Activity. It will be added to R.id.content_frame in 115 * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}. 116 * 117 * @param view View to display as main content. 118 */ 119 public void setMainContent(View view) { 120 ViewGroup parent = (ViewGroup) findViewById(getContentContainerId()); 121 parent.addView(view); 122 } 123 124 /** 125 * Set main content to display in this Activity. It will be added to R.id.content_frame in 126 * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}. 127 * 128 * @param resourceId Layout to display as main content. 129 */ 130 public void setMainContent(@LayoutRes int resourceId) { 131 ViewGroup parent = (ViewGroup) findViewById(getContentContainerId()); 132 LayoutInflater inflater = getLayoutInflater(); 133 inflater.inflate(resourceId, parent, true); 134 } 135 136 /** 137 * @return Adapter for root content of the Drawer. 138 */ 139 protected abstract CarDrawerAdapter getRootAdapter(); 140 141 /** 142 * Used to pass in next level of items to display in the Drawer, including updated title. It is 143 * pushed on top of the existing adapter in a stack. Navigating up from this level later will 144 * pop this adapter off and surface contents of the next adapter at the top of the stack (and 145 * its title). 146 * 147 * @param adapter Adapter for next level of content in the drawer. 148 */ 149 public final void switchToAdapter(CarDrawerAdapter adapter) { 150 mAdapterStack.peek().setTitleChangeListener(null); 151 mAdapterStack.push(adapter); 152 setTitleAndSwitchToAdapter(adapter); 153 } 154 155 /** 156 * Close the drawer if open. 157 */ 158 public void closeDrawer() { 159 if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { 160 mDrawerLayout.closeDrawer(Gravity.LEFT); 161 } 162 } 163 164 /** 165 * Used to open the drawer. 166 */ 167 public void openDrawer() { 168 if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { 169 mDrawerLayout.openDrawer(Gravity.LEFT); 170 } 171 } 172 173 /** 174 * @param listener Listener to be notified of Drawer events. 175 */ 176 public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { 177 mDrawerLayout.addDrawerListener(listener); 178 } 179 180 /** 181 * @param listener Listener to be notified of Drawer events. 182 */ 183 public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { 184 mDrawerLayout.removeDrawerListener(listener); 185 } 186 187 /** 188 * Used to switch between the Drawer PagedListView and the "loading" progress-bar while the next 189 * level's adapter contents are being fetched. 190 * 191 * @param enable If true, the progress-bar is displayed. If false, the Drawer PagedListView is 192 * added. 193 */ 194 public void showLoadingProgressBar(boolean enable) { 195 mDrawerList.setVisibility(enable ? View.INVISIBLE : View.VISIBLE); 196 mProgressBar.setVisibility(enable ? View.VISIBLE : View.GONE); 197 } 198 199 /** 200 * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own 201 * content/fragments inside here. 202 * 203 * @return Id of FrameLayout where main content of the subclass Activity can be added. 204 */ 205 protected int getContentContainerId() { 206 return R.id.content_frame; 207 } 208 209 private void setupDrawerToggling() { 210 mDrawerToggle = new ActionBarDrawerToggle( 211 this, /* host Activity */ 212 mDrawerLayout, /* DrawerLayout object */ 213 R.string.car_drawer_open, 214 R.string.car_drawer_close 215 ); 216 mDrawerLayout.addDrawerListener(mDrawerToggle); 217 mDrawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { 218 @Override 219 public void onDrawerSlide(View drawerView, float slideOffset) { 220 setTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET); 221 } 222 @Override 223 public void onDrawerOpened(View drawerView) {} 224 @Override 225 public void onDrawerClosed(View drawerView) { 226 // If drawer is closed for any reason, revert stack/drawer to initial root state. 227 cleanupStackAndShowRoot(); 228 scrollToPosition(0); 229 } 230 @Override 231 public void onDrawerStateChanged(int newState) {} 232 }); 233 getSupportActionBar().setDisplayHomeAsUpEnabled(true); 234 getSupportActionBar().setHomeButtonEnabled(true); 235 } 236 237 private void setTitleAndArrowColor(boolean drawerOpen) { 238 // When drawer open, use car_title, which resolves to appropriate color depending on 239 // day-night mode. When drawer is closed, we always use light color. 240 int titleColorResId = drawerOpen ? 241 R.color.car_title : R.color.car_title_light; 242 int titleColor = getColor(titleColorResId); 243 mToolbar.setTitleTextColor(titleColor); 244 mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor); 245 } 246 247 @Override 248 protected void onPostCreate(Bundle savedInstanceState) { 249 super.onPostCreate(savedInstanceState); 250 // Sync the toggle state after onRestoreInstanceState has occurred. 251 mDrawerToggle.syncState(); 252 253 // In case we're restarting after a config change (e.g. day, night switch), set colors 254 // again. Doing it here so that Drawer state is fully synced and we know if its open or not. 255 // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout. 256 setTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent)); 257 } 258 259 @Override 260 public void onConfigurationChanged(Configuration newConfig) { 261 super.onConfigurationChanged(newConfig); 262 // Pass any configuration change to the drawer toggls 263 mDrawerToggle.onConfigurationChanged(newConfig); 264 } 265 266 @Override 267 public boolean onOptionsItemSelected(MenuItem item) { 268 // Handle home-click and see if we can navigate up in the drawer. 269 if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) { 270 return true; 271 } 272 273 // DrawerToggle gets next chance to handle up-clicks (and any other clicks). 274 if (mDrawerToggle.onOptionsItemSelected(item)) { 275 return true; 276 } 277 278 return super.onOptionsItemSelected(item); 279 } 280 281 private void setTitleAndSwitchToAdapter(CarDrawerAdapter adapter) { 282 setToolbarTitleFrom(adapter); 283 // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between 284 // car_menu_list_item_normal, car_menu_list_item_small and car_list_empty layouts. 285 mDrawerList.getRecyclerView().setAdapter(adapter); 286 scrollToPosition(0); 287 } 288 289 public void scrollToPosition(int position) { 290 mDrawerList.getRecyclerView().smoothScrollToPosition(position); 291 } 292 293 private boolean maybeHandleUpClick() { 294 if (mAdapterStack.size() > 1) { 295 CarDrawerAdapter adapter = mAdapterStack.pop(); 296 adapter.setTitleChangeListener(null); 297 adapter.cleanup(); 298 setTitleAndSwitchToAdapter(mAdapterStack.peek()); 299 return true; 300 } 301 return false; 302 } 303 304 /** Clears stack down to root adapter and switches to root adapter. */ 305 private void cleanupStackAndShowRoot() { 306 while (mAdapterStack.size() > 1) { 307 CarDrawerAdapter adapter = mAdapterStack.pop(); 308 adapter.setTitleChangeListener(null); 309 adapter.cleanup(); 310 } 311 setTitleAndSwitchToAdapter(mAdapterStack.peek()); 312 } 313 } 314