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