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