1/*
2 * Copyright (C) 2017 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
17package androidx.navigation.ui;
18
19import android.animation.ObjectAnimator;
20import android.animation.ValueAnimator;
21import android.support.annotation.IdRes;
22import android.support.annotation.NonNull;
23import android.support.annotation.Nullable;
24import android.support.design.widget.BottomNavigationView;
25import android.support.design.widget.NavigationView;
26import android.support.v4.view.GravityCompat;
27import android.support.v4.widget.DrawerLayout;
28import android.support.v7.app.ActionBar;
29import android.support.v7.app.ActionBarDrawerToggle;
30import android.support.v7.app.AppCompatActivity;
31import android.support.v7.graphics.drawable.DrawerArrowDrawable;
32import android.text.TextUtils;
33import android.view.Menu;
34import android.view.MenuItem;
35import android.view.ViewParent;
36
37import androidx.navigation.NavController;
38import androidx.navigation.NavDestination;
39import androidx.navigation.NavGraph;
40import androidx.navigation.NavOptions;
41
42/**
43 * Class which hooks up elements typically in the 'chrome' of your application such as global
44 * navigation patterns like a navigation drawer or bottom nav bar with your {@link NavController}.
45 */
46public class NavigationUI {
47
48    // No instances. Static utilities only.
49    private NavigationUI() {
50    }
51
52    /**
53     * Attempt to navigate to the {@link NavDestination} associated with the given MenuItem. This
54     * MenuItem should have been added via one of the helper methods in this class.
55     *
56     * <p>Importantly, it assumes the {@link MenuItem#getItemId() menu item id} matches a valid
57     * {@link NavDestination#getAction(int) action id} or
58     * {@link NavDestination#getId() destination id} to be navigated to.</p>
59     *
60     * @param item The selected MenuItem.
61     * @param navController The NavController that hosts the destination.
62     * @return True if the {@link NavController} was able to navigate to the destination
63     * associated with the given MenuItem.
64     */
65    public static boolean onNavDestinationSelected(@NonNull MenuItem item,
66            @NonNull NavController navController) {
67        return onNavDestinationSelected(item, navController, false);
68    }
69
70    private static boolean onNavDestinationSelected(@NonNull MenuItem item,
71            @NonNull NavController navController, boolean popUp) {
72        NavOptions.Builder builder = new NavOptions.Builder()
73                .setLaunchSingleTop(true)
74                .setEnterAnim(R.anim.nav_default_enter_anim)
75                .setExitAnim(R.anim.nav_default_exit_anim)
76                .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
77                .setPopExitAnim(R.anim.nav_default_pop_exit_anim);
78        if (popUp) {
79            builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
80        }
81        NavOptions options = builder.build();
82        try {
83            //TODO provide proper API instead of using Exceptions as Control-Flow.
84            navController.navigate(item.getItemId(), null, options);
85            return true;
86        } catch (IllegalArgumentException e) {
87            return false;
88        }
89    }
90
91    /**
92     * Handles the Up button by delegating its behavior to the given NavController. This should
93     * generally be called from {@link AppCompatActivity#onSupportNavigateUp()}.
94     * <p>If you do not have a {@link DrawerLayout}, you should call
95     * {@link NavController#navigateUp()} directly.
96     *
97     * @param drawerLayout The DrawerLayout that should be opened if you are on the topmost level
98     *                     of the app.
99     * @param navController The NavController that hosts your content.
100     * @return True if the {@link NavController} was able to navigate up.
101     */
102    public static boolean navigateUp(@Nullable DrawerLayout drawerLayout,
103            @NonNull NavController navController) {
104        if (drawerLayout != null && navController.getCurrentDestination()
105                == findStartDestination(navController.getGraph())) {
106            drawerLayout.openDrawer(GravityCompat.START);
107            return true;
108        } else {
109            return navController.navigateUp();
110        }
111    }
112
113    /**
114     * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use
115     * with a {@link NavController}.
116     *
117     * <p>By calling this method, the title in the action bar will automatically be updated when
118     * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}).
119     *
120     * <p>The action bar will also display the Up button when you are on a non-root destination.
121     * Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button.
122     *
123     * @param activity The activity hosting the action bar that should be kept in sync with changes
124     *                 to the NavController.
125     * @param navController The NavController that supplies the secondary menu. Navigation actions
126 *                      on this NavController will be reflected in the title of the action bar.
127     */
128    public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
129            @NonNull NavController navController) {
130        setupActionBarWithNavController(activity, navController, null);
131    }
132
133    /**
134     * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use
135     * with a {@link NavController}.
136     *
137     * <p>By calling this method, the title in the action bar will automatically be updated when
138     * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}).
139     *
140     * <p>The action bar will also display the Up button when you are on a non-root destination and
141     * the drawer icon when on the root destination, automatically animating between them.
142     * Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button.
143     *  @param activity The activity hosting the action bar that should be kept in sync with changes
144     *                 to the NavController.
145     * @param navController The NavController whose navigation actions will be reflected
146     *                      in the title of the action bar.
147     * @param drawerLayout The DrawerLayout that should be toggled from the home button
148     */
149    public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
150            @NonNull NavController navController,
151            @Nullable DrawerLayout drawerLayout) {
152        navController.addOnNavigatedListener(
153                new ActionBarOnNavigatedListener(activity, drawerLayout));
154    }
155
156    /**
157     * Sets up a {@link NavigationView} for use with a {@link NavController}. This will call
158     * {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected.
159     * The selected item in the NavigationView will automatically be updated when the destination
160     * changes.
161     *
162     * @param navigationView The NavigationView that should be kept in sync with changes to the
163     *                       NavController.
164     * @param navController The NavController that supplies the primary and secondary menu.
165 *                      Navigation actions on this NavController will be reflected in the
166 *                      selected item in the NavigationView.
167     */
168    public static void setupWithNavController(@NonNull final NavigationView navigationView,
169            @NonNull final NavController navController) {
170        navigationView.setNavigationItemSelectedListener(
171                new NavigationView.OnNavigationItemSelectedListener() {
172                    @Override
173                    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
174                        boolean handled = onNavDestinationSelected(item, navController, true);
175                        if (handled) {
176                            ViewParent parent = navigationView.getParent();
177                            if (parent instanceof DrawerLayout) {
178                                ((DrawerLayout) parent).closeDrawer(navigationView);
179                            }
180                        }
181                        return handled;
182                    }
183                });
184        navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
185            @Override
186            public void onNavigated(@NonNull NavController controller,
187                    @NonNull NavDestination destination) {
188                Menu menu = navigationView.getMenu();
189                for (int h = 0, size = menu.size(); h < size; h++) {
190                    MenuItem item = menu.getItem(h);
191                    item.setChecked(matchDestination(destination, item.getItemId()));
192                }
193            }
194        });
195    }
196
197    /**
198     * Sets up a {@link BottomNavigationView} for use with a {@link NavController}. This will call
199     * {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected. The
200     * selected item in the BottomNavigationView will automatically be updated when the destination
201     * changes.
202     *
203     * @param bottomNavigationView The BottomNavigationView that should be kept in sync with
204     *                             changes to the NavController.
205     * @param navController The NavController that supplies the primary menu.
206 *                      Navigation actions on this NavController will be reflected in the
207 *                      selected item in the BottomNavigationView.
208     */
209    public static void setupWithNavController(
210            @NonNull final BottomNavigationView bottomNavigationView,
211            @NonNull final NavController navController) {
212        bottomNavigationView.setOnNavigationItemSelectedListener(
213                new BottomNavigationView.OnNavigationItemSelectedListener() {
214                    @Override
215                    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
216                        return onNavDestinationSelected(item, navController, true);
217                    }
218                });
219        navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
220            @Override
221            public void onNavigated(@NonNull NavController controller,
222                    @NonNull NavDestination destination) {
223                Menu menu = bottomNavigationView.getMenu();
224                for (int h = 0, size = menu.size(); h < size; h++) {
225                    MenuItem item = menu.getItem(h);
226                    if (matchDestination(destination, item.getItemId())) {
227                        item.setChecked(true);
228                    }
229                }
230            }
231        });
232    }
233
234    /**
235     * Determines whether the given <code>destId</code> matches the NavDestination. This handles
236     * both the default case (the destination's id matches the given id) and the nested case where
237     * the given id is a parent/grandparent/etc of the destination.
238     */
239    private static boolean matchDestination(@NonNull NavDestination destination,
240            @IdRes int destId) {
241        NavDestination currentDestination = destination;
242        while (currentDestination.getId() != destId && currentDestination.getParent() != null) {
243            currentDestination = currentDestination.getParent();
244        }
245        return currentDestination.getId() == destId;
246    }
247
248    /**
249     * Finds the actual start destination of the graph, handling cases where the graph's starting
250     * destination is itself a NavGraph.
251     */
252    private static NavDestination findStartDestination(@NonNull NavGraph graph) {
253        NavDestination startDestination = graph;
254        while (startDestination instanceof NavGraph) {
255            NavGraph parent = (NavGraph) startDestination;
256            startDestination = parent.findNode(parent.getStartDestination());
257        }
258        return startDestination;
259    }
260
261    /**
262     * The OnNavigatedListener specifically for keeping the ActionBar updated. This handles both
263     * updating the title and updating the Up Indicator transitioning between the
264     */
265    private static class ActionBarOnNavigatedListener implements NavController.OnNavigatedListener {
266        private final AppCompatActivity mActivity;
267        @Nullable
268        private final DrawerLayout mDrawerLayout;
269        private DrawerArrowDrawable mArrowDrawable;
270        private ValueAnimator mAnimator;
271
272        ActionBarOnNavigatedListener(
273                @NonNull AppCompatActivity activity, @Nullable DrawerLayout drawerLayout) {
274            mActivity = activity;
275            mDrawerLayout = drawerLayout;
276        }
277
278        @Override
279        public void onNavigated(@NonNull NavController controller,
280                @NonNull NavDestination destination) {
281            ActionBar actionBar = mActivity.getSupportActionBar();
282            CharSequence title = destination.getLabel();
283            if (!TextUtils.isEmpty(title)) {
284                actionBar.setTitle(title);
285            }
286            boolean isStartDestination = findStartDestination(controller.getGraph()) == destination;
287            actionBar.setDisplayHomeAsUpEnabled(mDrawerLayout != null || !isStartDestination);
288            setActionBarUpIndicator(mDrawerLayout != null && isStartDestination);
289        }
290
291        void setActionBarUpIndicator(boolean showAsDrawerIndicator) {
292            ActionBarDrawerToggle.Delegate delegate = mActivity.getDrawerToggleDelegate();
293            boolean animate = true;
294            if (mArrowDrawable == null) {
295                mArrowDrawable = new DrawerArrowDrawable(
296                        delegate.getActionBarThemedContext());
297                delegate.setActionBarUpIndicator(mArrowDrawable, 0);
298                // We're setting the initial state, so skip the animation
299                animate = false;
300            }
301            float endValue = showAsDrawerIndicator ? 0f : 1f;
302            if (animate) {
303                float startValue = mArrowDrawable.getProgress();
304                if (mAnimator != null) {
305                    mAnimator.cancel();
306                }
307                mAnimator = ObjectAnimator.ofFloat(mArrowDrawable, "progress",
308                        startValue, endValue);
309                mAnimator.start();
310            } else {
311                mArrowDrawable.setProgress(endValue);
312            }
313        }
314    }
315}
316