summaryrefslogtreecommitdiff
path: root/android/support/car/drawer/CarDrawerController.java
blob: 7b23714c3303410389acce63e5923fa0672d36bc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.support.car.drawer;

import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.annotation.AnimRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.car.R;
import android.support.car.widget.PagedListView;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.widget.ProgressBar;

import java.util.Stack;

/**
 * A controller that will handle the set up of the navigation drawer. It will hook up the
 * necessary buttons for up navigation, as well as expose methods to allow for a drill down
 * navigation.
 */
public class CarDrawerController {
    /** An animation for when a user navigates into a submenu. */
    @AnimRes
    private static final int DRILL_DOWN_ANIM = R.anim.fade_in_trans_right_layout_anim;

    /** An animation for when a user navigates up (when the back button is pressed). */
    @AnimRes
    private static final int NAVIGATE_UP_ANIM = R.anim.fade_in_trans_left_layout_anim;

    /** The amount that the drawer has been opened before its color should be switched. */
    private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f;

    /**
     * A representation of the hierarchy of navigation being displayed in the list. The ordering of
     * this stack is the order that the user has visited each level. When the user navigates up,
     * the adapters are popped from this list.
     */
    private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>();

    private final Context mContext;

    private final Toolbar mToolbar;
    private final DrawerLayout mDrawerLayout;
    private final ActionBarDrawerToggle mDrawerToggle;

    private final PagedListView mDrawerList;
    private final ProgressBar mProgressBar;
    private final View mDrawerContent;

    /**
     * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by
     * {@code drawerLayout}.
     *
     * <p>The given {@code drawerLayout} should either have a child View that is inflated from
     * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that
     * layout.
     *
     * @param toolbar The {@link Toolbar} that will serve as the action bar for an Activity.
     * @param drawerLayout The top-level container for the window content that shows the
     * interactive drawer.
     * @param drawerToggle The {@link ActionBarDrawerToggle} that bridges the given {@code toolbar}
     * and {@code drawerLayout}.
     */
    public CarDrawerController(Toolbar toolbar,
            DrawerLayout drawerLayout,
            ActionBarDrawerToggle drawerToggle) {
        mToolbar = toolbar;
        mContext = drawerLayout.getContext();
        mDrawerToggle = drawerToggle;
        mDrawerLayout = drawerLayout;

        mDrawerContent = drawerLayout.findViewById(R.id.drawer_content);
        mDrawerList = drawerLayout.findViewById(R.id.drawer_list);
        mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED);
        mProgressBar = drawerLayout.findViewById(R.id.drawer_progress);

        setupDrawerToggling();
    }

    /**
     * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of
     * this root adapter are shown when the drawer is first opened. It is also the top-most level of
     * navigation in the drawer.
     *
     * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then
     *                    this method will do nothing.
     */
    public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) {
        if (rootAdapter == null) {
            return;
        }

        // The root adapter is always the last item in the stack.
        if (mAdapterStack.size() > 0) {
            mAdapterStack.set(0, rootAdapter);
        } else {
            mAdapterStack.push(rootAdapter);
        }

        setToolbarTitleFrom(rootAdapter);
        mDrawerList.setAdapter(rootAdapter);
    }

    /**
     * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display
     * in the navigation drawer. The title will also be updated from the adapter.
     *
     * <p>This switch is treated as a navigation to the next level in the drawer. Navigation away
     * from this level will pop the given adapter off and surface contents of the previous adapter
     * that was set via this method. If no such adapter exists, then the root adapter set by
     * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead.
     *
     * @param adapter Adapter for next level of content in the drawer.
     */
    public final void pushAdapter(CarDrawerAdapter adapter) {
        mAdapterStack.peek().setTitleChangeListener(null);
        mAdapterStack.push(adapter);
        setDisplayAdapter(adapter);
        runLayoutAnimation(DRILL_DOWN_ANIM);
    }

    /** Close the drawer. */
    public void closeDrawer() {
        if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
            mDrawerLayout.closeDrawer(Gravity.LEFT);
        }
    }

    /** Opens the drawer. */
    public void openDrawer() {
        if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
            mDrawerLayout.openDrawer(Gravity.LEFT);
        }
    }

    /** Sets a listener to be notified of Drawer events. */
    public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
        mDrawerLayout.addDrawerListener(listener);
    }

    /** Removes a listener to be notified of Drawer events. */
    public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
        mDrawerLayout.removeDrawerListener(listener);
    }

    /**
     * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true},
     * the progress bar is displayed and the navigation list is hidden and vice versa.
     */
    public void showLoadingProgressBar(boolean show) {
        mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE);
        mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    /** Scroll to given position in the list. */
    public void scrollToPosition(int position) {
        mDrawerList.getRecyclerView().smoothScrollToPosition(position);
    }

    /**
     * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this
     * controller's internal Toolbar.
     */
    private void setToolbarTitleFrom(CarDrawerAdapter adapter) {
        if (adapter.getTitle() == null) {
            throw new RuntimeException("CarDrawerAdapter must supply a title via setTitle()");
        }

        mToolbar.setTitle(adapter.getTitle());
        adapter.setTitleChangeListener(mToolbar::setTitle);
    }

    /**
     * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer
     * hierarchy is properly displayed.
     */
    private void setupDrawerToggling() {
        mDrawerLayout.addDrawerListener(mDrawerToggle);
        mDrawerLayout.addDrawerListener(
                new DrawerLayout.DrawerListener() {
                    @Override
                    public void onDrawerSlide(View drawerView, float slideOffset) {
                        // Correctly set the title and arrow colors as they are different between
                        // the open and close states.
                        updateTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET);
                    }

                    @Override
                    public void onDrawerClosed(View drawerView) {
                        // If drawer is closed, revert stack/drawer to initial root state.
                        cleanupStackAndShowRoot();
                        scrollToPosition(0);
                    }

                    @Override
                    public void onDrawerOpened(View drawerView) {}

                    @Override
                    public void onDrawerStateChanged(int newState) {}
                });
    }

    /** Sets the title and arrow color of the drawer depending on if it is open or not. */
    private void updateTitleAndArrowColor(boolean drawerOpen) {
        // When the drawer is open, use car_title, which resolves to appropriate color depending on
        // day-night mode. When drawer is closed, we always use light color.
        int titleColorResId = drawerOpen ? R.color.car_title : R.color.car_title_light;
        int titleColor = mContext.getColor(titleColorResId);
        mToolbar.setTitleTextColor(titleColor);
        mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor);
    }

    /**
     * Synchronizes the display of the drawer with its linked {@link DrawerLayout}.
     *
     * <p>This should be called from the associated Activity's
     * {@link android.support.v7.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize
     * after teh DRawerLayout's instance state has been restored, and any other time when the
     * state may have diverged in such a way that this controller's associated
     * {@link ActionBarDrawerToggle} had not been notified.
     */
    public void syncState() {
        mDrawerToggle.syncState();

        // In case we're restarting after a config change (e.g. day, night switch), set colors
        // again. Doing it here so that Drawer state is fully synced and we know if its open or not.
        // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout.
        updateTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent));
    }

    /**
     * Notify this controller that device configurations may have changed.
     *
     * <p>This method should be called from the associated Activity's
     * {@code onConfigurationChanged()} method.
     */
    public void onConfigurationChanged(Configuration newConfig) {
        // Pass any configuration change to the drawer toggle.
        mDrawerToggle.onConfigurationChanged(newConfig);
    }

    /**
     * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called
     * when the Activity's method is called and will return {@code true} if the selection has
     * been handled.
     *
     * @return {@code true} if the item processing was handled by this class.
     */
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle home-click and see if we can navigate up in the drawer.
        if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) {
            return true;
        }

        // DrawerToggle gets next chance to handle up-clicks (and any other clicks).
        return mDrawerToggle.onOptionsItemSelected(item);
    }

    /**
     * Sets the given adapter as the one displaying the current contents of the drawer.
     *
     * <p>The drawer's title will also be derived from the given adapter.
     */
    private void setDisplayAdapter(CarDrawerAdapter adapter) {
        setToolbarTitleFrom(adapter);
        // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between
        // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts.
        mDrawerList.getRecyclerView().setAdapter(adapter);
    }

    /**
     * Switches to the previous level in the drawer hierarchy if the current list being displayed
     * is not the root adapter. This is analogous to a navigate up.
     *
     * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise.
     */
    private boolean maybeHandleUpClick() {
        // Check if already at the root level.
        if (mAdapterStack.size() <= 1) {
            return false;
        }

        CarDrawerAdapter adapter = mAdapterStack.pop();
        adapter.setTitleChangeListener(null);
        adapter.cleanup();
        setDisplayAdapter(mAdapterStack.peek());
        runLayoutAnimation(NAVIGATE_UP_ANIM);
        return true;
    }

    /** Clears stack down to root adapter and switches to root adapter. */
    private void cleanupStackAndShowRoot() {
        while (mAdapterStack.size() > 1) {
            CarDrawerAdapter adapter = mAdapterStack.pop();
            adapter.setTitleChangeListener(null);
            adapter.cleanup();
        }
        setDisplayAdapter(mAdapterStack.peek());
        runLayoutAnimation(NAVIGATE_UP_ANIM);
    }

    /**
     * Runs the given layout animation on the PagedListView. Running this animation will also
     * refresh the contents of the list.
     */
    private void runLayoutAnimation(@AnimRes int animation) {
        RecyclerView recyclerView = mDrawerList.getRecyclerView();
        recyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, animation));
        recyclerView.getAdapter().notifyDataSetChanged();
        recyclerView.scheduleLayoutAnimation();
    }
}