blob: 466071abf1a9526762d45b2c7a31ee7f0e5765bd [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;
23import android.util.AttributeSet;
Evan Millar76c67fc2009-08-07 09:12:49 -070024import android.util.Log;
Evan Millar7911ff52009-07-21 15:55:18 -070025import android.view.KeyEvent;
26import android.view.LayoutInflater;
Evan Millar7911ff52009-07-21 15:55:18 -070027import android.view.View;
28import android.view.ViewGroup;
Evan Millarf86847f2009-08-04 16:20:57 -070029import android.view.ViewTreeObserver;
Evan Millar7911ff52009-07-21 15:55:18 -070030import android.view.View.OnClickListener;
Evan Millarf86847f2009-08-04 16:20:57 -070031import android.view.View.OnFocusChangeListener;
Evan Millar76c67fc2009-08-07 09:12:49 -070032import android.widget.HorizontalScrollView;
33import android.widget.ImageView;
Evan Millar7911ff52009-07-21 15:55:18 -070034import android.widget.LinearLayout;
35import android.widget.RelativeLayout;
Evan Millar7911ff52009-07-21 15:55:18 -070036
37/*
38 * Tab widget that can contain more tabs than can fit on screen at once and scroll over them.
39 */
40public class ScrollingTabWidget extends RelativeLayout
Evan Millarf86847f2009-08-04 16:20:57 -070041 implements OnClickListener, ViewTreeObserver.OnGlobalFocusChangeListener,
42 OnFocusChangeListener {
Evan Millar7911ff52009-07-21 15:55:18 -070043
44 private static final String TAG = "ScrollingTabWidget";
45
46 private OnTabSelectionChangedListener mSelectionChangedListener;
47 private int mSelectedTab = 0;
Evan Millar76c67fc2009-08-07 09:12:49 -070048 private ImageView mLeftArrowView;
49 private ImageView mRightArrowView;
50 private HorizontalScrollView mTabsScrollWrapper;
51 private TabStripView mTabsView;
Evan Millar7911ff52009-07-21 15:55:18 -070052 private LayoutInflater mInflater;
Evan Millar7911ff52009-07-21 15:55:18 -070053
54 // Keeps track of the left most visible tab.
55 private int mLeftMostVisibleTabIndex = 0;
56
57 public ScrollingTabWidget(Context context) {
58 this(context, null);
59 }
60
61 public ScrollingTabWidget(Context context, AttributeSet attrs) {
62 this(context, attrs, 0);
63 }
64
65 public ScrollingTabWidget(Context context, AttributeSet attrs, int defStyle) {
66 super(context, attrs);
67
68 mInflater = (LayoutInflater) mContext.getSystemService(
69 Context.LAYOUT_INFLATER_SERVICE);
70
Evan Millarf86847f2009-08-04 16:20:57 -070071 setFocusable(true);
72 setOnFocusChangeListener(this);
Evan Millar76c67fc2009-08-07 09:12:49 -070073 if (!hasFocus()) {
74 setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
75 }
Evan Millarf86847f2009-08-04 16:20:57 -070076
Evan Millar76c67fc2009-08-07 09:12:49 -070077 mLeftArrowView = (ImageView) mInflater.inflate(R.layout.tab_left_arrow, this, false);
Evan Millar7911ff52009-07-21 15:55:18 -070078 mLeftArrowView.setOnClickListener(this);
Evan Millar76c67fc2009-08-07 09:12:49 -070079 mRightArrowView = (ImageView) mInflater.inflate(R.layout.tab_right_arrow, this, false);
Evan Millar7911ff52009-07-21 15:55:18 -070080 mRightArrowView.setOnClickListener(this);
Evan Millar76c67fc2009-08-07 09:12:49 -070081 mTabsScrollWrapper = (HorizontalScrollView) mInflater.inflate(
Evan Millar7911ff52009-07-21 15:55:18 -070082 R.layout.tab_layout, this, false);
Evan Millar76c67fc2009-08-07 09:12:49 -070083 mTabsView = (TabStripView) mTabsScrollWrapper.findViewById(android.R.id.tabs);
Evan Millar7911ff52009-07-21 15:55:18 -070084
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) {
Evan Millar76c67fc2009-08-07 09:12:49 -0700143 return mTabsView.getChildAt(index);
Evan Millar7911ff52009-07-21 15:55:18 -0700144 }
145
146 /**
147 * Returns the number of tab indicator views.
148 *
149 * @return the number of tab indicator views.
150 */
151 public int getTabCount() {
Evan Millar76c67fc2009-08-07 09:12:49 -0700152 return mTabsView.getChildCount();
Evan Millar7911ff52009-07-21 15:55:18 -0700153 }
154
Evan Millar56d2caa2009-08-20 20:30:12 -0700155 /**
156 * Returns the {@link ViewGroup} that actually contains the tabs. This is where the tab
157 * views should be attached to when being inflated.
158 */
159 public ViewGroup getTabParent() {
160 return mTabsView;
161 }
162
Evan Millar7911ff52009-07-21 15:55:18 -0700163 public void removeAllTabs() {
164 mTabsView.removeAllViews();
165 }
166
167 @Override
168 public void dispatchDraw(Canvas canvas) {
169 updateArrowVisibility();
170 super.dispatchDraw(canvas);
171 }
172
173 /**
174 * Sets the current tab.
175 * This method is used to bring a tab to the front of the Widget,
176 * and is used to post to the rest of the UI that a different tab
177 * has been brought to the foreground.
178 *
179 * Note, this is separate from the traditional "focus" that is
180 * employed from the view logic.
181 *
182 * For instance, if we have a list in a tabbed view, a user may be
183 * navigating up and down the list, moving the UI focus (orange
184 * highlighting) through the list items. The cursor movement does
185 * not effect the "selected" tab though, because what is being
186 * scrolled through is all on the same tab. The selected tab only
187 * changes when we navigate between tabs (moving from the list view
188 * to the next tabbed view, in this example).
189 *
190 * To move both the focus AND the selected tab at once, please use
191 * {@link #focusCurrentTab}. Normally, the view logic takes care of
192 * adjusting the focus, so unless you're circumventing the UI,
193 * you'll probably just focus your interest here.
194 *
195 * @param index The tab that you want to indicate as the selected
196 * tab (tab brought to the front of the widget)
197 *
198 * @see #focusCurrentTab
199 */
200 public void setCurrentTab(int index) {
201 if (index < 0 || index >= getTabCount()) {
202 return;
203 }
204
Evan Millar76c67fc2009-08-07 09:12:49 -0700205 mTabsView.setSelected(mSelectedTab, false);
Evan Millar7911ff52009-07-21 15:55:18 -0700206 mSelectedTab = index;
Evan Millar76c67fc2009-08-07 09:12:49 -0700207 mTabsView.setSelected(mSelectedTab, true);
Evan Millar7911ff52009-07-21 15:55:18 -0700208 }
209
210 /**
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700211 * Return index of the currently selected tab.
212 */
213 public int getCurrentTab() {
214 return mSelectedTab;
215 }
216
217 /**
Evan Millar7911ff52009-07-21 15:55:18 -0700218 * Sets the current tab and focuses the UI on it.
219 * This method makes sure that the focused tab matches the selected
220 * tab, normally at {@link #setCurrentTab}. Normally this would not
221 * be an issue if we go through the UI, since the UI is responsible
222 * for calling TabWidget.onFocusChanged(), but in the case where we
223 * are selecting the tab programmatically, we'll need to make sure
224 * focus keeps up.
225 *
226 * @param index The tab that you want focused (highlighted in orange)
227 * and selected (tab brought to the front of the widget)
228 *
229 * @see #setCurrentTab
230 */
231 public void focusCurrentTab(int index) {
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700232 if (index < 0 || index >= getTabCount()) {
233 return;
234 }
235
Evan Millar7911ff52009-07-21 15:55:18 -0700236 setCurrentTab(index);
Evan Millarf86847f2009-08-04 16:20:57 -0700237 getChildTabViewAt(index).requestFocus();
Evan Millar7911ff52009-07-21 15:55:18 -0700238
Evan Millar7911ff52009-07-21 15:55:18 -0700239 }
240
241 /**
242 * Adds a tab to the list of tabs. The tab's indicator view is specified
243 * by a layout id. InflateException will be thrown if there is a problem
244 * inflating.
245 *
246 * @param layoutResId The layout id to be inflated to make the tab indicator.
247 */
248 public void addTab(int layoutResId) {
249 addTab(mInflater.inflate(layoutResId, mTabsView, false));
250 }
251
252 /**
253 * Adds a tab to the list of tabs. The tab's indicator view must be provided.
254 *
255 * @param child
256 */
257 public void addTab(View child) {
258 if (child == null) {
259 return;
260 }
261
262 if (child.getLayoutParams() == null) {
263 final LayoutParams lp = new LayoutParams(
264 ViewGroup.LayoutParams.WRAP_CONTENT,
265 ViewGroup.LayoutParams.WRAP_CONTENT);
266 lp.setMargins(0, 0, 0, 0);
267 child.setLayoutParams(lp);
268 }
269
270 // Ensure you can navigate to the tab with the keyboard, and you can touch it
271 child.setFocusable(true);
272 child.setClickable(true);
273 child.setOnClickListener(new TabClickListener());
Evan Millarf86847f2009-08-04 16:20:57 -0700274 child.setOnFocusChangeListener(this);
Evan Millar7911ff52009-07-21 15:55:18 -0700275
Evan Millar7911ff52009-07-21 15:55:18 -0700276 mTabsView.addView(child);
277 }
278
279 /**
280 * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
281 * user clicked on a tab indicator.
282 */
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700283 public void setTabSelectionListener(OnTabSelectionChangedListener listener) {
Evan Millar7911ff52009-07-21 15:55:18 -0700284 mSelectionChangedListener = listener;
285 }
286
Evan Millarf86847f2009-08-04 16:20:57 -0700287 public void onGlobalFocusChanged(View oldFocus, View newFocus) {
288 if (isTab(oldFocus) && !isTab(newFocus)) {
289 onLoseFocus();
290 }
291 }
292
293 public void onFocusChange(View v, boolean hasFocus) {
294 if (v == this && hasFocus) {
295 onObtainFocus();
296 return;
297 }
298
299 if (hasFocus) {
300 for (int i = 0; i < getTabCount(); i++) {
301 if (getChildTabViewAt(i) == v) {
302 setCurrentTab(i);
303 mSelectionChangedListener.onTabSelectionChanged(i, false);
304 break;
Evan Millar7911ff52009-07-21 15:55:18 -0700305 }
306 }
307 }
308 }
309
Evan Millarf86847f2009-08-04 16:20:57 -0700310 /**
311 * Called when the {@link ScrollingTabWidget} gets focus. Here the
312 * widget decides which of it's tabs should have focus.
313 */
314 protected void onObtainFocus() {
315 // Setting this flag, allows the children of this View to obtain focus.
316 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
317 // Assign focus to the last selected tab.
318 focusCurrentTab(mSelectedTab);
319 mSelectionChangedListener.onTabSelectionChanged(mSelectedTab, false);
320 }
321
322 /**
323 * Called when the focus has left the {@link ScrollingTabWidget} or its
324 * descendants. At this time we want the children of this view to be marked
325 * as un-focusable, so that next time focus is moved to the widget, the widget
326 * gets control, and can assign focus where it wants.
327 */
328 protected void onLoseFocus() {
329 // Setting this flag will effectively make the tabs unfocusable. This will
330 // be toggled when the widget obtains focus again.
331 setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
332 }
333
334 public boolean isTab(View v) {
335 for (int i = 0; i < getTabCount(); i++) {
336 if (getChildTabViewAt(i) == v) {
337 return true;
338 }
339 }
340 return false;
341 }
342
Evan Millar7911ff52009-07-21 15:55:18 -0700343 private class TabClickListener implements OnClickListener {
344 public void onClick(View v) {
345 for (int i = 0; i < getTabCount(); i++) {
346 if (getChildTabViewAt(i) == v) {
347 setCurrentTab(i);
348 mSelectionChangedListener.onTabSelectionChanged(i, true);
349 break;
350 }
351 }
352 }
353 }
354
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700355 public interface OnTabSelectionChangedListener {
Evan Millar7911ff52009-07-21 15:55:18 -0700356 /**
357 * Informs the tab widget host which tab was selected. It also indicates
358 * if the tab was clicked/pressed or just focused into.
359 *
360 * @param tabIndex index of the tab that was selected
361 * @param clicked whether the selection changed due to a touch/click
362 * or due to focus entering the tab through navigation. Pass true
363 * if it was due to a press/click and false otherwise.
364 */
365 void onTabSelectionChanged(int tabIndex, boolean clicked);
366 }
367
Evan Millar7911ff52009-07-21 15:55:18 -0700368 public void onClick(View v) {
Evan Millar76c67fc2009-08-07 09:12:49 -0700369 updateLeftMostVisible();
Evan Millar7911ff52009-07-21 15:55:18 -0700370 if (v == mRightArrowView && (mLeftMostVisibleTabIndex + 1 < getTabCount())) {
371 tabScroll(true /* right */);
372 } else if (v == mLeftArrowView && mLeftMostVisibleTabIndex > 0) {
373 tabScroll(false /* left */);
374 }
375 }
376
377 /*
378 * Updates our record of the left most visible tab. We keep track of this explicitly
379 * on arrow clicks, but need to re-calibrate after focus navigation.
380 */
381 protected void updateLeftMostVisible() {
382 int viewableLeftEdge = mTabsScrollWrapper.getScrollX();
383
384 if (mLeftArrowView.getVisibility() == View.VISIBLE) {
385 viewableLeftEdge += mLeftArrowView.getWidth();
386 }
387
388 for (int i = 0; i < getTabCount(); i++) {
389 View tab = getChildTabViewAt(i);
390 int tabLeftEdge = tab.getLeft();
391 if (tabLeftEdge >= viewableLeftEdge) {
392 mLeftMostVisibleTabIndex = i;
393 break;
394 }
395 }
396 }
397
398 /**
399 * Scrolls the tabs by exactly one tab width.
400 *
401 * @param directionRight if true, scroll to the right, if false, scroll to the left.
402 */
403 protected void tabScroll(boolean directionRight) {
404 int scrollWidth = 0;
405 View newLeftMostVisibleTab = null;
406 if (directionRight) {
407 newLeftMostVisibleTab = getChildTabViewAt(++mLeftMostVisibleTabIndex);
408 } else {
409 newLeftMostVisibleTab = getChildTabViewAt(--mLeftMostVisibleTabIndex);
410 }
411
412 scrollWidth = newLeftMostVisibleTab.getLeft() - mTabsScrollWrapper.getScrollX();
413 if (mLeftMostVisibleTabIndex > 0) {
414 scrollWidth -= mLeftArrowView.getWidth();
415 }
416 mTabsScrollWrapper.smoothScrollBy(scrollWidth, 0);
417 }
418
419}