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