ScrollingTabContainerView.java revision daa1d56726358c4db476dff75c7fd0be201c64c5
1/*
2 * Copyright (C) 2011 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 */
16package com.android.internal.widget;
17
18import com.android.internal.view.ActionBarPolicy;
19
20import android.animation.Animator;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.app.ActionBar;
24import android.content.Context;
25import android.content.res.Configuration;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
28import android.text.TextUtils;
29import android.text.TextUtils.TruncateAt;
30import android.view.Gravity;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.ViewParent;
34import android.view.animation.DecelerateInterpolator;
35import android.widget.AdapterView;
36import android.widget.BaseAdapter;
37import android.widget.HorizontalScrollView;
38import android.widget.ImageView;
39import android.widget.LinearLayout;
40import android.widget.ListView;
41import android.widget.Spinner;
42import android.widget.TextView;
43import android.widget.Toast;
44
45/**
46 * This widget implements the dynamic action bar tab behavior that can change
47 * across different configurations or circumstances.
48 */
49public class ScrollingTabContainerView extends HorizontalScrollView
50        implements AdapterView.OnItemClickListener {
51    private static final String TAG = "ScrollingTabContainerView";
52    Runnable mTabSelector;
53    private TabClickListener mTabClickListener;
54
55    private LinearLayout mTabLayout;
56    private Spinner mTabSpinner;
57    private boolean mAllowCollapse;
58
59    int mMaxTabWidth;
60    int mStackedTabMaxWidth;
61    private int mContentHeight;
62    private int mSelectedTabIndex;
63
64    protected Animator mVisibilityAnim;
65    protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener();
66
67    private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator();
68
69    private static final int FADE_DURATION = 200;
70
71    public ScrollingTabContainerView(Context context) {
72        super(context);
73        setHorizontalScrollBarEnabled(false);
74
75        ActionBarPolicy abp = ActionBarPolicy.get(context);
76        setContentHeight(abp.getTabContainerHeight());
77        mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
78
79        mTabLayout = createTabLayout();
80        addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
81                ViewGroup.LayoutParams.MATCH_PARENT));
82    }
83
84    @Override
85    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
86        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
87        final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY;
88        setFillViewport(lockedExpanded);
89
90        final int childCount = mTabLayout.getChildCount();
91        if (childCount > 1 &&
92                (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) {
93            if (childCount > 2) {
94                mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f);
95            } else {
96                mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
97            }
98            mMaxTabWidth = Math.min(mMaxTabWidth, mStackedTabMaxWidth);
99        } else {
100            mMaxTabWidth = -1;
101        }
102
103        heightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
104
105        final boolean canCollapse = !lockedExpanded && mAllowCollapse;
106
107        if (canCollapse) {
108            // See if we should expand
109            mTabLayout.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec);
110            if (mTabLayout.getMeasuredWidth() > MeasureSpec.getSize(widthMeasureSpec)) {
111                performCollapse();
112            } else {
113                performExpand();
114            }
115        } else {
116            performExpand();
117        }
118
119        final int oldWidth = getMeasuredWidth();
120        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
121        final int newWidth = getMeasuredWidth();
122
123        if (lockedExpanded && oldWidth != newWidth) {
124            // Recenter the tab display if we're at a new (scrollable) size.
125            setTabSelected(mSelectedTabIndex);
126        }
127    }
128
129    /**
130     * Indicates whether this view is collapsed into a dropdown menu instead
131     * of traditional tabs.
132     * @return true if showing as a spinner
133     */
134    private boolean isCollapsed() {
135        return mTabSpinner != null && mTabSpinner.getParent() == this;
136    }
137
138    public void setAllowCollapse(boolean allowCollapse) {
139        mAllowCollapse = allowCollapse;
140    }
141
142    private void performCollapse() {
143        if (isCollapsed()) return;
144
145        if (mTabSpinner == null) {
146            mTabSpinner = createSpinner();
147        }
148        removeView(mTabLayout);
149        addView(mTabSpinner, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
150                ViewGroup.LayoutParams.MATCH_PARENT));
151        if (mTabSpinner.getAdapter() == null) {
152            mTabSpinner.setAdapter(new TabAdapter());
153        }
154        if (mTabSelector != null) {
155            removeCallbacks(mTabSelector);
156            mTabSelector = null;
157        }
158        mTabSpinner.setSelection(mSelectedTabIndex);
159    }
160
161    private boolean performExpand() {
162        if (!isCollapsed()) return false;
163
164        removeView(mTabSpinner);
165        addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
166                ViewGroup.LayoutParams.MATCH_PARENT));
167        setTabSelected(mTabSpinner.getSelectedItemPosition());
168        return false;
169    }
170
171    public void setTabSelected(int position) {
172        mSelectedTabIndex = position;
173        final int tabCount = mTabLayout.getChildCount();
174        for (int i = 0; i < tabCount; i++) {
175            final View child = mTabLayout.getChildAt(i);
176            final boolean isSelected = i == position;
177            child.setSelected(isSelected);
178            if (isSelected) {
179                animateToTab(position);
180            }
181        }
182        if (mTabSpinner != null && position >= 0) {
183            mTabSpinner.setSelection(position);
184        }
185    }
186
187    public void setContentHeight(int contentHeight) {
188        mContentHeight = contentHeight;
189        requestLayout();
190    }
191
192    private LinearLayout createTabLayout() {
193        final LinearLayout tabLayout = new LinearLayout(getContext(), null,
194                com.android.internal.R.attr.actionBarTabBarStyle);
195        tabLayout.setMeasureWithLargestChildEnabled(true);
196        tabLayout.setGravity(Gravity.CENTER);
197        tabLayout.setLayoutParams(new LinearLayout.LayoutParams(
198                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
199        return tabLayout;
200    }
201
202    private Spinner createSpinner() {
203        final Spinner spinner = new Spinner(getContext(), null,
204                com.android.internal.R.attr.actionDropDownStyle);
205        spinner.setLayoutParams(new LinearLayout.LayoutParams(
206                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
207        spinner.setOnItemClickListenerInt(this);
208        return spinner;
209    }
210
211    @Override
212    protected void onConfigurationChanged(Configuration newConfig) {
213        super.onConfigurationChanged(newConfig);
214
215        ActionBarPolicy abp = ActionBarPolicy.get(getContext());
216        // Action bar can change size on configuration changes.
217        // Reread the desired height from the theme-specified style.
218        setContentHeight(abp.getTabContainerHeight());
219        mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
220    }
221
222    public void animateToVisibility(int visibility) {
223        if (mVisibilityAnim != null) {
224            mVisibilityAnim.cancel();
225        }
226        if (visibility == VISIBLE) {
227            if (getVisibility() != VISIBLE) {
228                setAlpha(0);
229            }
230            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1);
231            anim.setDuration(FADE_DURATION);
232            anim.setInterpolator(sAlphaInterpolator);
233
234            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
235            anim.start();
236        } else {
237            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0);
238            anim.setDuration(FADE_DURATION);
239            anim.setInterpolator(sAlphaInterpolator);
240
241            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
242            anim.start();
243        }
244    }
245
246    public void animateToTab(final int position) {
247        final View tabView = mTabLayout.getChildAt(position);
248        if (mTabSelector != null) {
249            removeCallbacks(mTabSelector);
250        }
251        mTabSelector = new Runnable() {
252            public void run() {
253                final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2;
254                smoothScrollTo(scrollPos, 0);
255                mTabSelector = null;
256            }
257        };
258        post(mTabSelector);
259    }
260
261    @Override
262    public void onAttachedToWindow() {
263        super.onAttachedToWindow();
264        if (mTabSelector != null) {
265            // Re-post the selector we saved
266            post(mTabSelector);
267        }
268    }
269
270    @Override
271    public void onDetachedFromWindow() {
272        super.onDetachedFromWindow();
273        if (mTabSelector != null) {
274            removeCallbacks(mTabSelector);
275        }
276    }
277
278    private TabView createTabView(ActionBar.Tab tab, boolean forAdapter) {
279        final TabView tabView = new TabView(getContext(), tab, forAdapter);
280        if (forAdapter) {
281            tabView.setBackgroundDrawable(null);
282            tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT,
283                    mContentHeight));
284        } else {
285            tabView.setFocusable(true);
286
287            if (mTabClickListener == null) {
288                mTabClickListener = new TabClickListener();
289            }
290            tabView.setOnClickListener(mTabClickListener);
291        }
292        return tabView;
293    }
294
295    public void addTab(ActionBar.Tab tab, boolean setSelected) {
296        TabView tabView = createTabView(tab, false);
297        mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0,
298                LayoutParams.MATCH_PARENT, 1));
299        if (mTabSpinner != null) {
300            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
301        }
302        if (setSelected) {
303            tabView.setSelected(true);
304        }
305        if (mAllowCollapse) {
306            requestLayout();
307        }
308    }
309
310    public void addTab(ActionBar.Tab tab, int position, boolean setSelected) {
311        final TabView tabView = createTabView(tab, false);
312        mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams(
313                0, LayoutParams.MATCH_PARENT, 1));
314        if (mTabSpinner != null) {
315            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
316        }
317        if (setSelected) {
318            tabView.setSelected(true);
319        }
320        if (mAllowCollapse) {
321            requestLayout();
322        }
323    }
324
325    public void updateTab(int position) {
326        ((TabView) mTabLayout.getChildAt(position)).update();
327        if (mTabSpinner != null) {
328            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
329        }
330        if (mAllowCollapse) {
331            requestLayout();
332        }
333    }
334
335    public void removeTabAt(int position) {
336        mTabLayout.removeViewAt(position);
337        if (mTabSpinner != null) {
338            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
339        }
340        if (mAllowCollapse) {
341            requestLayout();
342        }
343    }
344
345    public void removeAllTabs() {
346        mTabLayout.removeAllViews();
347        if (mTabSpinner != null) {
348            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
349        }
350        if (mAllowCollapse) {
351            requestLayout();
352        }
353    }
354
355    @Override
356    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
357        TabView tabView = (TabView) view;
358        tabView.getTab().select();
359    }
360
361    private class TabView extends LinearLayout implements OnLongClickListener {
362        private ActionBar.Tab mTab;
363        private TextView mTextView;
364        private ImageView mIconView;
365        private View mCustomView;
366
367        public TabView(Context context, ActionBar.Tab tab, boolean forList) {
368            super(context, null, com.android.internal.R.attr.actionBarTabStyle);
369            mTab = tab;
370
371            if (forList) {
372                setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
373            }
374
375            update();
376        }
377
378        public void bindTab(ActionBar.Tab tab) {
379            mTab = tab;
380            update();
381        }
382
383        @Override
384        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
385            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
386
387            // Re-measure if we went beyond our maximum size.
388            if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) {
389                super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY),
390                        heightMeasureSpec);
391            }
392        }
393
394        public void update() {
395            final ActionBar.Tab tab = mTab;
396            final View custom = tab.getCustomView();
397            if (custom != null) {
398                final ViewParent customParent = custom.getParent();
399                if (customParent != this) {
400                    if (customParent != null) ((ViewGroup) customParent).removeView(custom);
401                    addView(custom);
402                }
403                mCustomView = custom;
404                if (mTextView != null) mTextView.setVisibility(GONE);
405                if (mIconView != null) {
406                    mIconView.setVisibility(GONE);
407                    mIconView.setImageDrawable(null);
408                }
409            } else {
410                if (mCustomView != null) {
411                    removeView(mCustomView);
412                    mCustomView = null;
413                }
414
415                final Drawable icon = tab.getIcon();
416                final CharSequence text = tab.getText();
417
418                if (icon != null) {
419                    if (mIconView == null) {
420                        ImageView iconView = new ImageView(getContext());
421                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
422                                LayoutParams.WRAP_CONTENT);
423                        lp.gravity = Gravity.CENTER_VERTICAL;
424                        iconView.setLayoutParams(lp);
425                        addView(iconView, 0);
426                        mIconView = iconView;
427                    }
428                    mIconView.setImageDrawable(icon);
429                    mIconView.setVisibility(VISIBLE);
430                } else if (mIconView != null) {
431                    mIconView.setVisibility(GONE);
432                    mIconView.setImageDrawable(null);
433                }
434
435                final boolean hasText = !TextUtils.isEmpty(text);
436                if (hasText) {
437                    if (mTextView == null) {
438                        TextView textView = new TextView(getContext(), null,
439                                com.android.internal.R.attr.actionBarTabTextStyle);
440                        textView.setEllipsize(TruncateAt.END);
441                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
442                                LayoutParams.WRAP_CONTENT);
443                        lp.gravity = Gravity.CENTER_VERTICAL;
444                        textView.setLayoutParams(lp);
445                        addView(textView);
446                        mTextView = textView;
447                    }
448                    mTextView.setText(text);
449                    mTextView.setVisibility(VISIBLE);
450                } else if (mTextView != null) {
451                    mTextView.setVisibility(GONE);
452                    mTextView.setText(null);
453                }
454
455                if (mIconView != null) {
456                    mIconView.setContentDescription(tab.getContentDescription());
457                }
458
459                if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) {
460                    setOnLongClickListener(this);
461                } else {
462                    setOnLongClickListener(null);
463                    setLongClickable(false);
464                }
465            }
466        }
467
468        public boolean onLongClick(View v) {
469            final int[] screenPos = new int[2];
470            getLocationOnScreen(screenPos);
471
472            final Context context = getContext();
473            final int width = getWidth();
474            final int height = getHeight();
475            final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
476
477            Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(),
478                    Toast.LENGTH_SHORT);
479            // Show under the tab
480            cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
481                    (screenPos[0] + width / 2) - screenWidth / 2, height);
482
483            cheatSheet.show();
484            return true;
485        }
486
487        public ActionBar.Tab getTab() {
488            return mTab;
489        }
490    }
491
492    private class TabAdapter extends BaseAdapter {
493        @Override
494        public int getCount() {
495            return mTabLayout.getChildCount();
496        }
497
498        @Override
499        public Object getItem(int position) {
500            return ((TabView) mTabLayout.getChildAt(position)).getTab();
501        }
502
503        @Override
504        public long getItemId(int position) {
505            return position;
506        }
507
508        @Override
509        public View getView(int position, View convertView, ViewGroup parent) {
510            if (convertView == null) {
511                convertView = createTabView((ActionBar.Tab) getItem(position), true);
512            } else {
513                ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
514            }
515            return convertView;
516        }
517    }
518
519    private class TabClickListener implements OnClickListener {
520        public void onClick(View view) {
521            TabView tabView = (TabView) view;
522            tabView.getTab().select();
523            final int tabCount = mTabLayout.getChildCount();
524            for (int i = 0; i < tabCount; i++) {
525                final View child = mTabLayout.getChildAt(i);
526                child.setSelected(child == view);
527            }
528        }
529    }
530
531    protected class VisibilityAnimListener implements Animator.AnimatorListener {
532        private boolean mCanceled = false;
533        private int mFinalVisibility;
534
535        public VisibilityAnimListener withFinalVisibility(int visibility) {
536            mFinalVisibility = visibility;
537            return this;
538        }
539
540        @Override
541        public void onAnimationStart(Animator animation) {
542            setVisibility(VISIBLE);
543            mVisibilityAnim = animation;
544            mCanceled = false;
545        }
546
547        @Override
548        public void onAnimationEnd(Animator animation) {
549            if (mCanceled) return;
550
551            mVisibilityAnim = null;
552            setVisibility(mFinalVisibility);
553        }
554
555        @Override
556        public void onAnimationCancel(Animator animation) {
557            mCanceled = true;
558        }
559
560        @Override
561        public void onAnimationRepeat(Animator animation) {
562        }
563    }
564}
565