/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts.activities; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.app.Activity; import android.content.Context; import android.content.res.ColorStateList; import android.os.Bundle; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout.LayoutParams; import android.widget.SearchView.OnCloseListener; import android.widget.TextView; import com.android.contacts.R; import com.android.contacts.activities.ActionBarAdapter.Listener.Action; import com.android.contacts.activities.PeopleActivity; import com.android.contacts.compat.CompatUtils; import com.android.contacts.list.ContactsRequest; import com.android.contacts.util.MaterialColorMapUtils; import java.util.ArrayList; /** * Adapter for the action bar at the top of the Contacts activity. */ public class ActionBarAdapter implements OnCloseListener { public interface Listener { public abstract class Action { public static final int CHANGE_SEARCH_QUERY = 0; public static final int START_SEARCH_MODE = 1; public static final int START_SELECTION_MODE = 2; public static final int STOP_SEARCH_AND_SELECTION_MODE = 3; public static final int BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE = 4; } void onAction(int action); void onUpButtonPressed(); } private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode"; private static final String EXTRA_KEY_QUERY = "navBar.query"; private static final String EXTRA_KEY_SELECTED_MODE = "navBar.selectionMode"; private boolean mSelectionMode; private boolean mSearchMode; private String mQueryString; private EditText mSearchView; private View mClearSearchView; private View mSearchContainer; private View mSelectionContainer; private int mMaxToolbarContentInsetStart; private int mActionBarAnimationDuration; private final Activity mActivity; private Listener mListener; private final ActionBar mActionBar; private final Toolbar mToolbar; /** * Frame that contains the toolbar and draws the toolbar's background color. This is useful * for placing things behind the toolbar. */ private final FrameLayout mToolBarFrame; private boolean mShowHomeIcon; private boolean mShowHomeAsUp; private int mSearchHintResId; private ValueAnimator mStatusBarAnimator; public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, Toolbar toolbar) { this(activity, listener, actionBar, toolbar, R.string.hint_findContacts); } public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, Toolbar toolbar, int searchHintResId) { mActivity = activity; mListener = listener; mActionBar = actionBar; mToolbar = toolbar; mToolBarFrame = (FrameLayout) mToolbar.getParent(); mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart(); mSearchHintResId = searchHintResId; mActionBarAnimationDuration = mActivity.getResources().getInteger(R.integer.action_bar_animation_duration); setupSearchAndSelectionViews(); } public void setShowHomeIcon(boolean showHomeIcon) { mShowHomeIcon = showHomeIcon; } public void setShowHomeAsUp(boolean showHomeAsUp) { mShowHomeAsUp = showHomeAsUp; } public View getSelectionContainer() { return mSelectionContainer; } private void setupSearchAndSelectionViews() { final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); // Setup search bar mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar, /* attachToRoot = */ false); mSearchContainer.setVisibility(View.VISIBLE); mToolbar.addView(mSearchContainer); mSearchContainer.setBackgroundColor(mActivity.getResources().getColor( R.color.searchbox_background_color)); mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view); mSearchView.setHint(mActivity.getString(mSearchHintResId)); mSearchView.addTextChangedListener(new SearchTextWatcher()); final ImageButton searchBackButton = (ImageButton) mSearchContainer .findViewById(R.id.search_back_button); searchBackButton.setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { mListener.onUpButtonPressed(); } } }); searchBackButton.getDrawable().setAutoMirrored(true); mClearSearchView = mSearchContainer.findViewById(R.id.search_close_button); mClearSearchView.setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { setQueryString(null); } }); // Setup selection bar mSelectionContainer = inflater.inflate(R.layout.selection_bar, mToolbar, /* attachToRoot = */ false); // Insert the selection container into mToolBarFrame behind the Toolbar, so that // the Toolbar's MenuItems can appear on top of the selection container. mToolBarFrame.addView(mSelectionContainer, 0); mSelectionContainer.findViewById(R.id.selection_close).setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { mListener.onUpButtonPressed(); } } }); } public void initialize(Bundle savedState, ContactsRequest request) { if (savedState == null) { mSearchMode = request.isSearchMode(); mQueryString = request.getQueryString(); mSelectionMode = false; } else { mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE); mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE); mQueryString = savedState.getString(EXTRA_KEY_QUERY); } // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in // search mode. update(true /* skipAnimation */); // Expanding the {@link SearchView} clears the query, so set the query from the // {@link ContactsRequest} after it has been expanded, if applicable. if (mSearchMode && !TextUtils.isEmpty(mQueryString)) { setQueryString(mQueryString); } } public void setListener(Listener listener) { mListener = listener; } private class SearchTextWatcher implements TextWatcher { @Override public void onTextChanged(CharSequence queryString, int start, int before, int count) { if (queryString.equals(mQueryString)) { return; } mQueryString = queryString.toString(); if (!mSearchMode) { if (!TextUtils.isEmpty(queryString)) { setSearchMode(true); } } else if (mListener != null) { mListener.onAction(Action.CHANGE_SEARCH_QUERY); } mClearSearchView.setVisibility( TextUtils.isEmpty(queryString) ? View.GONE : View.VISIBLE); } @Override public void afterTextChanged(Editable s) {} @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} } /** * @return Whether in search mode, i.e. if the search view is visible/expanded. * * Note even if the action bar is in search mode, if the query is empty, the search fragment * will not be in search mode. */ public boolean isSearchMode() { return mSearchMode; } /** * @return Whether in selection mode, i.e. if the selection view is visible/expanded. */ public boolean isSelectionMode() { return mSelectionMode; } public void setSearchMode(boolean flag) { if (mSearchMode != flag) { mSearchMode = flag; update(false /* skipAnimation */); if (mSearchView == null) { return; } if (mSearchMode) { mSearchView.setEnabled(true); setFocusOnSearchView(); } else { // Disable search view, so that it doesn't keep the IME visible. mSearchView.setEnabled(false); } setQueryString(null); } else if (flag) { // Everything is already set up. Still make sure the keyboard is up if (mSearchView != null) setFocusOnSearchView(); } } public void setSelectionMode(boolean flag) { if (mSelectionMode != flag) { mSelectionMode = flag; update(false /* skipAnimation */); } } public String getQueryString() { return mSearchMode ? mQueryString : null; } public void setQueryString(String query) { mQueryString = query; if (mSearchView != null) { mSearchView.setText(query); // When programmatically entering text into the search view, the most reasonable // place for the cursor is after all the text. mSearchView.setSelection(mSearchView.getText() == null ? 0 : mSearchView.getText().length()); } } /** @return true if the "UP" icon is showing. */ public boolean isUpShowing() { return mSearchMode; // Only shown on the search mode. } private void updateDisplayOptionsInner() { // All the flags we may change in this method. final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_HOME_AS_UP; // The current flags set to the action bar. (only the ones that we may change here) final int current = mActionBar.getDisplayOptions() & MASK; final boolean isSearchOrSelectionMode = mSearchMode || mSelectionMode; // Build the new flags... int newFlags = 0; if (mShowHomeIcon && !isSearchOrSelectionMode) { newFlags |= ActionBar.DISPLAY_SHOW_HOME; if (mShowHomeAsUp) { newFlags |= ActionBar.DISPLAY_HOME_AS_UP; } } if (mSearchMode && !mSelectionMode) { // The search container is placed inside the toolbar. So we need to disable the // Toolbar's content inset in order to allow the search container to be the width of // the window. mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd()); } if (!isSearchOrSelectionMode) { newFlags |= ActionBar.DISPLAY_SHOW_TITLE; mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart, mToolbar.getContentInsetEnd()); mToolbar.setNavigationIcon(R.drawable.quantum_ic_menu_vd_theme_24); } else { mToolbar.setNavigationIcon(null); } if (mSelectionMode) { // Minimize the horizontal width of the Toolbar since the selection container is placed // behind the toolbar and its left hand side needs to be clickable. FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams(); params.width = LayoutParams.WRAP_CONTENT; params.gravity = Gravity.END; mToolbar.setLayoutParams(params); } else { FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams(); params.width = LayoutParams.MATCH_PARENT; params.gravity = Gravity.END; mToolbar.setLayoutParams(params); } if (current != newFlags) { // Pass the mask here to preserve other flags that we're not interested here. mActionBar.setDisplayOptions(newFlags, MASK); } } private void update(boolean skipAnimation) { updateOverflowButtonColor(); final boolean isSelectionModeChanging = (mSelectionContainer.getParent() == null) == mSelectionMode; final boolean isSwitchingFromSearchToSelection = mSearchMode && isSelectionModeChanging || mSearchMode && mSelectionMode; final boolean isSearchModeChanging = (mSearchContainer.getParent() == null) == mSearchMode; final boolean isTabHeightChanging = isSearchModeChanging || isSelectionModeChanging; // Update toolbar and status bar color. mToolBarFrame.setBackgroundColor(MaterialColorMapUtils.getToolBarColor(mActivity)); updateStatusBarColor(isSelectionModeChanging && !isSearchModeChanging); // When skipAnimation=true, it is possible that we will switch from search mode // to selection mode directly. So we need to remove the undesired container in addition // to adding the desired container. if (skipAnimation || isSwitchingFromSearchToSelection) { if (isTabHeightChanging || isSwitchingFromSearchToSelection) { mToolbar.removeView(mSearchContainer); mToolBarFrame.removeView(mSelectionContainer); if (mSelectionMode) { addSelectionContainer(); } else if (mSearchMode) { addSearchContainer(); } updateDisplayOptions(isSearchModeChanging); } return; } // Handle a switch to/from selection mode, due to UI interaction. if (isSelectionModeChanging) { if (mSelectionMode) { addSelectionContainer(); mSelectionContainer.setAlpha(0); mSelectionContainer.animate().alpha(1).setDuration(mActionBarAnimationDuration); updateDisplayOptions(isSearchModeChanging); } else { if (mListener != null) { mListener.onAction(Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE); } mSelectionContainer.setAlpha(1); mSelectionContainer.animate().alpha(0).setDuration(mActionBarAnimationDuration) .withEndAction(new Runnable() { @Override public void run() { updateDisplayOptions(isSearchModeChanging); mToolBarFrame.removeView(mSelectionContainer); } }); } } // Handle a switch to/from search mode, due to UI interaction. if (isSearchModeChanging) { if (mSearchMode) { addSearchContainer(); mSearchContainer.setAlpha(0); mSearchContainer.animate().alpha(1).setDuration(mActionBarAnimationDuration); updateDisplayOptions(isSearchModeChanging); } else { mSearchContainer.setAlpha(1); mSearchContainer.animate().alpha(0).setDuration(mActionBarAnimationDuration) .withEndAction(new Runnable() { @Override public void run() { updateDisplayOptions(isSearchModeChanging); mToolbar.removeView(mSearchContainer); } }); } } } /** * Find overflow menu ImageView by its content description and update its color. */ public void updateOverflowButtonColor() { final String overflowDescription = mActivity.getResources().getString( R.string.abc_action_menu_overflow_description); final ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView(); final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // Find the overflow ImageView. final ArrayList outViews = new ArrayList<>(); decorView.findViewsWithText(outViews, overflowDescription, View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION); for (View view : outViews) { if (!(view instanceof ImageView)) { continue; } final ImageView overflow = (ImageView) view; // Update the overflow image color. final int iconColor; if (mSelectionMode) { iconColor = mActivity.getResources().getColor( R.color.actionbar_color_grey_solid); } else { iconColor = mActivity.getResources().getColor( R.color.actionbar_text_color); } overflow.setImageTintList(ColorStateList.valueOf(iconColor)); } // We're done, remove the listener. decorView.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); } public void setSelectionCount(int selectionCount) { TextView textView = (TextView) mSelectionContainer.findViewById(R.id.selection_count_text); if (selectionCount == 0) { textView.setVisibility(View.GONE); } else { textView.setVisibility(View.VISIBLE); } textView.setText(String.valueOf(selectionCount)); } public void setActionBarTitle(String title) { final TextView textView = (TextView) mSelectionContainer.findViewById(R.id.selection_count_text); textView.setVisibility(View.VISIBLE); textView.setText(title); } private void updateStatusBarColor(boolean shouldAnimate) { if (!CompatUtils.isLollipopCompatible()) { return; // we can't change the status bar color prior to Lollipop } if (mSelectionMode) { final int cabStatusBarColor = ContextCompat.getColor( mActivity, R.color.contextual_selection_bar_status_bar_color); runStatusBarAnimation(/* colorTo */ cabStatusBarColor); } else { if (shouldAnimate) { runStatusBarAnimation(/* colorTo */ MaterialColorMapUtils.getStatusBarColor(mActivity)); } else if (mActivity instanceof PeopleActivity) { ((PeopleActivity) mActivity).updateStatusBarBackground(); } } } private void runStatusBarAnimation(int colorTo) { final Window window = mActivity.getWindow(); if (window.getStatusBarColor() != colorTo) { // Cancel running animation. if (mStatusBarAnimator != null && mStatusBarAnimator.isRunning()) { mStatusBarAnimator.cancel(); } final int from = window.getStatusBarColor(); // Set up mStatusBarAnimator and run animation. mStatusBarAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), from, colorTo); mStatusBarAnimator.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { window.setStatusBarColor((Integer) animator.getAnimatedValue()); } }); mStatusBarAnimator.setDuration(mActionBarAnimationDuration); mStatusBarAnimator.setStartDelay(0); mStatusBarAnimator.start(); } } private void addSearchContainer() { mToolbar.removeView(mSearchContainer); mToolbar.addView(mSearchContainer); mSearchContainer.setAlpha(1); } private void addSelectionContainer() { mToolBarFrame.removeView(mSelectionContainer); mToolBarFrame.addView(mSelectionContainer, 0); mSelectionContainer.setAlpha(1); } private void updateDisplayOptions(boolean isSearchModeChanging) { if (mSearchMode && !mSelectionMode) { setFocusOnSearchView(); // Since we have the {@link SearchView} in a custom action bar, we must manually handle // expanding the {@link SearchView} when a search is initiated. Note that a side effect // of this method is that the {@link SearchView} query text is set to empty string. if (isSearchModeChanging) { final CharSequence queryText = mSearchView.getText(); if (!TextUtils.isEmpty(queryText)) { mSearchView.setText(queryText); } } } if (mListener != null) { if (mSearchMode) { mListener.onAction(Action.START_SEARCH_MODE); } if (mSelectionMode) { mListener.onAction(Action.START_SELECTION_MODE); } if (!mSearchMode && !mSelectionMode) { mListener.onAction(Action.STOP_SEARCH_AND_SELECTION_MODE); } } updateDisplayOptionsInner(); } @Override public boolean onClose() { setSearchMode(false); return false; } public void onSaveInstanceState(Bundle outState) { outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode); outState.putBoolean(EXTRA_KEY_SELECTED_MODE, mSelectionMode); outState.putString(EXTRA_KEY_QUERY, mQueryString); } public void setFocusOnSearchView() { mSearchView.requestFocus(); showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue. } private void showInputMethod(View view) { final InputMethodManager imm = (InputMethodManager) mActivity.getSystemService( Context.INPUT_METHOD_SERVICE); if (imm != null) { imm.showSoftInput(view, 0); } } }