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