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.ValueAnimator;
     20 import android.app.ActionBar;
     21 import android.content.Context;
     22 import android.content.SharedPreferences;
     23 import android.content.res.TypedArray;
     24 import android.os.Bundle;
     25 import android.preference.PreferenceManager;
     26 import android.text.Editable;
     27 import android.text.TextUtils;
     28 import android.text.TextWatcher;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.view.inputmethod.InputMethodManager;
     33 import android.widget.SearchView;
     34 import android.widget.SearchView.OnCloseListener;
     35 import android.view.View.OnClickListener;
     36 import android.widget.EditText;
     37 import android.widget.Toolbar;
     38 
     39 import com.android.contacts.R;
     40 import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
     41 import com.android.contacts.list.ContactsRequest;
     42 
     43 /**
     44  * Adapter for the action bar at the top of the Contacts activity.
     45  */
     46 public class ActionBarAdapter implements OnCloseListener {
     47 
     48     public interface Listener {
     49         public abstract class Action {
     50             public static final int CHANGE_SEARCH_QUERY = 0;
     51             public static final int START_SEARCH_MODE = 1;
     52             public static final int STOP_SEARCH_MODE = 2;
     53         }
     54 
     55         void onAction(int action);
     56 
     57         /**
     58          * Called when the user selects a tab.  The new tab can be obtained using
     59          * {@link #getCurrentTab}.
     60          */
     61         void onSelectedTabChanged();
     62 
     63         void onUpButtonPressed();
     64     }
     65 
     66     private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
     67     private static final String EXTRA_KEY_QUERY = "navBar.query";
     68     private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
     69 
     70     private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
     71 
     72     private boolean mSearchMode;
     73     private String mQueryString;
     74 
     75     private EditText mSearchView;
     76     /** The view that represents tabs when we are in portrait mode **/
     77     private View mPortraitTabs;
     78     /** The view that represents tabs when we are in landscape mode **/
     79     private View mLandscapeTabs;
     80     private View mSearchContainer;
     81 
     82     private int mMaxPortraitTabHeight;
     83     private int mMaxToolbarContentInsetStart;
     84 
     85     private final Context mContext;
     86     private final SharedPreferences mPrefs;
     87 
     88     private Listener mListener;
     89 
     90     private final ActionBar mActionBar;
     91     private final Toolbar mToolbar;
     92 
     93     private boolean mShowHomeIcon;
     94 
     95     public interface TabState {
     96         public static int FAVORITES = 0;
     97         public static int ALL = 1;
     98 
     99         public static int COUNT = 2;
    100         public static int DEFAULT = ALL;
    101     }
    102 
    103     private int mCurrentTab = TabState.DEFAULT;
    104 
    105     public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
    106             View portraitTabs, View landscapeTabs, Toolbar toolbar) {
    107         mContext = context;
    108         mListener = listener;
    109         mActionBar = actionBar;
    110         mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    111         mPortraitTabs = portraitTabs;
    112         mLandscapeTabs = landscapeTabs;
    113         mToolbar = toolbar;
    114         mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
    115         mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon);
    116 
    117         setupSearchView();
    118         setupTabs(context);
    119     }
    120 
    121     private void setupTabs(Context context) {
    122         final TypedArray attributeArray = context.obtainStyledAttributes(
    123                 new int[]{android.R.attr.actionBarSize});
    124         mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0);
    125         // Hide tabs initially
    126         setPortraitTabHeight(0);
    127     }
    128 
    129     private void setupSearchView() {
    130         final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService(
    131                 Context.LAYOUT_INFLATER_SERVICE);
    132         mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar,
    133                 /* attachToRoot = */ false);
    134         mSearchContainer.setVisibility(View.VISIBLE);
    135         mToolbar.addView(mSearchContainer);
    136 
    137         mSearchContainer.setBackgroundColor(mContext.getResources().getColor(
    138                 R.color.searchbox_background_color));
    139         mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view);
    140         mSearchView.setHint(mContext.getString(R.string.hint_findContacts));
    141         mSearchView.addTextChangedListener(new SearchTextWatcher());
    142         mSearchContainer.findViewById(R.id.search_close_button).setOnClickListener(
    143                 new OnClickListener() {
    144             @Override
    145             public void onClick(View v) {
    146                 mSearchView.setText(null);
    147             }
    148         });
    149         mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener(
    150                 new OnClickListener() {
    151             @Override
    152             public void onClick(View v) {
    153                 if (mListener != null) {
    154                     mListener.onUpButtonPressed();
    155                 }
    156             }
    157         });
    158     }
    159 
    160     public void initialize(Bundle savedState, ContactsRequest request) {
    161         if (savedState == null) {
    162             mSearchMode = request.isSearchMode();
    163             mQueryString = request.getQueryString();
    164             mCurrentTab = loadLastTabPreference();
    165         } else {
    166             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
    167             mQueryString = savedState.getString(EXTRA_KEY_QUERY);
    168 
    169             // Just set to the field here.  The listener will be notified by update().
    170             mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
    171         }
    172         if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) {
    173             // Invalid tab index was saved (b/12938207). Restore the default.
    174             mCurrentTab = TabState.DEFAULT;
    175         }
    176         // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
    177         // search mode.
    178         update(true /* skipAnimation */);
    179         // Expanding the {@link SearchView} clears the query, so set the query from the
    180         // {@link ContactsRequest} after it has been expanded, if applicable.
    181         if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
    182             setQueryString(mQueryString);
    183         }
    184     }
    185 
    186     public void setListener(Listener listener) {
    187         mListener = listener;
    188     }
    189 
    190     private class SearchTextWatcher implements TextWatcher {
    191 
    192         @Override
    193         public void onTextChanged(CharSequence queryString, int start, int before, int count) {
    194             if (queryString.equals(mQueryString)) {
    195                 return;
    196             }
    197             mQueryString = queryString.toString();
    198             if (!mSearchMode) {
    199                 if (!TextUtils.isEmpty(queryString)) {
    200                     setSearchMode(true);
    201                 }
    202             } else if (mListener != null) {
    203                 mListener.onAction(Action.CHANGE_SEARCH_QUERY);
    204             }
    205         }
    206 
    207         @Override
    208         public void afterTextChanged(Editable s) {}
    209 
    210         @Override
    211         public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
    212     }
    213 
    214     /**
    215      * Save the current tab selection, and notify the listener.
    216      */
    217     public void setCurrentTab(int tab) {
    218         setCurrentTab(tab, true);
    219     }
    220 
    221     /**
    222      * Save the current tab selection.
    223      */
    224     public void setCurrentTab(int tab, boolean notifyListener) {
    225         if (tab == mCurrentTab) {
    226             return;
    227         }
    228         mCurrentTab = tab;
    229 
    230         if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
    231         saveLastTabPreference(mCurrentTab);
    232     }
    233 
    234     public int getCurrentTab() {
    235         return mCurrentTab;
    236     }
    237 
    238     /**
    239      * @return Whether in search mode, i.e. if the search view is visible/expanded.
    240      *
    241      * Note even if the action bar is in search mode, if the query is empty, the search fragment
    242      * will not be in search mode.
    243      */
    244     public boolean isSearchMode() {
    245         return mSearchMode;
    246     }
    247 
    248     public void setSearchMode(boolean flag) {
    249         if (mSearchMode != flag) {
    250             mSearchMode = flag;
    251             update(false /* skipAnimation */);
    252             if (mSearchView == null) {
    253                 return;
    254             }
    255             if (mSearchMode) {
    256                 setFocusOnSearchView();
    257             } else {
    258                 mSearchView.setText(null);
    259             }
    260         } else if (flag) {
    261             // Everything is already set up. Still make sure the keyboard is up
    262             if (mSearchView != null) setFocusOnSearchView();
    263         }
    264     }
    265 
    266     public String getQueryString() {
    267         return mSearchMode ? mQueryString : null;
    268     }
    269 
    270     public void setQueryString(String query) {
    271         mQueryString = query;
    272         if (mSearchView != null) {
    273             mSearchView.setText(query);
    274         }
    275     }
    276 
    277     /** @return true if the "UP" icon is showing. */
    278     public boolean isUpShowing() {
    279         return mSearchMode; // Only shown on the search mode.
    280     }
    281 
    282     private void updateDisplayOptionsInner() {
    283         // All the flags we may change in this method.
    284         final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
    285                 | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
    286 
    287         // The current flags set to the action bar.  (only the ones that we may change here)
    288         final int current = mActionBar.getDisplayOptions() & MASK;
    289 
    290         // Build the new flags...
    291         int newFlags = 0;
    292         if (mShowHomeIcon && !mSearchMode) {
    293             newFlags |= ActionBar.DISPLAY_SHOW_HOME;
    294         }
    295         if (mSearchMode) {
    296             newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
    297             mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd());
    298         } else {
    299             newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
    300             mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart,
    301                     mToolbar.getContentInsetEnd());
    302         }
    303 
    304 
    305         if (current != newFlags) {
    306             // Pass the mask here to preserve other flags that we're not interested here.
    307             mActionBar.setDisplayOptions(newFlags, MASK);
    308         }
    309     }
    310 
    311     private void update(boolean skipAnimation) {
    312         final boolean isIconifiedChanging
    313                 = (mSearchContainer.getParent() == null) == mSearchMode;
    314         if (isIconifiedChanging && !skipAnimation) {
    315             mToolbar.removeView(mLandscapeTabs);
    316             if (mSearchMode) {
    317                 addSearchContainer();
    318                 mSearchContainer.setAlpha(0);
    319                 mSearchContainer.animate().alpha(1);
    320                 animateTabHeightChange(mMaxPortraitTabHeight, 0);
    321                 updateDisplayOptions(isIconifiedChanging);
    322             } else {
    323                 mSearchContainer.setAlpha(1);
    324                 animateTabHeightChange(0, mMaxPortraitTabHeight);
    325                 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() {
    326                     @Override
    327                     public void run() {
    328                         updateDisplayOptionsInner();
    329                         updateDisplayOptions(isIconifiedChanging);
    330                         addLandscapeViewPagerTabs();
    331                         mToolbar.removeView(mSearchContainer);
    332                     }
    333                 });
    334             }
    335             return;
    336         }
    337         if (isIconifiedChanging && skipAnimation) {
    338             mToolbar.removeView(mLandscapeTabs);
    339             if (mSearchMode) {
    340                 setPortraitTabHeight(0);
    341                 addSearchContainer();
    342             } else {
    343                 setPortraitTabHeight(mMaxPortraitTabHeight);
    344                 mToolbar.removeView(mSearchContainer);
    345                 addLandscapeViewPagerTabs();
    346             }
    347         }
    348         updateDisplayOptions(isIconifiedChanging);
    349     }
    350 
    351     private void addLandscapeViewPagerTabs() {
    352         if (mLandscapeTabs != null) {
    353             mToolbar.removeView(mLandscapeTabs);
    354             mToolbar.addView(mLandscapeTabs);
    355         }
    356     }
    357 
    358     private void addSearchContainer() {
    359         mToolbar.removeView(mSearchContainer);
    360         mToolbar.addView(mSearchContainer);
    361     }
    362 
    363     private void updateDisplayOptions(boolean isIconifiedChanging) {
    364         if (mSearchMode) {
    365             setFocusOnSearchView();
    366             // Since we have the {@link SearchView} in a custom action bar, we must manually handle
    367             // expanding the {@link SearchView} when a search is initiated. Note that a side effect
    368             // of this method is that the {@link SearchView} query text is set to empty string.
    369             if (isIconifiedChanging) {
    370                 final CharSequence queryText = mSearchView.getText();
    371                 if (!TextUtils.isEmpty(queryText)) {
    372                     mSearchView.setText(queryText);
    373                 }
    374             }
    375             if (mListener != null) {
    376                 mListener.onAction(Action.START_SEARCH_MODE);
    377             }
    378         } else {
    379             if (mListener != null) {
    380                 mListener.onAction(Action.STOP_SEARCH_MODE);
    381                 mListener.onSelectedTabChanged();
    382             }
    383         }
    384         updateDisplayOptionsInner();
    385     }
    386 
    387     @Override
    388     public boolean onClose() {
    389         setSearchMode(false);
    390         return false;
    391     }
    392 
    393     public void onSaveInstanceState(Bundle outState) {
    394         outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
    395         outState.putString(EXTRA_KEY_QUERY, mQueryString);
    396         outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
    397     }
    398 
    399     /**
    400      * Clears the focus from the {@link SearchView} if we are in search mode.
    401      * This will suppress the IME if it is visible.
    402      */
    403     public void clearFocusOnSearchView() {
    404         if (isSearchMode()) {
    405             if (mSearchView != null) {
    406                 mSearchView.clearFocus();
    407             }
    408         }
    409     }
    410 
    411     public void setFocusOnSearchView() {
    412         mSearchView.requestFocus();
    413         showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
    414     }
    415 
    416     private void showInputMethod(View view) {
    417         final InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
    418                 Context.INPUT_METHOD_SERVICE);
    419         if (imm != null) {
    420             imm.showSoftInput(view, 0);
    421         }
    422     }
    423 
    424     private void saveLastTabPreference(int tab) {
    425         mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
    426     }
    427 
    428     private int loadLastTabPreference() {
    429         try {
    430             return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT);
    431         } catch (IllegalArgumentException e) {
    432             // Preference is corrupt?
    433             return TabState.DEFAULT;
    434         }
    435     }
    436 
    437     private void animateTabHeightChange(int start, int end) {
    438         if (mPortraitTabs == null) {
    439             return;
    440         }
    441         final ValueAnimator animator = ValueAnimator.ofInt(start, end);
    442         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    443             @Override
    444             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    445                 int value = (Integer) valueAnimator.getAnimatedValue();
    446                 setPortraitTabHeight(value);
    447             }
    448         });
    449         animator.setDuration(100).start();
    450     }
    451 
    452     private void setPortraitTabHeight(int height) {
    453         if (mPortraitTabs == null) {
    454             return;
    455         }
    456         ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams();
    457         layoutParams.height = height;
    458         mPortraitTabs.setLayoutParams(layoutParams);
    459     }
    460 }
    461