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.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