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