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
17package com.android.contacts;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.util.AttributeSet;
22import android.view.LayoutInflater;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.ViewTreeObserver;
26import android.view.View.OnClickListener;
27import android.view.View.OnFocusChangeListener;
28import android.widget.HorizontalScrollView;
29import android.widget.ImageView;
30import 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 */
35public 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