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