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.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            final TabAdapter adapter = new TabAdapter(mContext);
153            adapter.setDropDownViewContext(mTabSpinner.getPopupContext());
154            mTabSpinner.setAdapter(adapter);
155        }
156        if (mTabSelector != null) {
157            removeCallbacks(mTabSelector);
158            mTabSelector = null;
159        }
160        mTabSpinner.setSelection(mSelectedTabIndex);
161    }
162
163    private boolean performExpand() {
164        if (!isCollapsed()) return false;
165
166        removeView(mTabSpinner);
167        addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
168                ViewGroup.LayoutParams.MATCH_PARENT));
169        setTabSelected(mTabSpinner.getSelectedItemPosition());
170        return false;
171    }
172
173    public void setTabSelected(int position) {
174        mSelectedTabIndex = position;
175        final int tabCount = mTabLayout.getChildCount();
176        for (int i = 0; i < tabCount; i++) {
177            final View child = mTabLayout.getChildAt(i);
178            final boolean isSelected = i == position;
179            child.setSelected(isSelected);
180            if (isSelected) {
181                animateToTab(position);
182            }
183        }
184        if (mTabSpinner != null && position >= 0) {
185            mTabSpinner.setSelection(position);
186        }
187    }
188
189    public void setContentHeight(int contentHeight) {
190        mContentHeight = contentHeight;
191        requestLayout();
192    }
193
194    private LinearLayout createTabLayout() {
195        final LinearLayout tabLayout = new LinearLayout(getContext(), null,
196                com.android.internal.R.attr.actionBarTabBarStyle);
197        tabLayout.setMeasureWithLargestChildEnabled(true);
198        tabLayout.setGravity(Gravity.CENTER);
199        tabLayout.setLayoutParams(new LinearLayout.LayoutParams(
200                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
201        return tabLayout;
202    }
203
204    private Spinner createSpinner() {
205        final Spinner spinner = new Spinner(getContext(), null,
206                com.android.internal.R.attr.actionDropDownStyle);
207        spinner.setLayoutParams(new LinearLayout.LayoutParams(
208                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT));
209        spinner.setOnItemClickListenerInt(this);
210        return spinner;
211    }
212
213    @Override
214    protected void onConfigurationChanged(Configuration newConfig) {
215        super.onConfigurationChanged(newConfig);
216
217        ActionBarPolicy abp = ActionBarPolicy.get(getContext());
218        // Action bar can change size on configuration changes.
219        // Reread the desired height from the theme-specified style.
220        setContentHeight(abp.getTabContainerHeight());
221        mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
222    }
223
224    public void animateToVisibility(int visibility) {
225        if (mVisibilityAnim != null) {
226            mVisibilityAnim.cancel();
227        }
228        if (visibility == VISIBLE) {
229            if (getVisibility() != VISIBLE) {
230                setAlpha(0);
231            }
232            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1);
233            anim.setDuration(FADE_DURATION);
234            anim.setInterpolator(sAlphaInterpolator);
235
236            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
237            anim.start();
238        } else {
239            ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0);
240            anim.setDuration(FADE_DURATION);
241            anim.setInterpolator(sAlphaInterpolator);
242
243            anim.addListener(mVisAnimListener.withFinalVisibility(visibility));
244            anim.start();
245        }
246    }
247
248    public void animateToTab(final int position) {
249        final View tabView = mTabLayout.getChildAt(position);
250        if (mTabSelector != null) {
251            removeCallbacks(mTabSelector);
252        }
253        mTabSelector = new Runnable() {
254            public void run() {
255                final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2;
256                smoothScrollTo(scrollPos, 0);
257                mTabSelector = null;
258            }
259        };
260        post(mTabSelector);
261    }
262
263    @Override
264    public void onAttachedToWindow() {
265        super.onAttachedToWindow();
266        if (mTabSelector != null) {
267            // Re-post the selector we saved
268            post(mTabSelector);
269        }
270    }
271
272    @Override
273    public void onDetachedFromWindow() {
274        super.onDetachedFromWindow();
275        if (mTabSelector != null) {
276            removeCallbacks(mTabSelector);
277        }
278    }
279
280    private TabView createTabView(Context context, ActionBar.Tab tab, boolean forAdapter) {
281        final TabView tabView = new TabView(context, tab, forAdapter);
282        if (forAdapter) {
283            tabView.setBackgroundDrawable(null);
284            tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT,
285                    mContentHeight));
286        } else {
287            tabView.setFocusable(true);
288
289            if (mTabClickListener == null) {
290                mTabClickListener = new TabClickListener();
291            }
292            tabView.setOnClickListener(mTabClickListener);
293        }
294        return tabView;
295    }
296
297    public void addTab(ActionBar.Tab tab, boolean setSelected) {
298        TabView tabView = createTabView(mContext, tab, false);
299        mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0,
300                LayoutParams.MATCH_PARENT, 1));
301        if (mTabSpinner != null) {
302            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
303        }
304        if (setSelected) {
305            tabView.setSelected(true);
306        }
307        if (mAllowCollapse) {
308            requestLayout();
309        }
310    }
311
312    public void addTab(ActionBar.Tab tab, int position, boolean setSelected) {
313        final TabView tabView = createTabView(mContext, tab, false);
314        mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams(
315                0, LayoutParams.MATCH_PARENT, 1));
316        if (mTabSpinner != null) {
317            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
318        }
319        if (setSelected) {
320            tabView.setSelected(true);
321        }
322        if (mAllowCollapse) {
323            requestLayout();
324        }
325    }
326
327    public void updateTab(int position) {
328        ((TabView) mTabLayout.getChildAt(position)).update();
329        if (mTabSpinner != null) {
330            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
331        }
332        if (mAllowCollapse) {
333            requestLayout();
334        }
335    }
336
337    public void removeTabAt(int position) {
338        mTabLayout.removeViewAt(position);
339        if (mTabSpinner != null) {
340            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
341        }
342        if (mAllowCollapse) {
343            requestLayout();
344        }
345    }
346
347    public void removeAllTabs() {
348        mTabLayout.removeAllViews();
349        if (mTabSpinner != null) {
350            ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
351        }
352        if (mAllowCollapse) {
353            requestLayout();
354        }
355    }
356
357    @Override
358    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
359        TabView tabView = (TabView) view;
360        tabView.getTab().select();
361    }
362
363    private class TabView extends LinearLayout implements OnLongClickListener {
364        private ActionBar.Tab mTab;
365        private TextView mTextView;
366        private ImageView mIconView;
367        private View mCustomView;
368
369        public TabView(Context context, ActionBar.Tab tab, boolean forList) {
370            super(context, null, com.android.internal.R.attr.actionBarTabStyle);
371            mTab = tab;
372
373            if (forList) {
374                setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
375            }
376
377            update();
378        }
379
380        public void bindTab(ActionBar.Tab tab) {
381            mTab = tab;
382            update();
383        }
384
385        @Override
386        public void setSelected(boolean selected) {
387            final boolean changed = (isSelected() != selected);
388            super.setSelected(selected);
389            if (changed && selected) {
390                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
391            }
392        }
393
394        @Override
395        public CharSequence getAccessibilityClassName() {
396            // This view masquerades as an action bar tab.
397            return ActionBar.Tab.class.getName();
398        }
399
400        @Override
401        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
402            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
403
404            // Re-measure if we went beyond our maximum size.
405            if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) {
406                super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY),
407                        heightMeasureSpec);
408            }
409        }
410
411        public void update() {
412            final ActionBar.Tab tab = mTab;
413            final View custom = tab.getCustomView();
414            if (custom != null) {
415                final ViewParent customParent = custom.getParent();
416                if (customParent != this) {
417                    if (customParent != null) ((ViewGroup) customParent).removeView(custom);
418                    addView(custom);
419                }
420                mCustomView = custom;
421                if (mTextView != null) mTextView.setVisibility(GONE);
422                if (mIconView != null) {
423                    mIconView.setVisibility(GONE);
424                    mIconView.setImageDrawable(null);
425                }
426            } else {
427                if (mCustomView != null) {
428                    removeView(mCustomView);
429                    mCustomView = null;
430                }
431
432                final Drawable icon = tab.getIcon();
433                final CharSequence text = tab.getText();
434
435                if (icon != null) {
436                    if (mIconView == null) {
437                        ImageView iconView = new ImageView(getContext());
438                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
439                                LayoutParams.WRAP_CONTENT);
440                        lp.gravity = Gravity.CENTER_VERTICAL;
441                        iconView.setLayoutParams(lp);
442                        addView(iconView, 0);
443                        mIconView = iconView;
444                    }
445                    mIconView.setImageDrawable(icon);
446                    mIconView.setVisibility(VISIBLE);
447                } else if (mIconView != null) {
448                    mIconView.setVisibility(GONE);
449                    mIconView.setImageDrawable(null);
450                }
451
452                final boolean hasText = !TextUtils.isEmpty(text);
453                if (hasText) {
454                    if (mTextView == null) {
455                        TextView textView = new TextView(getContext(), null,
456                                com.android.internal.R.attr.actionBarTabTextStyle);
457                        textView.setEllipsize(TruncateAt.END);
458                        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
459                                LayoutParams.WRAP_CONTENT);
460                        lp.gravity = Gravity.CENTER_VERTICAL;
461                        textView.setLayoutParams(lp);
462                        addView(textView);
463                        mTextView = textView;
464                    }
465                    mTextView.setText(text);
466                    mTextView.setVisibility(VISIBLE);
467                } else if (mTextView != null) {
468                    mTextView.setVisibility(GONE);
469                    mTextView.setText(null);
470                }
471
472                if (mIconView != null) {
473                    mIconView.setContentDescription(tab.getContentDescription());
474                }
475
476                if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) {
477                    setOnLongClickListener(this);
478                } else {
479                    setOnLongClickListener(null);
480                    setLongClickable(false);
481                }
482            }
483        }
484
485        public boolean onLongClick(View v) {
486            final int[] screenPos = new int[2];
487            getLocationOnScreen(screenPos);
488
489            final Context context = getContext();
490            final int width = getWidth();
491            final int height = getHeight();
492            final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
493
494            Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(),
495                    Toast.LENGTH_SHORT);
496            // Show under the tab
497            cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
498                    (screenPos[0] + width / 2) - screenWidth / 2, height);
499
500            cheatSheet.show();
501            return true;
502        }
503
504        public ActionBar.Tab getTab() {
505            return mTab;
506        }
507    }
508
509    private class TabAdapter extends BaseAdapter {
510        private Context mDropDownContext;
511
512        public TabAdapter(Context context) {
513            setDropDownViewContext(context);
514        }
515
516        public void setDropDownViewContext(Context context) {
517            mDropDownContext = context;
518        }
519
520        @Override
521        public int getCount() {
522            return mTabLayout.getChildCount();
523        }
524
525        @Override
526        public Object getItem(int position) {
527            return ((TabView) mTabLayout.getChildAt(position)).getTab();
528        }
529
530        @Override
531        public long getItemId(int position) {
532            return position;
533        }
534
535        @Override
536        public View getView(int position, View convertView, ViewGroup parent) {
537            if (convertView == null) {
538                convertView = createTabView(mContext, (ActionBar.Tab) getItem(position), true);
539            } else {
540                ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
541            }
542            return convertView;
543        }
544
545        @Override
546        public View getDropDownView(int position, View convertView, ViewGroup parent) {
547            if (convertView == null) {
548                convertView = createTabView(mDropDownContext,
549                        (ActionBar.Tab) getItem(position), true);
550            } else {
551                ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
552            }
553            return convertView;
554        }
555    }
556
557    private class TabClickListener implements OnClickListener {
558        public void onClick(View view) {
559            TabView tabView = (TabView) view;
560            tabView.getTab().select();
561            final int tabCount = mTabLayout.getChildCount();
562            for (int i = 0; i < tabCount; i++) {
563                final View child = mTabLayout.getChildAt(i);
564                child.setSelected(child == view);
565            }
566        }
567    }
568
569    protected class VisibilityAnimListener implements Animator.AnimatorListener {
570        private boolean mCanceled = false;
571        private int mFinalVisibility;
572
573        public VisibilityAnimListener withFinalVisibility(int visibility) {
574            mFinalVisibility = visibility;
575            return this;
576        }
577
578        @Override
579        public void onAnimationStart(Animator animation) {
580            setVisibility(VISIBLE);
581            mVisibilityAnim = animation;
582            mCanceled = false;
583        }
584
585        @Override
586        public void onAnimationEnd(Animator animation) {
587            if (mCanceled) return;
588
589            mVisibilityAnim = null;
590            setVisibility(mFinalVisibility);
591        }
592
593        @Override
594        public void onAnimationCancel(Animator animation) {
595            mCanceled = true;
596        }
597
598        @Override
599        public void onAnimationRepeat(Animator animation) {
600        }
601    }
602}
603