ScrollingTabContainerView.java revision f5645cbafe7eed33452d888f16726bee8a0cd9fe
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        mTabLayout = createTabLayout();
72        addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
73                ViewGroup.LayoutParams.MATCH_PARENT));
74    }
75
76    @Override
77    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
78        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
79        final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY;
80        setFillViewport(lockedExpanded);
81
82        final int childCount = mTabLayout.getChildCount();
83        if (childCount > 1 &&
84                (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) {
85            if (childCount > 2) {
86                mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f);
87            } else {
88                mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
89            }
90        } else {
91            mMaxTabWidth = -1;
92        }
93
94        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
95        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
96        if (heightMode != MeasureSpec.UNSPECIFIED) {
97            if (mContentHeight == 0 && heightMode == MeasureSpec.EXACTLY) {
98                // Use this as our content height.
99                mContentHeight = heightSize;
100            }
101            heightSize = Math.min(heightSize, mContentHeight);
102            heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
103        }
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    }
183
184    public void setContentHeight(int contentHeight) {
185        mContentHeight = contentHeight;
186        requestLayout();
187    }
188
189    private LinearLayout createTabLayout() {
190        final LinearLayout tabLayout = new LinearLayout(getContext(), null,
191                com.android.internal.R.attr.actionBarTabBarStyle);
192        tabLayout.setMeasureWithLargestChildEnabled(true);
193        tabLayout.setLayoutParams(new LinearLayout.LayoutParams(
194                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
195        return tabLayout;
196    }
197
198    private Spinner createSpinner() {
199        final Spinner spinner = new Spinner(getContext(), null,
200                com.android.internal.R.attr.actionDropDownStyle);
201        spinner.setLayoutParams(new LinearLayout.LayoutParams(
202                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
203        spinner.setOnItemSelectedListener(this);
204        return spinner;
205    }
206
207    @Override
208    protected void onConfigurationChanged(Configuration newConfig) {
209        super.onConfigurationChanged(newConfig);
210
211        // Action bar can change size on configuration changes.
212        // Reread the desired height from the theme-specified style.
213        TypedArray a = getContext().obtainStyledAttributes(null, R.styleable.ActionBar,
214                com.android.internal.R.attr.actionBarStyle, 0);
215        setContentHeight(a.getLayoutDimension(R.styleable.ActionBar_height, 0));
216        a.recycle();
217    }
218
219    public void animateToVisibility(int visibility) {
220        if (mVisibilityAnim != null) {
221            mVisibilityAnim.cancel();
222        }
223        if (visibility == VISIBLE) {
224            if (getVisibility() != VISIBLE) {
225                setAlpha(0);
226            }
227            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1);
228            anim.setDuration(FADE_DURATION);
229            anim.setInterpolator(sAlphaInterpolator);
230
231            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
232            anim.start();
233        } else {
234            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0);
235            anim.setDuration(FADE_DURATION);
236            anim.setInterpolator(sAlphaInterpolator);
237
238            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
239            anim.start();
240        }
241    }
242
243    public void animateToTab(final int position) {
244        final View tabView = mTabLayout.getChildAt(position);
245        if (mTabSelector != null) {
246            removeCallbacks(mTabSelector);
247        }
248        mTabSelector = new Runnable() {
249            public void run() {
250                final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2;
251                smoothScrollTo(scrollPos, 0);
252                mTabSelector = null;
253            }
254        };
255        post(mTabSelector);
256    }
257
258    @Override
259    public void onAttachedToWindow() {
260        super.onAttachedToWindow();
261        if (mTabSelector != null) {
262            // Re-post the selector we saved
263            post(mTabSelector);
264        }
265    }
266
267    @Override
268    public void onDetachedFromWindow() {
269        super.onDetachedFromWindow();
270        if (mTabSelector != null) {
271            removeCallbacks(mTabSelector);
272        }
273    }
274
275    private TabView createTabView(ActionBar.Tab tab, boolean forAdapter) {
276        final TabView tabView = new TabView(getContext(), tab, forAdapter);
277        if (forAdapter) {
278            tabView.setBackgroundDrawable(null);
279            tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT,
280                    mContentHeight));
281        } else {
282            tabView.setFocusable(true);
283
284            if (mTabClickListener == null) {
285                mTabClickListener = new TabClickListener();
286            }
287            tabView.setOnClickListener(mTabClickListener);
288        }
289        return tabView;
290    }
291
292    public void addTab(ActionBar.Tab tab, boolean setSelected) {
293        TabView tabView = createTabView(tab, false);
294        mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0,
295                LayoutParams.MATCH_PARENT, 1));
296        if (mTabSpinner != null) {
297            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
298        }
299        if (setSelected) {
300            tabView.setSelected(true);
301        }
302        if (mAllowCollapse) {
303            requestLayout();
304        }
305    }
306
307    public void addTab(ActionBar.Tab tab, int position, boolean setSelected) {
308        final TabView tabView = createTabView(tab, false);
309        mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams(
310                0, LayoutParams.MATCH_PARENT, 1));
311        if (mTabSpinner != null) {
312            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
313        }
314        if (setSelected) {
315            tabView.setSelected(true);
316        }
317        if (mAllowCollapse) {
318            requestLayout();
319        }
320    }
321
322    public void updateTab(int position) {
323        ((TabView) mTabLayout.getChildAt(position)).update();
324        if (mTabSpinner != null) {
325            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
326        }
327        if (mAllowCollapse) {
328            requestLayout();
329        }
330    }
331
332    public void removeTabAt(int position) {
333        mTabLayout.removeViewAt(position);
334        if (mTabSpinner != null) {
335            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
336        }
337        if (mAllowCollapse) {
338            requestLayout();
339        }
340    }
341
342    public void removeAllTabs() {
343        mTabLayout.removeAllViews();
344        if (mTabSpinner != null) {
345            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
346        }
347        if (mAllowCollapse) {
348            requestLayout();
349        }
350    }
351
352    @Override
353    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
354        TabView tabView = (TabView) view;
355        tabView.getTab().select();
356    }
357
358    @Override
359    public void onNothingSelected(AdapterView<?> parent) {
360    }
361
362    private class TabView extends LinearLayout {
363        private ActionBar.Tab mTab;
364        private TextView mTextView;
365        private ImageView mIconView;
366        private View mCustomView;
367
368        public TabView(Context context, ActionBar.Tab tab, boolean forList) {
369            super(context, null, com.android.internal.R.attr.actionBarTabStyle);
370            mTab = tab;
371
372            if (forList) {
373                setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
374            }
375
376            update();
377        }
378
379        public void bindTab(ActionBar.Tab tab) {
380            mTab = tab;
381            update();
382        }
383
384        @Override
385        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
386            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
387
388            // Re-measure if we went beyond our maximum size.
389            if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) {
390                super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY),
391                        heightMeasureSpec);
392            }
393        }
394
395        public void update() {
396            final ActionBar.Tab tab = mTab;
397            final View custom = tab.getCustomView();
398            if (custom != null) {
399                addView(custom);
400                mCustomView = custom;
401                if (mTextView != null) mTextView.setVisibility(GONE);
402                if (mIconView != null) {
403                    mIconView.setVisibility(GONE);
404                    mIconView.setImageDrawable(null);
405                }
406            } else {
407                if (mCustomView != null) {
408                    removeView(mCustomView);
409                    mCustomView = null;
410                }
411
412                final Drawable icon = tab.getIcon();
413                final CharSequence text = tab.getText();
414
415                if (icon != null) {
416                    if (mIconView == null) {
417                        ImageView iconView = new ImageView(getContext());
418                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
419                                LayoutParams.WRAP_CONTENT);
420                        lp.gravity = Gravity.CENTER_VERTICAL;
421                        iconView.setLayoutParams(lp);
422                        addView(iconView, 0);
423                        mIconView = iconView;
424                    }
425                    mIconView.setImageDrawable(icon);
426                    mIconView.setVisibility(VISIBLE);
427                } else if (mIconView != null) {
428                    mIconView.setVisibility(GONE);
429                    mIconView.setImageDrawable(null);
430                }
431
432                if (text != null) {
433                    if (mTextView == null) {
434                        TextView textView = new TextView(getContext(), null,
435                                com.android.internal.R.attr.actionBarTabTextStyle);
436                        textView.setEllipsize(TruncateAt.END);
437                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
438                                LayoutParams.WRAP_CONTENT);
439                        lp.gravity = Gravity.CENTER_VERTICAL;
440                        textView.setLayoutParams(lp);
441                        addView(textView);
442                        mTextView = textView;
443                    }
444                    mTextView.setText(text);
445                    mTextView.setVisibility(VISIBLE);
446                } else if (mTextView != null) {
447                    mTextView.setVisibility(GONE);
448                    mTextView.setText(null);
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