ActionBarAdapter.java revision 8d71f11c70c974518f3482c46a10a5601628a029
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.content.Context;
22import android.content.SharedPreferences;
23import android.content.res.TypedArray;
24import android.os.Bundle;
25import android.preference.PreferenceManager;
26import android.text.Editable;
27import android.text.TextUtils;
28import android.text.TextWatcher;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.inputmethod.InputMethodManager;
33import android.widget.SearchView;
34import android.widget.SearchView.OnCloseListener;
35import android.view.View.OnClickListener;
36import android.widget.EditText;
37import android.widget.Toolbar;
38
39import com.android.contacts.R;
40import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
41import com.android.contacts.list.ContactsRequest;
42
43/**
44 * Adapter for the action bar at the top of the Contacts activity.
45 */
46public 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