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