Home | History | Annotate | Download | only in app
      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