blob: e6f29a4ab6a9044c44c8053a71e3b86c6cdc06b7 [file] [log] [blame]
/*
* 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 androidx.core.content.ContextCompat;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.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<View> 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);
}
}
}