blob: 6974a6ecc462e767e47d1e32926b86d1c863f8ba [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
Dmitri Plotnikov99eafe72009-09-03 14:09:45 -0700206 if (mSelectedTab < getTabCount()) {
207 mTabsView.setSelected(mSelectedTab, false);
208 }
Evan Millar7911ff52009-07-21 15:55:18 -0700209 mSelectedTab = index;
Evan Millar76c67fc2009-08-07 09:12:49 -0700210 mTabsView.setSelected(mSelectedTab, true);
Evan Millar7911ff52009-07-21 15:55:18 -0700211 }
212
213 /**
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700214 * Return index of the currently selected tab.
215 */
216 public int getCurrentTab() {
217 return mSelectedTab;
218 }
219
220 /**
Evan Millar7911ff52009-07-21 15:55:18 -0700221 * Sets the current tab and focuses the UI on it.
222 * This method makes sure that the focused tab matches the selected
223 * tab, normally at {@link #setCurrentTab}. Normally this would not
224 * be an issue if we go through the UI, since the UI is responsible
225 * for calling TabWidget.onFocusChanged(), but in the case where we
226 * are selecting the tab programmatically, we'll need to make sure
227 * focus keeps up.
228 *
229 * @param index The tab that you want focused (highlighted in orange)
230 * and selected (tab brought to the front of the widget)
231 *
232 * @see #setCurrentTab
233 */
234 public void focusCurrentTab(int index) {
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700235 if (index < 0 || index >= getTabCount()) {
236 return;
237 }
238
Evan Millar7911ff52009-07-21 15:55:18 -0700239 setCurrentTab(index);
Evan Millarf86847f2009-08-04 16:20:57 -0700240 getChildTabViewAt(index).requestFocus();
Evan Millar7911ff52009-07-21 15:55:18 -0700241
Evan Millar7911ff52009-07-21 15:55:18 -0700242 }
243
244 /**
245 * Adds a tab to the list of tabs. The tab's indicator view is specified
246 * by a layout id. InflateException will be thrown if there is a problem
247 * inflating.
248 *
249 * @param layoutResId The layout id to be inflated to make the tab indicator.
250 */
251 public void addTab(int layoutResId) {
252 addTab(mInflater.inflate(layoutResId, mTabsView, false));
253 }
254
255 /**
256 * Adds a tab to the list of tabs. The tab's indicator view must be provided.
257 *
258 * @param child
259 */
260 public void addTab(View child) {
261 if (child == null) {
262 return;
263 }
264
265 if (child.getLayoutParams() == null) {
266 final LayoutParams lp = new LayoutParams(
267 ViewGroup.LayoutParams.WRAP_CONTENT,
268 ViewGroup.LayoutParams.WRAP_CONTENT);
269 lp.setMargins(0, 0, 0, 0);
270 child.setLayoutParams(lp);
271 }
272
273 // Ensure you can navigate to the tab with the keyboard, and you can touch it
274 child.setFocusable(true);
275 child.setClickable(true);
276 child.setOnClickListener(new TabClickListener());
Evan Millarf86847f2009-08-04 16:20:57 -0700277 child.setOnFocusChangeListener(this);
Evan Millar7911ff52009-07-21 15:55:18 -0700278
Evan Millar7911ff52009-07-21 15:55:18 -0700279 mTabsView.addView(child);
280 }
281
282 /**
283 * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
284 * user clicked on a tab indicator.
285 */
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700286 public void setTabSelectionListener(OnTabSelectionChangedListener listener) {
Evan Millar7911ff52009-07-21 15:55:18 -0700287 mSelectionChangedListener = listener;
288 }
289
Evan Millarf86847f2009-08-04 16:20:57 -0700290 public void onGlobalFocusChanged(View oldFocus, View newFocus) {
291 if (isTab(oldFocus) && !isTab(newFocus)) {
292 onLoseFocus();
293 }
294 }
295
296 public void onFocusChange(View v, boolean hasFocus) {
297 if (v == this && hasFocus) {
298 onObtainFocus();
299 return;
300 }
301
302 if (hasFocus) {
303 for (int i = 0; i < getTabCount(); i++) {
304 if (getChildTabViewAt(i) == v) {
305 setCurrentTab(i);
306 mSelectionChangedListener.onTabSelectionChanged(i, false);
307 break;
Evan Millar7911ff52009-07-21 15:55:18 -0700308 }
309 }
310 }
311 }
312
Evan Millarf86847f2009-08-04 16:20:57 -0700313 /**
314 * Called when the {@link ScrollingTabWidget} gets focus. Here the
315 * widget decides which of it's tabs should have focus.
316 */
317 protected void onObtainFocus() {
318 // Setting this flag, allows the children of this View to obtain focus.
319 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
320 // Assign focus to the last selected tab.
321 focusCurrentTab(mSelectedTab);
322 mSelectionChangedListener.onTabSelectionChanged(mSelectedTab, false);
323 }
324
325 /**
326 * Called when the focus has left the {@link ScrollingTabWidget} or its
327 * descendants. At this time we want the children of this view to be marked
328 * as un-focusable, so that next time focus is moved to the widget, the widget
329 * gets control, and can assign focus where it wants.
330 */
331 protected void onLoseFocus() {
332 // Setting this flag will effectively make the tabs unfocusable. This will
333 // be toggled when the widget obtains focus again.
334 setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
335 }
336
337 public boolean isTab(View v) {
338 for (int i = 0; i < getTabCount(); i++) {
339 if (getChildTabViewAt(i) == v) {
340 return true;
341 }
342 }
343 return false;
344 }
345
Evan Millar7911ff52009-07-21 15:55:18 -0700346 private class TabClickListener implements OnClickListener {
347 public void onClick(View v) {
348 for (int i = 0; i < getTabCount(); i++) {
349 if (getChildTabViewAt(i) == v) {
350 setCurrentTab(i);
351 mSelectionChangedListener.onTabSelectionChanged(i, true);
352 break;
353 }
354 }
355 }
356 }
357
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700358 public interface OnTabSelectionChangedListener {
Evan Millar7911ff52009-07-21 15:55:18 -0700359 /**
360 * Informs the tab widget host which tab was selected. It also indicates
361 * if the tab was clicked/pressed or just focused into.
362 *
363 * @param tabIndex index of the tab that was selected
364 * @param clicked whether the selection changed due to a touch/click
365 * or due to focus entering the tab through navigation. Pass true
366 * if it was due to a press/click and false otherwise.
367 */
368 void onTabSelectionChanged(int tabIndex, boolean clicked);
369 }
370
Evan Millar7911ff52009-07-21 15:55:18 -0700371 public void onClick(View v) {
Evan Millar76c67fc2009-08-07 09:12:49 -0700372 updateLeftMostVisible();
Evan Millar7911ff52009-07-21 15:55:18 -0700373 if (v == mRightArrowView && (mLeftMostVisibleTabIndex + 1 < getTabCount())) {
374 tabScroll(true /* right */);
375 } else if (v == mLeftArrowView && mLeftMostVisibleTabIndex > 0) {
376 tabScroll(false /* left */);
377 }
378 }
379
380 /*
381 * Updates our record of the left most visible tab. We keep track of this explicitly
382 * on arrow clicks, but need to re-calibrate after focus navigation.
383 */
384 protected void updateLeftMostVisible() {
385 int viewableLeftEdge = mTabsScrollWrapper.getScrollX();
386
387 if (mLeftArrowView.getVisibility() == View.VISIBLE) {
388 viewableLeftEdge += mLeftArrowView.getWidth();
389 }
390
391 for (int i = 0; i < getTabCount(); i++) {
392 View tab = getChildTabViewAt(i);
393 int tabLeftEdge = tab.getLeft();
394 if (tabLeftEdge >= viewableLeftEdge) {
395 mLeftMostVisibleTabIndex = i;
396 break;
397 }
398 }
399 }
400
401 /**
402 * Scrolls the tabs by exactly one tab width.
403 *
404 * @param directionRight if true, scroll to the right, if false, scroll to the left.
405 */
406 protected void tabScroll(boolean directionRight) {
407 int scrollWidth = 0;
408 View newLeftMostVisibleTab = null;
409 if (directionRight) {
410 newLeftMostVisibleTab = getChildTabViewAt(++mLeftMostVisibleTabIndex);
411 } else {
412 newLeftMostVisibleTab = getChildTabViewAt(--mLeftMostVisibleTabIndex);
413 }
414
415 scrollWidth = newLeftMostVisibleTab.getLeft() - mTabsScrollWrapper.getScrollX();
416 if (mLeftMostVisibleTabIndex > 0) {
417 scrollWidth -= mLeftArrowView.getWidth();
418 }
419 mTabsScrollWrapper.smoothScrollBy(scrollWidth, 0);
420 }
421
422}