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