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