1 /* 2 * Copyright (C) 2016 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.documentsui; 18 19 import static com.android.documentsui.base.SharedMinimal.VERBOSE; 20 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.graphics.Outline; 24 import android.graphics.drawable.ColorDrawable; 25 import android.graphics.drawable.Drawable; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.ViewOutlineProvider; 29 import android.view.Window; 30 import android.view.WindowManager; 31 import android.widget.FrameLayout; 32 33 import androidx.annotation.ColorRes; 34 import androidx.annotation.Nullable; 35 import androidx.appcompat.widget.Toolbar; 36 import androidx.core.content.ContextCompat; 37 38 import com.android.documentsui.base.RootInfo; 39 import com.android.documentsui.base.State; 40 import com.android.documentsui.base.UserId; 41 import com.android.documentsui.dirlist.AnimationView; 42 import com.android.documentsui.util.VersionUtils; 43 44 import com.google.android.material.appbar.AppBarLayout; 45 import com.google.android.material.appbar.CollapsingToolbarLayout; 46 47 import java.util.function.IntConsumer; 48 49 /** 50 * A facade over the portions of the app and drawer toolbars. 51 */ 52 public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListener { 53 54 private static final String TAG = "NavigationViewManager"; 55 56 private final DrawerController mDrawer; 57 private final Toolbar mToolbar; 58 private final BaseActivity mActivity; 59 private final View mHeader; 60 private final State mState; 61 private final NavigationViewManager.Environment mEnv; 62 private final Breadcrumb mBreadcrumb; 63 private final ProfileTabs mProfileTabs; 64 private final View mSearchBarView; 65 private final CollapsingToolbarLayout mCollapsingBarLayout; 66 private final Drawable mDefaultActionBarBackground; 67 private final ViewOutlineProvider mDefaultOutlineProvider; 68 private final ViewOutlineProvider mSearchBarOutlineProvider; 69 private final boolean mShowSearchBar; 70 71 private boolean mIsActionModeActivated = false; 72 private @ColorRes int mDefaultStatusBarColorResId; 73 NavigationViewManager( BaseActivity activity, DrawerController drawer, State state, NavigationViewManager.Environment env, Breadcrumb breadcrumb, View tabLayoutContainer, UserIdManager userIdManager)74 public NavigationViewManager( 75 BaseActivity activity, 76 DrawerController drawer, 77 State state, 78 NavigationViewManager.Environment env, 79 Breadcrumb breadcrumb, 80 View tabLayoutContainer, 81 UserIdManager userIdManager) { 82 83 mActivity = activity; 84 mToolbar = activity.findViewById(R.id.toolbar); 85 mHeader = activity.findViewById(R.id.directory_header); 86 mDrawer = drawer; 87 mState = state; 88 mEnv = env; 89 mBreadcrumb = breadcrumb; 90 mBreadcrumb.setup(env, state, this::onNavigationItemSelected); 91 mProfileTabs = new ProfileTabs(tabLayoutContainer, mState, userIdManager, mEnv, activity); 92 93 mToolbar.setNavigationOnClickListener( 94 new View.OnClickListener() { 95 @Override 96 public void onClick(View v) { 97 onNavigationIconClicked(); 98 } 99 }); 100 mSearchBarView = activity.findViewById(R.id.searchbar_title); 101 mCollapsingBarLayout = activity.findViewById(R.id.collapsing_toolbar); 102 mDefaultActionBarBackground = mToolbar.getBackground(); 103 mDefaultOutlineProvider = mToolbar.getOutlineProvider(); 104 mShowSearchBar = activity.getResources().getBoolean(R.bool.show_search_bar); 105 106 final int[] styledAttrs = {android.R.attr.statusBarColor}; 107 TypedArray a = mActivity.obtainStyledAttributes(styledAttrs); 108 mDefaultStatusBarColorResId = a.getResourceId(0, -1); 109 if (mDefaultStatusBarColorResId == -1) { 110 Log.w(TAG, "Retrieve statusBarColorResId from theme failed, assigned default"); 111 mDefaultStatusBarColorResId = R.color.app_background_color; 112 } 113 a.recycle(); 114 115 final Resources resources = mToolbar.getResources(); 116 final int radius = resources.getDimensionPixelSize(R.dimen.search_bar_radius); 117 final int marginStart = 118 resources.getDimensionPixelSize(R.dimen.search_bar_background_margin_start); 119 final int marginEnd = 120 resources.getDimensionPixelSize(R.dimen.search_bar_background_margin_end); 121 mSearchBarOutlineProvider = new ViewOutlineProvider() { 122 @Override 123 public void getOutline(View view, Outline outline) { 124 outline.setRoundRect(marginStart, 0, 125 view.getWidth() - marginEnd, view.getHeight(), radius); 126 } 127 }; 128 } 129 130 @Override onOffsetChanged(AppBarLayout appBarLayout, int offset)131 public void onOffsetChanged(AppBarLayout appBarLayout, int offset) { 132 if (!VersionUtils.isAtLeastS()) { 133 return; 134 } 135 136 // For S+ Only. Change toolbar color dynamically based on scroll offset. 137 // Usually this can be done in xml using app:contentScrim and app:statusBarScrim, however 138 // in our case since we also put directory_header.xml inside the CollapsingToolbarLayout, 139 // the scrim will also cover the directory header. Long term need to think about how to 140 // move directory_header out of the AppBarLayout. 141 142 Window window = mActivity.getWindow(); 143 View actionBar = window.getDecorView().findViewById(R.id.action_mode_bar); 144 int dynamicHeaderColor = ContextCompat.getColor(mActivity, 145 offset == 0 ? mDefaultStatusBarColorResId : R.color.color_surface_header); 146 if (actionBar != null) { 147 // Action bar needs to be updated separately for selection mode. 148 actionBar.setBackgroundColor(dynamicHeaderColor); 149 } 150 151 window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 152 window.setStatusBarColor(dynamicHeaderColor); 153 if (shouldShowSearchBar()) { 154 // Do not change search bar background. 155 } else { 156 mToolbar.setBackground(new ColorDrawable(dynamicHeaderColor)); 157 } 158 } 159 setSearchBarClickListener(View.OnClickListener listener)160 public void setSearchBarClickListener(View.OnClickListener listener) { 161 mSearchBarView.setOnClickListener(listener); 162 } 163 getProfileTabsAddons()164 public ProfileTabsAddons getProfileTabsAddons() { 165 return mProfileTabs; 166 } 167 168 /** 169 * Sets a listener to the profile tabs. 170 */ setProfileTabsListener(ProfileTabs.Listener listener)171 public void setProfileTabsListener(ProfileTabs.Listener listener) { 172 mProfileTabs.setListener(listener); 173 } 174 onNavigationIconClicked()175 private void onNavigationIconClicked() { 176 if (mDrawer.isPresent()) { 177 mDrawer.setOpen(true); 178 } 179 } 180 onNavigationItemSelected(int position)181 void onNavigationItemSelected(int position) { 182 boolean changed = false; 183 while (mState.stack.size() > position + 1) { 184 changed = true; 185 mState.stack.pop(); 186 } 187 if (changed) { 188 mEnv.refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE); 189 } 190 } 191 getSelectedUser()192 public UserId getSelectedUser() { 193 return mProfileTabs.getSelectedUser(); 194 } 195 setActionModeActivated(boolean actionModeActivated)196 public void setActionModeActivated(boolean actionModeActivated) { 197 mIsActionModeActivated = actionModeActivated; 198 update(); 199 } 200 update()201 public void update() { 202 updateScrollFlag(); 203 updateToolbar(); 204 mProfileTabs.updateView(); 205 206 // TODO: Looks to me like this block is never getting hit. 207 if (mEnv.isSearchExpanded()) { 208 mToolbar.setTitle(null); 209 mBreadcrumb.show(false); 210 return; 211 } 212 213 mDrawer.setTitle(mEnv.getDrawerTitle()); 214 215 mToolbar.setNavigationIcon(getActionBarIcon()); 216 mToolbar.setNavigationContentDescription(R.string.drawer_open); 217 218 if (shouldShowSearchBar()) { 219 mBreadcrumb.show(false); 220 mToolbar.setTitle(null); 221 mSearchBarView.setVisibility(View.VISIBLE); 222 } else { 223 mSearchBarView.setVisibility(View.GONE); 224 String title = mState.stack.size() <= 1 225 ? mEnv.getCurrentRoot().title : mState.stack.getTitle(); 226 if (VERBOSE) Log.v(TAG, "New toolbar title is: " + title); 227 mToolbar.setTitle(title); 228 mBreadcrumb.show(true); 229 mBreadcrumb.postUpdate(); 230 } 231 } 232 updateScrollFlag()233 private void updateScrollFlag() { 234 if (mCollapsingBarLayout == null) { 235 return; 236 } 237 238 AppBarLayout.LayoutParams lp = 239 (AppBarLayout.LayoutParams) mCollapsingBarLayout.getLayoutParams(); 240 lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 241 | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); 242 mCollapsingBarLayout.setLayoutParams(lp); 243 } 244 updateToolbar()245 private void updateToolbar() { 246 if (mCollapsingBarLayout == null) { 247 // Tablet mode does not use CollapsingBarLayout 248 // (res/layout-sw720dp/directory_app_bar.xml or res/layout/fixed_layout.xml) 249 if (shouldShowSearchBar()) { 250 mToolbar.setBackgroundResource(R.drawable.search_bar_background); 251 mToolbar.setOutlineProvider(mSearchBarOutlineProvider); 252 } else { 253 mToolbar.setBackground(mDefaultActionBarBackground); 254 mToolbar.setOutlineProvider(null); 255 } 256 return; 257 } 258 259 CollapsingToolbarLayout.LayoutParams toolbarLayoutParams = 260 (CollapsingToolbarLayout.LayoutParams) mToolbar.getLayoutParams(); 261 262 int headerTopOffset = 0; 263 if (shouldShowSearchBar() && !mIsActionModeActivated) { 264 mToolbar.setBackgroundResource(R.drawable.search_bar_background); 265 mToolbar.setOutlineProvider(mSearchBarOutlineProvider); 266 int searchBarMargin = mToolbar.getResources().getDimensionPixelSize( 267 R.dimen.search_bar_margin); 268 toolbarLayoutParams.setMargins(searchBarMargin, searchBarMargin, searchBarMargin, 269 searchBarMargin); 270 mToolbar.setLayoutParams(toolbarLayoutParams); 271 mToolbar.setElevation( 272 mToolbar.getResources().getDimensionPixelSize(R.dimen.search_bar_elevation)); 273 headerTopOffset = toolbarLayoutParams.height + searchBarMargin * 2; 274 } else { 275 mToolbar.setBackground(mDefaultActionBarBackground); 276 mToolbar.setOutlineProvider(mDefaultOutlineProvider); 277 int actionBarMargin = mToolbar.getResources().getDimensionPixelSize( 278 R.dimen.action_bar_margin); 279 toolbarLayoutParams.setMargins(0, 0, 0, /* bottom= */ actionBarMargin); 280 mToolbar.setLayoutParams(toolbarLayoutParams); 281 mToolbar.setElevation( 282 mToolbar.getResources().getDimensionPixelSize(R.dimen.action_bar_elevation)); 283 headerTopOffset = toolbarLayoutParams.height + actionBarMargin; 284 } 285 286 if (!mIsActionModeActivated) { 287 FrameLayout.LayoutParams headerLayoutParams = 288 (FrameLayout.LayoutParams) mHeader.getLayoutParams(); 289 headerLayoutParams.setMargins(0, /* top= */ headerTopOffset, 0, 0); 290 mHeader.setLayoutParams(headerLayoutParams); 291 } 292 } 293 shouldShowSearchBar()294 private boolean shouldShowSearchBar() { 295 return mState.stack.isRecents() && !mEnv.isSearchExpanded() && mShowSearchBar; 296 } 297 298 // Hamburger if drawer is present, else sad nullness. 299 private @Nullable getActionBarIcon()300 Drawable getActionBarIcon() { 301 if (mDrawer.isPresent()) { 302 return mToolbar.getContext().getDrawable(R.drawable.ic_hamburger); 303 } else { 304 return null; 305 } 306 } 307 revealRootsDrawer(boolean open)308 void revealRootsDrawer(boolean open) { 309 mDrawer.setOpen(open); 310 } 311 312 interface Breadcrumb { setup(Environment env, State state, IntConsumer listener)313 void setup(Environment env, State state, IntConsumer listener); 314 show(boolean visibility)315 void show(boolean visibility); 316 postUpdate()317 void postUpdate(); 318 } 319 320 interface Environment { 321 @Deprecated 322 // Use CommonAddones#getCurrentRoot getCurrentRoot()323 RootInfo getCurrentRoot(); 324 getDrawerTitle()325 String getDrawerTitle(); 326 327 @Deprecated 328 // Use CommonAddones#refreshCurrentRootAndDirectory refreshCurrentRootAndDirectory(int animation)329 void refreshCurrentRootAndDirectory(int animation); 330 isSearchExpanded()331 boolean isSearchExpanded(); 332 } 333 } 334