ActionBarAdapter.java revision 56dfa580b5497f25169a4c875800ffff95cd31cd
1/* 2 * Copyright (C) 2010 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 com.android.contacts.activities; 18 19import android.animation.ValueAnimator; 20import android.app.ActionBar; 21import android.app.Activity; 22import android.content.Context; 23import android.content.SharedPreferences; 24import android.content.res.TypedArray; 25import android.os.Bundle; 26import android.preference.PreferenceManager; 27import android.text.Editable; 28import android.text.TextUtils; 29import android.text.TextWatcher; 30import android.view.Gravity; 31import android.view.LayoutInflater; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.inputmethod.InputMethodManager; 35import android.widget.FrameLayout; 36import android.widget.LinearLayout.LayoutParams; 37import android.widget.SearchView.OnCloseListener; 38import android.view.View.OnClickListener; 39import android.widget.EditText; 40import android.widget.TextView; 41import android.widget.Toolbar; 42 43import com.android.contacts.R; 44import com.android.contacts.activities.ActionBarAdapter.Listener.Action; 45import com.android.contacts.list.ContactsRequest; 46 47import com.android.contacts.common.compat.CompatUtils; 48 49/** 50 * Adapter for the action bar at the top of the Contacts activity. 51 */ 52public class ActionBarAdapter implements OnCloseListener { 53 54 public interface Listener { 55 public abstract class Action { 56 public static final int CHANGE_SEARCH_QUERY = 0; 57 public static final int START_SEARCH_MODE = 1; 58 public static final int START_SELECTION_MODE = 2; 59 public static final int STOP_SEARCH_AND_SELECTION_MODE = 3; 60 public static final int BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE = 4; 61 } 62 63 void onAction(int action); 64 65 /** 66 * Called when the user selects a tab. The new tab can be obtained using 67 * {@link #getCurrentTab}. 68 */ 69 void onSelectedTabChanged(); 70 71 void onUpButtonPressed(); 72 } 73 74 private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode"; 75 private static final String EXTRA_KEY_QUERY = "navBar.query"; 76 private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab"; 77 private static final String EXTRA_KEY_SELECTED_MODE = "navBar.selectionMode"; 78 private static final String EXTRA_KEY_SHOULD_OPEN_OVERFLOW = "navBar.shouldOpenOverflow"; 79 80 private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab"; 81 82 private boolean mSelectionMode; 83 private boolean mSearchMode; 84 private boolean mShouldOverflowOpen; 85 private boolean mIsOverflowOpen; 86 private String mQueryString; 87 88 private EditText mSearchView; 89 private View mClearSearchView; 90 /** The view that represents tabs when we are in portrait mode **/ 91 private View mPortraitTabs; 92 /** The view that represents tabs when we are in landscape mode **/ 93 private View mLandscapeTabs; 94 private View mSearchContainer; 95 private View mSelectionContainer; 96 97 private int mMaxPortraitTabHeight; 98 private int mMaxToolbarContentInsetStart; 99 100 private final Activity mActivity; 101 private final SharedPreferences mPrefs; 102 103 private Listener mListener; 104 105 private final ActionBar mActionBar; 106 private final Toolbar mToolbar; 107 /** 108 * Frame that contains the toolbar and draws the toolbar's background color. This is useful 109 * for placing things behind the toolbar. 110 */ 111 private final FrameLayout mToolBarFrame; 112 113 private boolean mShowHomeIcon; 114 115 public interface TabState { 116 public static int FAVORITES = 0; 117 public static int ALL = 1; 118 119 public static int COUNT = 2; 120 public static int DEFAULT = ALL; 121 } 122 123 private int mCurrentTab = TabState.DEFAULT; 124 125 public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, 126 View portraitTabs, View landscapeTabs, Toolbar toolbar) { 127 mActivity = activity; 128 mListener = listener; 129 mActionBar = actionBar; 130 mPrefs = PreferenceManager.getDefaultSharedPreferences(mActivity); 131 mPortraitTabs = portraitTabs; 132 mLandscapeTabs = landscapeTabs; 133 mToolbar = toolbar; 134 mToolBarFrame = (FrameLayout) mToolbar.getParent(); 135 mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart(); 136 mShowHomeIcon = mActivity.getResources().getBoolean(R.bool.show_home_icon); 137 138 setupSearchAndSelectionViews(); 139 setupTabs(mActivity); 140 } 141 142 private void setupTabs(Context context) { 143 final TypedArray attributeArray = context.obtainStyledAttributes( 144 new int[]{android.R.attr.actionBarSize}); 145 mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0); 146 // Hide tabs initially 147 setPortraitTabHeight(0); 148 } 149 150 private void setupSearchAndSelectionViews() { 151 final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService( 152 Context.LAYOUT_INFLATER_SERVICE); 153 154 // Setup search bar 155 mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar, 156 /* attachToRoot = */ false); 157 mSearchContainer.setVisibility(View.VISIBLE); 158 mToolbar.addView(mSearchContainer); 159 mSearchContainer.setBackgroundColor(mActivity.getResources().getColor( 160 R.color.searchbox_background_color)); 161 mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view); 162 mSearchView.setHint(mActivity.getString(R.string.hint_findContacts)); 163 mSearchView.addTextChangedListener(new SearchTextWatcher()); 164 mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener( 165 new OnClickListener() { 166 @Override 167 public void onClick(View v) { 168 if (mListener != null) { 169 mListener.onUpButtonPressed(); 170 } 171 } 172 }); 173 174 mClearSearchView = mSearchContainer.findViewById(R.id.search_close_button); 175 mClearSearchView.setOnClickListener( 176 new OnClickListener() { 177 @Override 178 public void onClick(View v) { 179 setQueryString(null); 180 } 181 }); 182 183 // Setup selection bar 184 mSelectionContainer = inflater.inflate(R.layout.selection_bar, mToolbar, 185 /* attachToRoot = */ false); 186 // Insert the selection container into mToolBarFrame behind the Toolbar, so that 187 // the Toolbar's MenuItems can appear on top of the selection container. 188 mToolBarFrame.addView(mSelectionContainer, 0); 189 mSelectionContainer.findViewById(R.id.selection_close).setOnClickListener( 190 new OnClickListener() { 191 @Override 192 public void onClick(View v) { 193 if (mListener != null) { 194 mListener.onUpButtonPressed(); 195 } 196 } 197 }); 198 } 199 200 public void initialize(Bundle savedState, ContactsRequest request) { 201 if (savedState == null) { 202 mSearchMode = request.isSearchMode(); 203 mQueryString = request.getQueryString(); 204 mCurrentTab = loadLastTabPreference(); 205 mSelectionMode = false; 206 setShouldOpenOverflow(false); 207 } else { 208 mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE); 209 mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE); 210 mQueryString = savedState.getString(EXTRA_KEY_QUERY); 211 setShouldOpenOverflow(savedState.getBoolean(EXTRA_KEY_SHOULD_OPEN_OVERFLOW)); 212 // Just set to the field here. The listener will be notified by update(). 213 mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB); 214 } 215 if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) { 216 // Invalid tab index was saved (b/12938207). Restore the default. 217 mCurrentTab = TabState.DEFAULT; 218 } 219 // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in 220 // search mode. 221 update(true /* skipAnimation */); 222 // Expanding the {@link SearchView} clears the query, so set the query from the 223 // {@link ContactsRequest} after it has been expanded, if applicable. 224 if (mSearchMode && !TextUtils.isEmpty(mQueryString)) { 225 setQueryString(mQueryString); 226 } 227 } 228 229 public void setListener(Listener listener) { 230 mListener = listener; 231 } 232 233 private class SearchTextWatcher implements TextWatcher { 234 235 @Override 236 public void onTextChanged(CharSequence queryString, int start, int before, int count) { 237 if (queryString.equals(mQueryString)) { 238 return; 239 } 240 mQueryString = queryString.toString(); 241 if (!mSearchMode) { 242 if (!TextUtils.isEmpty(queryString)) { 243 setSearchMode(true); 244 } 245 } else if (mListener != null) { 246 mListener.onAction(Action.CHANGE_SEARCH_QUERY); 247 } 248 mClearSearchView.setVisibility( 249 TextUtils.isEmpty(queryString) ? View.GONE : View.VISIBLE); 250 } 251 252 @Override 253 public void afterTextChanged(Editable s) {} 254 255 @Override 256 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 257 } 258 259 /** 260 * Save the current tab selection, and notify the listener. 261 */ 262 public void setCurrentTab(int tab) { 263 setCurrentTab(tab, true); 264 } 265 266 /** 267 * Save the current tab selection. 268 */ 269 public void setCurrentTab(int tab, boolean notifyListener) { 270 if (tab == mCurrentTab) { 271 return; 272 } 273 mCurrentTab = tab; 274 275 if (notifyListener && mListener != null) mListener.onSelectedTabChanged(); 276 saveLastTabPreference(mCurrentTab); 277 } 278 279 public int getCurrentTab() { 280 return mCurrentTab; 281 } 282 283 /** 284 * @return Whether in search mode, i.e. if the search view is visible/expanded. 285 * 286 * Note even if the action bar is in search mode, if the query is empty, the search fragment 287 * will not be in search mode. 288 */ 289 public boolean isSearchMode() { 290 return mSearchMode; 291 } 292 293 /** 294 * @return Whether in selection mode, i.e. if the selection view is visible/expanded. 295 */ 296 public boolean isSelectionMode() { 297 return mSelectionMode; 298 } 299 300 public void setSearchMode(boolean flag) { 301 if (mSearchMode != flag) { 302 mSearchMode = flag; 303 update(false /* skipAnimation */); 304 if (mSearchView == null) { 305 return; 306 } 307 if (mSearchMode) { 308 mSearchView.setEnabled(true); 309 setFocusOnSearchView(); 310 } else { 311 // Disable search view, so that it doesn't keep the IME visible. 312 mSearchView.setEnabled(false); 313 } 314 setQueryString(null); 315 } else if (flag) { 316 // Everything is already set up. Still make sure the keyboard is up 317 if (mSearchView != null) setFocusOnSearchView(); 318 } 319 } 320 321 public void setSelectionMode(boolean flag) { 322 if (mSelectionMode != flag) { 323 mSelectionMode = flag; 324 update(false /* skipAnimation */); 325 } 326 } 327 328 public void setShouldOpenOverflow(boolean shouldOpenOverflow) { 329 mShouldOverflowOpen = shouldOpenOverflow; 330 } 331 332 public boolean shouldOpenOverflow() { 333 return mShouldOverflowOpen; 334 } 335 336 public void setOverflowOpen(boolean isOverflowOpen) { 337 mIsOverflowOpen = isOverflowOpen; 338 } 339 340 public boolean isOverflowOpen() { 341 return mIsOverflowOpen; 342 } 343 344 public String getQueryString() { 345 return mSearchMode ? mQueryString : null; 346 } 347 348 public void setQueryString(String query) { 349 mQueryString = query; 350 if (mSearchView != null) { 351 mSearchView.setText(query); 352 // When programmatically entering text into the search view, the most reasonable 353 // place for the cursor is after all the text. 354 mSearchView.setSelection(mSearchView.getText() == null ? 355 0 : mSearchView.getText().length()); 356 } 357 } 358 359 /** @return true if the "UP" icon is showing. */ 360 public boolean isUpShowing() { 361 return mSearchMode; // Only shown on the search mode. 362 } 363 364 private void updateDisplayOptionsInner() { 365 // All the flags we may change in this method. 366 final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME 367 | ActionBar.DISPLAY_HOME_AS_UP; 368 369 // The current flags set to the action bar. (only the ones that we may change here) 370 final int current = mActionBar.getDisplayOptions() & MASK; 371 372 final boolean isSearchOrSelectionMode = mSearchMode || mSelectionMode; 373 374 // Build the new flags... 375 int newFlags = 0; 376 if (mShowHomeIcon && !isSearchOrSelectionMode) { 377 newFlags |= ActionBar.DISPLAY_SHOW_HOME; 378 } 379 if (mSearchMode && !mSelectionMode) { 380 // The search container is placed inside the toolbar. So we need to disable the 381 // Toolbar's content inset in order to allow the search container to be the width of 382 // the window. 383 mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd()); 384 } 385 if (!isSearchOrSelectionMode) { 386 newFlags |= ActionBar.DISPLAY_SHOW_TITLE; 387 mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart, 388 mToolbar.getContentInsetEnd()); 389 } 390 391 if (mSelectionMode) { 392 // Minimize the horizontal width of the Toolbar since the selection container is placed 393 // behind the toolbar and its left hand side needs to be clickable. 394 FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams(); 395 params.width = LayoutParams.WRAP_CONTENT; 396 params.gravity = Gravity.END; 397 mToolbar.setLayoutParams(params); 398 } else { 399 FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams(); 400 params.width = LayoutParams.MATCH_PARENT; 401 params.gravity = Gravity.END; 402 mToolbar.setLayoutParams(params); 403 } 404 405 if (current != newFlags) { 406 // Pass the mask here to preserve other flags that we're not interested here. 407 mActionBar.setDisplayOptions(newFlags, MASK); 408 } 409 } 410 411 private void update(boolean skipAnimation) { 412 updateStatusBarColor(); 413 414 final boolean isSelectionModeChanging 415 = (mSelectionContainer.getParent() == null) == mSelectionMode; 416 final boolean isSwitchingFromSearchToSelection = 417 mSearchMode && isSelectionModeChanging || mSearchMode && mSelectionMode; 418 final boolean isSearchModeChanging 419 = (mSearchContainer.getParent() == null) == mSearchMode; 420 final boolean isTabHeightChanging = isSearchModeChanging || isSelectionModeChanging; 421 422 // When skipAnimation=true, it is possible that we will switch from search mode 423 // to selection mode directly. So we need to remove the undesired container in addition 424 // to adding the desired container. 425 if (skipAnimation || isSwitchingFromSearchToSelection) { 426 if (isTabHeightChanging || isSwitchingFromSearchToSelection) { 427 mToolbar.removeView(mLandscapeTabs); 428 mToolbar.removeView(mSearchContainer); 429 mToolBarFrame.removeView(mSelectionContainer); 430 if (mSelectionMode) { 431 setPortraitTabHeight(0); 432 addSelectionContainer(); 433 } else if (mSearchMode) { 434 setPortraitTabHeight(0); 435 addSearchContainer(); 436 } else { 437 setPortraitTabHeight(mMaxPortraitTabHeight); 438 addLandscapeViewPagerTabs(); 439 } 440 updateDisplayOptions(isSearchModeChanging); 441 } 442 return; 443 } 444 445 // Handle a switch to/from selection mode, due to UI interaction. 446 if (isSelectionModeChanging) { 447 mToolbar.removeView(mLandscapeTabs); 448 if (mSelectionMode) { 449 addSelectionContainer(); 450 mSelectionContainer.setAlpha(0); 451 mSelectionContainer.animate().alpha(1); 452 animateTabHeightChange(mMaxPortraitTabHeight, 0); 453 updateDisplayOptions(isSearchModeChanging); 454 } else { 455 if (mListener != null) { 456 mListener.onAction(Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE); 457 } 458 mSelectionContainer.setAlpha(1); 459 animateTabHeightChange(0, mMaxPortraitTabHeight); 460 mSelectionContainer.animate().alpha(0).withEndAction(new Runnable() { 461 @Override 462 public void run() { 463 updateDisplayOptions(isSearchModeChanging); 464 addLandscapeViewPagerTabs(); 465 mToolBarFrame.removeView(mSelectionContainer); 466 } 467 }); 468 } 469 } 470 471 // Handle a switch to/from search mode, due to UI interaction. 472 if (isSearchModeChanging) { 473 mToolbar.removeView(mLandscapeTabs); 474 if (mSearchMode) { 475 addSearchContainer(); 476 mSearchContainer.setAlpha(0); 477 mSearchContainer.animate().alpha(1); 478 animateTabHeightChange(mMaxPortraitTabHeight, 0); 479 updateDisplayOptions(isSearchModeChanging); 480 } else { 481 mSearchContainer.setAlpha(1); 482 animateTabHeightChange(0, mMaxPortraitTabHeight); 483 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() { 484 @Override 485 public void run() { 486 updateDisplayOptions(isSearchModeChanging); 487 addLandscapeViewPagerTabs(); 488 mToolbar.removeView(mSearchContainer); 489 } 490 }); 491 } 492 } 493 } 494 495 public void setSelectionCount(int selectionCount) { 496 TextView textView = (TextView) mSelectionContainer.findViewById(R.id.selection_count_text); 497 if (selectionCount == 0) { 498 textView.setVisibility(View.GONE); 499 } else { 500 textView.setVisibility(View.VISIBLE); 501 } 502 textView.setText(String.valueOf(selectionCount)); 503 } 504 505 private void updateStatusBarColor() { 506 if (mSelectionMode) { 507 final int cabStatusBarColor = mActivity.getResources().getColor( 508 R.color.contextual_selection_bar_status_bar_color); 509 mActivity.getWindow().setStatusBarColor(cabStatusBarColor); 510 } else { 511 final int normalStatusBarColor = CompatUtils.getColorCompat 512 (mActivity, R.color.primary_color_dark); 513 mActivity.getWindow().setStatusBarColor(normalStatusBarColor); 514 } 515 } 516 517 private void addLandscapeViewPagerTabs() { 518 if (mLandscapeTabs != null) { 519 mToolbar.removeView(mLandscapeTabs); 520 mToolbar.addView(mLandscapeTabs); 521 } 522 } 523 524 private void addSearchContainer() { 525 mToolbar.removeView(mSearchContainer); 526 mToolbar.addView(mSearchContainer); 527 mSearchContainer.setAlpha(1); 528 } 529 530 private void addSelectionContainer() { 531 mToolBarFrame.removeView(mSelectionContainer); 532 mToolBarFrame.addView(mSelectionContainer, 0); 533 mSelectionContainer.setAlpha(1); 534 } 535 536 private void updateDisplayOptions(boolean isSearchModeChanging) { 537 if (mSearchMode && !mSelectionMode) { 538 setFocusOnSearchView(); 539 // Since we have the {@link SearchView} in a custom action bar, we must manually handle 540 // expanding the {@link SearchView} when a search is initiated. Note that a side effect 541 // of this method is that the {@link SearchView} query text is set to empty string. 542 if (isSearchModeChanging) { 543 final CharSequence queryText = mSearchView.getText(); 544 if (!TextUtils.isEmpty(queryText)) { 545 mSearchView.setText(queryText); 546 } 547 } 548 } 549 if (mListener != null) { 550 if (mSearchMode) { 551 mListener.onAction(Action.START_SEARCH_MODE); 552 } 553 if (mSelectionMode) { 554 mListener.onAction(Action.START_SELECTION_MODE); 555 } 556 if (!mSearchMode && !mSelectionMode) { 557 mListener.onAction(Action.STOP_SEARCH_AND_SELECTION_MODE); 558 mListener.onSelectedTabChanged(); 559 } 560 } 561 updateDisplayOptionsInner(); 562 } 563 564 @Override 565 public boolean onClose() { 566 setSearchMode(false); 567 return false; 568 } 569 570 public void onSaveInstanceState(Bundle outState) { 571 outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode); 572 outState.putBoolean(EXTRA_KEY_SELECTED_MODE, mSelectionMode); 573 outState.putBoolean(EXTRA_KEY_SHOULD_OPEN_OVERFLOW, mShouldOverflowOpen); 574 outState.putString(EXTRA_KEY_QUERY, mQueryString); 575 outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab); 576 } 577 578 public void setFocusOnSearchView() { 579 mSearchView.requestFocus(); 580 showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue. 581 } 582 583 private void showInputMethod(View view) { 584 final InputMethodManager imm = (InputMethodManager) mActivity.getSystemService( 585 Context.INPUT_METHOD_SERVICE); 586 if (imm != null) { 587 imm.showSoftInput(view, 0); 588 } 589 } 590 591 private void saveLastTabPreference(int tab) { 592 mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply(); 593 } 594 595 private int loadLastTabPreference() { 596 try { 597 return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT); 598 } catch (IllegalArgumentException e) { 599 // Preference is corrupt? 600 return TabState.DEFAULT; 601 } 602 } 603 604 private void animateTabHeightChange(int start, int end) { 605 if (mPortraitTabs == null) { 606 return; 607 } 608 final ValueAnimator animator = ValueAnimator.ofInt(start, end); 609 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 610 @Override 611 public void onAnimationUpdate(ValueAnimator valueAnimator) { 612 int value = (Integer) valueAnimator.getAnimatedValue(); 613 setPortraitTabHeight(value); 614 } 615 }); 616 animator.setDuration(100).start(); 617 } 618 619 private void setPortraitTabHeight(int height) { 620 if (mPortraitTabs == null) { 621 return; 622 } 623 ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams(); 624 layoutParams.height = height; 625 mPortraitTabs.setLayoutParams(layoutParams); 626 } 627} 628