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.car.ui.PagedListView; 24 import android.support.v4.widget.DrawerLayout; 25 import android.support.v7.app.ActionBarDrawerToggle; 26 import android.support.v7.app.AppCompatActivity; 27 import android.support.v7.widget.Toolbar; 28 import android.view.Gravity; 29 import android.view.LayoutInflater; 30 import android.view.MenuItem; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.ProgressBar; 34 35 import com.android.car.stream.ui.R; 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 private void setToolbarTitleFrom(CarDrawerAdapter adapter) { 98 if (adapter.getTitle() != null) { 99 mToolbar.setTitle(adapter.getTitle()); 100 } else { 101 throw new RuntimeException("CarDrawerAdapter subclass must supply title via " + 102 " setTitle()"); 103 } 104 adapter.setTitleChangeListener(mToolbar::setTitle); 105 } 106 107 /** 108 * Set main content to display in this Activity. It will be added to R.id.content_frame in 109 * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}. 110 * 111 * @param view View to display as main content. 112 */ 113 public void setMainContent(View view) { 114 ViewGroup parent = (ViewGroup) findViewById(getContentContainerId()); 115 parent.addView(view); 116 } 117 118 /** 119 * Set main content to display in this Activity. It will be added to R.id.content_frame in 120 * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}. 121 * 122 * @param resourceId Layout to display as main content. 123 */ 124 public void setMainContent(@LayoutRes int resourceId) { 125 ViewGroup parent = (ViewGroup) findViewById(getContentContainerId()); 126 LayoutInflater inflater = getLayoutInflater(); 127 inflater.inflate(resourceId, parent, true); 128 } 129 130 /** 131 * @return Adapter for root content of the Drawer. 132 */ 133 protected abstract CarDrawerAdapter getRootAdapter(); 134 135 /** 136 * Used to pass in next level of items to display in the Drawer, including updated title. It is 137 * pushed on top of the existing adapter in a stack. Navigating up from this level later will 138 * pop this adapter off and surface contents of the next adapter at the top of the stack (and 139 * its title). 140 * 141 * @param adapter Adapter for next level of content in the drawer. 142 */ 143 public final void switchToAdapter(CarDrawerAdapter adapter) { 144 mAdapterStack.peek().setTitleChangeListener(null); 145 mAdapterStack.push(adapter); 146 setTitleAndSwitchToAdapter(adapter); 147 } 148 149 /** 150 * Close the drawer if open. 151 */ 152 public void closeDrawer() { 153 if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { 154 mDrawerLayout.closeDrawer(Gravity.LEFT); 155 } 156 } 157 158 /** 159 * Used to open the drawer. 160 */ 161 public void openDrawer() { 162 if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { 163 mDrawerLayout.openDrawer(Gravity.LEFT); 164 } 165 } 166 167 /** 168 * @param listener Listener to be notified of Drawer events. 169 */ 170 public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { 171 mDrawerLayout.addDrawerListener(listener); 172 } 173 174 /** 175 * @param listener Listener to be notified of Drawer events. 176 */ 177 public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { 178 mDrawerLayout.removeDrawerListener(listener); 179 } 180 181 /** 182 * Used to switch between the Drawer PagedListView and the "loading" progress-bar while the next 183 * level's adapter contents are being fetched. 184 * 185 * @param enable If true, the progress-bar is displayed. If false, the Drawer PagedListView is 186 * added. 187 */ 188 public void showLoadingProgressBar(boolean enable) { 189 mDrawerList.setVisibility(enable ? View.INVISIBLE : View.VISIBLE); 190 mProgressBar.setVisibility(enable ? View.VISIBLE : View.GONE); 191 } 192 193 /** 194 * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own 195 * content/fragments inside here. 196 * 197 * @return Id of FrameLayout where main content of the subclass Activity can be added. 198 */ 199 protected int getContentContainerId() { 200 return R.id.content_frame; 201 } 202 203 private void setupDrawerToggling() { 204 mDrawerToggle = new ActionBarDrawerToggle( 205 this, /* host Activity */ 206 mDrawerLayout, /* DrawerLayout object */ 207 // The string id's below are for accessibility. However 208 // since they won't be used in cars, we just pass car_drawer_unused. 209 R.string.car_drawer_unused, 210 R.string.car_drawer_unused 211 ); 212 mDrawerLayout.addDrawerListener(mDrawerToggle); 213 mDrawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { 214 @Override 215 public void onDrawerSlide(View drawerView, float slideOffset) { 216 setTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET); 217 } 218 @Override 219 public void onDrawerOpened(View drawerView) {} 220 @Override 221 public void onDrawerClosed(View drawerView) { 222 // If drawer is closed for any reason, revert stack/drawer to initial root state. 223 cleanupStackAndShowRoot(); 224 mDrawerList.getRecyclerView().scrollToPosition(0); 225 } 226 @Override 227 public void onDrawerStateChanged(int newState) {} 228 }); 229 getSupportActionBar().setDisplayHomeAsUpEnabled(true); 230 getSupportActionBar().setHomeButtonEnabled(true); 231 } 232 233 private void setTitleAndArrowColor(boolean drawerOpen) { 234 // When drawer open, use car_title, which resolves to appropriate color depending on 235 // day-night mode. When drawer is closed, we always use light color. 236 int titleColorResId = drawerOpen ? 237 R.color.car_title : R.color.car_title_light; 238 int titleColor = getColor(titleColorResId); 239 mToolbar.setTitleTextColor(titleColor); 240 mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor); 241 } 242 243 @Override 244 protected void onPostCreate(Bundle savedInstanceState) { 245 super.onPostCreate(savedInstanceState); 246 // Sync the toggle state after onRestoreInstanceState has occurred. 247 mDrawerToggle.syncState(); 248 249 // In case we're restarting after a config change (e.g. day, night switch), set colors 250 // again. Doing it here so that Drawer state is fully synced and we know if its open or not. 251 // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout. 252 setTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent)); 253 } 254 255 @Override 256 public void onConfigurationChanged(Configuration newConfig) { 257 super.onConfigurationChanged(newConfig); 258 // Pass any configuration change to the drawer toggls 259 mDrawerToggle.onConfigurationChanged(newConfig); 260 } 261 262 @Override 263 public boolean onOptionsItemSelected(MenuItem item) { 264 // Handle home-click and see if we can navigate up in the drawer. 265 if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) { 266 return true; 267 } 268 269 // DrawerToggle gets next chance to handle up-clicks (and any other clicks). 270 if (mDrawerToggle.onOptionsItemSelected(item)) { 271 return true; 272 } 273 274 return super.onOptionsItemSelected(item); 275 } 276 277 private void setTitleAndSwitchToAdapter(CarDrawerAdapter adapter) { 278 setToolbarTitleFrom(adapter); 279 // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between 280 // car_menu_list_item_normal, car_menu_list_item_small and car_list_empty layouts. 281 mDrawerList.getRecyclerView().setAdapter(adapter); 282 mDrawerList.getRecyclerView().scrollToPosition(0); 283 } 284 285 private boolean maybeHandleUpClick() { 286 if (mAdapterStack.size() > 1) { 287 CarDrawerAdapter adapter = mAdapterStack.pop(); 288 adapter.setTitleChangeListener(null); 289 adapter.cleanup(); 290 setTitleAndSwitchToAdapter(mAdapterStack.peek()); 291 return true; 292 } 293 return false; 294 } 295 296 /** Clears stack down to root adapter and switches to root adapter. */ 297 private void cleanupStackAndShowRoot() { 298 while (mAdapterStack.size() > 1) { 299 CarDrawerAdapter adapter = mAdapterStack.pop(); 300 adapter.setTitleChangeListener(null); 301 adapter.cleanup(); 302 } 303 setTitleAndSwitchToAdapter(mAdapterStack.peek()); 304 } 305 } 306