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