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