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