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