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