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