Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2009 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 
     17 package com.android.contacts;
     18 
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.util.AttributeSet;
     22 import android.view.LayoutInflater;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 import android.view.ViewTreeObserver;
     26 import android.view.View.OnClickListener;
     27 import android.view.View.OnFocusChangeListener;
     28 import android.widget.HorizontalScrollView;
     29 import android.widget.ImageView;
     30 import android.widget.RelativeLayout;
     31 
     32 /*
     33  * Tab widget that can contain more tabs than can fit on screen at once and scroll over them.
     34  */
     35 public class ScrollingTabWidget extends RelativeLayout
     36         implements OnClickListener, ViewTreeObserver.OnGlobalFocusChangeListener,
     37         OnFocusChangeListener {
     38 
     39     private static final String TAG = "ScrollingTabWidget";
     40 
     41     private OnTabSelectionChangedListener mSelectionChangedListener;
     42     private int mSelectedTab = 0;
     43     private ImageView mLeftArrowView;
     44     private ImageView mRightArrowView;
     45     private HorizontalScrollView mTabsScrollWrapper;
     46     private TabStripView mTabsView;
     47     private LayoutInflater mInflater;
     48 
     49     // Keeps track of the left most visible tab.
     50     private int mLeftMostVisibleTabIndex = 0;
     51 
     52     public ScrollingTabWidget(Context context) {
     53         this(context, null);
     54     }
     55 
     56     public ScrollingTabWidget(Context context, AttributeSet attrs) {
     57         this(context, attrs, 0);
     58     }
     59 
     60     public ScrollingTabWidget(Context context, AttributeSet attrs, int defStyle) {
     61         super(context, attrs);
     62 
     63         mInflater = (LayoutInflater) mContext.getSystemService(
     64                 Context.LAYOUT_INFLATER_SERVICE);
     65 
     66         setFocusable(true);
     67         setOnFocusChangeListener(this);
     68         if (!hasFocus()) {
     69             setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
     70         }
     71 
     72         mLeftArrowView = (ImageView) mInflater.inflate(R.layout.tab_left_arrow, this, false);
     73         mLeftArrowView.setOnClickListener(this);
     74         mRightArrowView = (ImageView) mInflater.inflate(R.layout.tab_right_arrow, this, false);
     75         mRightArrowView.setOnClickListener(this);
     76         mTabsScrollWrapper = (HorizontalScrollView) mInflater.inflate(
     77                 R.layout.tab_layout, this, false);
     78         mTabsView = (TabStripView) mTabsScrollWrapper.findViewById(android.R.id.tabs);
     79         View accountNameView = mInflater.inflate(R.layout.tab_account_name, this, false);
     80 
     81         mLeftArrowView.setVisibility(View.INVISIBLE);
     82         mRightArrowView.setVisibility(View.INVISIBLE);
     83 
     84         addView(mTabsScrollWrapper);
     85         addView(mLeftArrowView);
     86         addView(mRightArrowView);
     87         addView(accountNameView);
     88     }
     89 
     90     @Override
     91     protected void onAttachedToWindow() {
     92         super.onAttachedToWindow();
     93         final ViewTreeObserver treeObserver = getViewTreeObserver();
     94         if (treeObserver != null) {
     95             treeObserver.addOnGlobalFocusChangeListener(this);
     96         }
     97     }
     98 
     99     @Override
    100     protected void onDetachedFromWindow() {
    101         super.onDetachedFromWindow();
    102         final ViewTreeObserver treeObserver = getViewTreeObserver();
    103         if (treeObserver != null) {
    104             treeObserver.removeOnGlobalFocusChangeListener(this);
    105         }
    106     }
    107 
    108     protected void updateArrowVisibility() {
    109         int scrollViewLeftEdge = mTabsScrollWrapper.getScrollX();
    110         int tabsViewLeftEdge = mTabsView.getLeft();
    111         int scrollViewRightEdge = scrollViewLeftEdge + mTabsScrollWrapper.getWidth();
    112         int tabsViewRightEdge = mTabsView.getRight();
    113 
    114         int rightArrowCurrentVisibility = mRightArrowView.getVisibility();
    115         if (scrollViewRightEdge == tabsViewRightEdge
    116                 && rightArrowCurrentVisibility == View.VISIBLE) {
    117             mRightArrowView.setVisibility(View.INVISIBLE);
    118         } else if (scrollViewRightEdge < tabsViewRightEdge
    119                 && rightArrowCurrentVisibility != View.VISIBLE) {
    120             mRightArrowView.setVisibility(View.VISIBLE);
    121         }
    122 
    123         int leftArrowCurrentVisibility = mLeftArrowView.getVisibility();
    124         if (scrollViewLeftEdge == tabsViewLeftEdge
    125                 && leftArrowCurrentVisibility == View.VISIBLE) {
    126             mLeftArrowView.setVisibility(View.INVISIBLE);
    127         } else if (scrollViewLeftEdge > tabsViewLeftEdge
    128                 && leftArrowCurrentVisibility != View.VISIBLE) {
    129             mLeftArrowView.setVisibility(View.VISIBLE);
    130         }
    131     }
    132 
    133     /**
    134      * Returns the tab indicator view at the given index.
    135      *
    136      * @param index the zero-based index of the tab indicator view to return
    137      * @return the tab indicator view at the given index
    138      */
    139     public View getChildTabViewAt(int index) {
    140         return mTabsView.getChildAt(index);
    141     }
    142 
    143     /**
    144      * Returns the number of tab indicator views.
    145      *
    146      * @return the number of tab indicator views.
    147      */
    148     public int getTabCount() {
    149         return mTabsView.getChildCount();
    150     }
    151 
    152     /**
    153      * Returns the {@link ViewGroup} that actually contains the tabs. This is where the tab
    154      * views should be attached to when being inflated.
    155      */
    156     public ViewGroup getTabParent() {
    157         return mTabsView;
    158     }
    159 
    160     public void removeAllTabs() {
    161         mTabsView.removeAllViews();
    162     }
    163 
    164     @Override
    165     public void dispatchDraw(Canvas canvas) {
    166         updateArrowVisibility();
    167         super.dispatchDraw(canvas);
    168     }
    169 
    170     /**
    171      * Sets the current tab.
    172      * This method is used to bring a tab to the front of the Widget,
    173      * and is used to post to the rest of the UI that a different tab
    174      * has been brought to the foreground.
    175      *
    176      * Note, this is separate from the traditional "focus" that is
    177      * employed from the view logic.
    178      *
    179      * For instance, if we have a list in a tabbed view, a user may be
    180      * navigating up and down the list, moving the UI focus (orange
    181      * highlighting) through the list items.  The cursor movement does
    182      * not effect the "selected" tab though, because what is being
    183      * scrolled through is all on the same tab.  The selected tab only
    184      * changes when we navigate between tabs (moving from the list view
    185      * to the next tabbed view, in this example).
    186      *
    187      * To move both the focus AND the selected tab at once, please use
    188      * {@link #focusCurrentTab}. Normally, the view logic takes care of
    189      * adjusting the focus, so unless you're circumventing the UI,
    190      * you'll probably just focus your interest here.
    191      *
    192      *  @param index The tab that you want to indicate as the selected
    193      *  tab (tab brought to the front of the widget)
    194      *
    195      *  @see #focusCurrentTab
    196      */
    197     public void setCurrentTab(int index) {
    198         if (index < 0 || index >= getTabCount()) {
    199             return;
    200         }
    201 
    202         if (mSelectedTab < getTabCount()) {
    203             mTabsView.setSelected(mSelectedTab, false);
    204         }
    205         mSelectedTab = index;
    206         mTabsView.setSelected(mSelectedTab, true);
    207     }
    208 
    209     /**
    210      * Return index of the currently selected tab.
    211      */
    212     public int getCurrentTab() {
    213         return mSelectedTab;
    214     }
    215 
    216     /**
    217      * Sets the current tab and focuses the UI on it.
    218      * This method makes sure that the focused tab matches the selected
    219      * tab, normally at {@link #setCurrentTab}.  Normally this would not
    220      * be an issue if we go through the UI, since the UI is responsible
    221      * for calling TabWidget.onFocusChanged(), but in the case where we
    222      * are selecting the tab programmatically, we'll need to make sure
    223      * focus keeps up.
    224      *
    225      *  @param index The tab that you want focused (highlighted in orange)
    226      *  and selected (tab brought to the front of the widget)
    227      *
    228      *  @see #setCurrentTab
    229      */
    230     public void focusCurrentTab(int index) {
    231         if (index < 0 || index >= getTabCount()) {
    232             return;
    233         }
    234 
    235         setCurrentTab(index);
    236         getChildTabViewAt(index).requestFocus();
    237 
    238     }
    239 
    240     /**
    241      * Adds a tab to the list of tabs. The tab's indicator view is specified
    242      * by a layout id. InflateException will be thrown if there is a problem
    243      * inflating.
    244      *
    245      * @param layoutResId The layout id to be inflated to make the tab indicator.
    246      */
    247     public void addTab(int layoutResId) {
    248         addTab(mInflater.inflate(layoutResId, mTabsView, false));
    249     }
    250 
    251     /**
    252      * Adds a tab to the list of tabs. The tab's indicator view must be provided.
    253      *
    254      * @param child
    255      */
    256     public void addTab(View child) {
    257         if (child == null) {
    258             return;
    259         }
    260 
    261         if (child.getLayoutParams() == null) {
    262             final LayoutParams lp = new LayoutParams(
    263                     ViewGroup.LayoutParams.WRAP_CONTENT,
    264                     ViewGroup.LayoutParams.WRAP_CONTENT);
    265             lp.setMargins(0, 0, 0, 0);
    266             child.setLayoutParams(lp);
    267         }
    268 
    269         // Ensure you can navigate to the tab with the keyboard, and you can touch it
    270         child.setFocusable(true);
    271         child.setClickable(true);
    272         child.setOnClickListener(new TabClickListener());
    273         child.setOnFocusChangeListener(this);
    274 
    275         mTabsView.addView(child);
    276     }
    277 
    278     /**
    279      * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
    280      * user clicked on a tab indicator.
    281      */
    282     public void setTabSelectionListener(OnTabSelectionChangedListener listener) {
    283         mSelectionChangedListener = listener;
    284     }
    285 
    286     public void onGlobalFocusChanged(View oldFocus, View newFocus) {
    287         if (isTab(oldFocus) && !isTab(newFocus)) {
    288             onLoseFocus();
    289         }
    290     }
    291 
    292     public void onFocusChange(View v, boolean hasFocus) {
    293         if (v == this && hasFocus) {
    294             onObtainFocus();
    295             return;
    296         }
    297 
    298         if (hasFocus) {
    299             for (int i = 0; i < getTabCount(); i++) {
    300                 if (getChildTabViewAt(i) == v) {
    301                     setCurrentTab(i);
    302                     mSelectionChangedListener.onTabSelectionChanged(i, false);
    303                     break;
    304                 }
    305             }
    306         }
    307     }
    308 
    309     /**
    310      * Called when the {@link ScrollingTabWidget} gets focus. Here the
    311      * widget decides which of it's tabs should have focus.
    312      */
    313     protected void onObtainFocus() {
    314         // Setting this flag, allows the children of this View to obtain focus.
    315         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
    316         // Assign focus to the last selected tab.
    317         focusCurrentTab(mSelectedTab);
    318         mSelectionChangedListener.onTabSelectionChanged(mSelectedTab, false);
    319     }
    320 
    321     /**
    322      * Called when the focus has left the {@link ScrollingTabWidget} or its
    323      * descendants. At this time we want the children of this view to be marked
    324      * as un-focusable, so that next time focus is moved to the widget, the widget
    325      * gets control, and can assign focus where it wants.
    326      */
    327     protected void onLoseFocus() {
    328         // Setting this flag will effectively make the tabs unfocusable. This will
    329         // be toggled when the widget obtains focus again.
    330         setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
    331     }
    332 
    333     public boolean isTab(View v) {
    334         for (int i = 0; i < getTabCount(); i++) {
    335             if (getChildTabViewAt(i) == v) {
    336                 return true;
    337             }
    338         }
    339         return false;
    340     }
    341 
    342     private class TabClickListener implements OnClickListener {
    343         public void onClick(View v) {
    344             for (int i = 0; i < getTabCount(); i++) {
    345                 if (getChildTabViewAt(i) == v) {
    346                     setCurrentTab(i);
    347                     mSelectionChangedListener.onTabSelectionChanged(i, true);
    348                     break;
    349                 }
    350             }
    351         }
    352     }
    353 
    354     public interface OnTabSelectionChangedListener {
    355         /**
    356          * Informs the tab widget host which tab was selected. It also indicates
    357          * if the tab was clicked/pressed or just focused into.
    358          *
    359          * @param tabIndex index of the tab that was selected
    360          * @param clicked whether the selection changed due to a touch/click
    361          * or due to focus entering the tab through navigation. Pass true
    362          * if it was due to a press/click and false otherwise.
    363          */
    364         void onTabSelectionChanged(int tabIndex, boolean clicked);
    365     }
    366 
    367     public void onClick(View v) {
    368         updateLeftMostVisible();
    369         if (v == mRightArrowView && (mLeftMostVisibleTabIndex + 1 < getTabCount())) {
    370             tabScroll(true /* right */);
    371         } else if (v == mLeftArrowView && mLeftMostVisibleTabIndex > 0) {
    372             tabScroll(false /* left */);
    373         }
    374     }
    375 
    376     /*
    377      * Updates our record of the left most visible tab. We keep track of this explicitly
    378      * on arrow clicks, but need to re-calibrate after focus navigation.
    379      */
    380     protected void updateLeftMostVisible() {
    381         int viewableLeftEdge = mTabsScrollWrapper.getScrollX();
    382 
    383         if (mLeftArrowView.getVisibility() == View.VISIBLE) {
    384             viewableLeftEdge += mLeftArrowView.getWidth();
    385         }
    386 
    387         for (int i = 0; i < getTabCount(); i++) {
    388             View tab = getChildTabViewAt(i);
    389             int tabLeftEdge = tab.getLeft();
    390             if (tabLeftEdge >= viewableLeftEdge) {
    391                 mLeftMostVisibleTabIndex = i;
    392                 break;
    393             }
    394         }
    395     }
    396 
    397     /**
    398      * Scrolls the tabs by exactly one tab width.
    399      *
    400      * @param directionRight if true, scroll to the right, if false, scroll to the left.
    401      */
    402     protected void tabScroll(boolean directionRight) {
    403         int scrollWidth = 0;
    404         View newLeftMostVisibleTab = null;
    405         if (directionRight) {
    406             newLeftMostVisibleTab = getChildTabViewAt(++mLeftMostVisibleTabIndex);
    407         } else {
    408             newLeftMostVisibleTab = getChildTabViewAt(--mLeftMostVisibleTabIndex);
    409         }
    410 
    411         scrollWidth = newLeftMostVisibleTab.getLeft() - mTabsScrollWrapper.getScrollX();
    412         if (mLeftMostVisibleTabIndex > 0) {
    413             scrollWidth -= mLeftArrowView.getWidth();
    414         }
    415         mTabsScrollWrapper.smoothScrollBy(scrollWidth, 0);
    416     }
    417 
    418 }
    419