blob: b45abe460285aa1d3a2c00c691f6e6e707b571bd [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 Millar7911ff52009-07-21 15:55:18 -070021import android.util.AttributeSet;
Evan Millar7911ff52009-07-21 15:55:18 -070022import android.view.LayoutInflater;
Evan Millar7911ff52009-07-21 15:55:18 -070023import android.view.View;
24import android.view.ViewGroup;
Evan Millarf86847f2009-08-04 16:20:57 -070025import android.view.ViewTreeObserver;
Evan Millar7911ff52009-07-21 15:55:18 -070026import android.view.View.OnClickListener;
Evan Millarf86847f2009-08-04 16:20:57 -070027import android.view.View.OnFocusChangeListener;
Evan Millar76c67fc2009-08-07 09:12:49 -070028import android.widget.HorizontalScrollView;
29import android.widget.ImageView;
Evan Millar7911ff52009-07-21 15:55:18 -070030import android.widget.RelativeLayout;
Evan Millar7911ff52009-07-21 15:55:18 -070031
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
Evan Millarf86847f2009-08-04 16:20:57 -070036 implements OnClickListener, ViewTreeObserver.OnGlobalFocusChangeListener,
37 OnFocusChangeListener {
Evan Millar7911ff52009-07-21 15:55:18 -070038
39 private static final String TAG = "ScrollingTabWidget";
40
41 private OnTabSelectionChangedListener mSelectionChangedListener;
42 private int mSelectedTab = 0;
Evan Millar76c67fc2009-08-07 09:12:49 -070043 private ImageView mLeftArrowView;
44 private ImageView mRightArrowView;
45 private HorizontalScrollView mTabsScrollWrapper;
46 private TabStripView mTabsView;
Evan Millar7911ff52009-07-21 15:55:18 -070047 private LayoutInflater mInflater;
Evan Millar7911ff52009-07-21 15:55:18 -070048
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
Evan Millarf86847f2009-08-04 16:20:57 -070066 setFocusable(true);
67 setOnFocusChangeListener(this);
Evan Millar76c67fc2009-08-07 09:12:49 -070068 if (!hasFocus()) {
69 setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
70 }
Evan Millarf86847f2009-08-04 16:20:57 -070071
Evan Millar76c67fc2009-08-07 09:12:49 -070072 mLeftArrowView = (ImageView) mInflater.inflate(R.layout.tab_left_arrow, this, false);
Evan Millar7911ff52009-07-21 15:55:18 -070073 mLeftArrowView.setOnClickListener(this);
Evan Millar76c67fc2009-08-07 09:12:49 -070074 mRightArrowView = (ImageView) mInflater.inflate(R.layout.tab_right_arrow, this, false);
Evan Millar7911ff52009-07-21 15:55:18 -070075 mRightArrowView.setOnClickListener(this);
Evan Millar76c67fc2009-08-07 09:12:49 -070076 mTabsScrollWrapper = (HorizontalScrollView) mInflater.inflate(
Evan Millar7911ff52009-07-21 15:55:18 -070077 R.layout.tab_layout, this, false);
Evan Millar76c67fc2009-08-07 09:12:49 -070078 mTabsView = (TabStripView) mTabsScrollWrapper.findViewById(android.R.id.tabs);
Tadashi G. Takaokab4a72982009-09-29 13:08:28 -070079 View accountNameView = mInflater.inflate(R.layout.tab_account_name, this, false);
Evan Millar7911ff52009-07-21 15:55:18 -070080
81 mLeftArrowView.setVisibility(View.INVISIBLE);
82 mRightArrowView.setVisibility(View.INVISIBLE);
83
84 addView(mTabsScrollWrapper);
85 addView(mLeftArrowView);
86 addView(mRightArrowView);
Tadashi G. Takaokab4a72982009-09-29 13:08:28 -070087 addView(accountNameView);
Evan Millar7911ff52009-07-21 15:55:18 -070088 }
89
Evan Millarf86847f2009-08-04 16:20:57 -070090 @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
Evan Millar7911ff52009-07-21 15:55:18 -0700108 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) {
Evan Millar76c67fc2009-08-07 09:12:49 -0700140 return mTabsView.getChildAt(index);
Evan Millar7911ff52009-07-21 15:55:18 -0700141 }
142
143 /**
144 * Returns the number of tab indicator views.
145 *
146 * @return the number of tab indicator views.
147 */
148 public int getTabCount() {
Evan Millar76c67fc2009-08-07 09:12:49 -0700149 return mTabsView.getChildCount();
Evan Millar7911ff52009-07-21 15:55:18 -0700150 }
151
Evan Millar56d2caa2009-08-20 20:30:12 -0700152 /**
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
Evan Millar7911ff52009-07-21 15:55:18 -0700160 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
Dmitri Plotnikov99eafe72009-09-03 14:09:45 -0700202 if (mSelectedTab < getTabCount()) {
203 mTabsView.setSelected(mSelectedTab, false);
204 }
Evan Millar7911ff52009-07-21 15:55:18 -0700205 mSelectedTab = index;
Evan Millar76c67fc2009-08-07 09:12:49 -0700206 mTabsView.setSelected(mSelectedTab, true);
Evan Millar7911ff52009-07-21 15:55:18 -0700207 }
208
209 /**
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700210 * Return index of the currently selected tab.
211 */
212 public int getCurrentTab() {
213 return mSelectedTab;
214 }
215
216 /**
Evan Millar7911ff52009-07-21 15:55:18 -0700217 * 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) {
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700231 if (index < 0 || index >= getTabCount()) {
232 return;
233 }
234
Evan Millar7911ff52009-07-21 15:55:18 -0700235 setCurrentTab(index);
Evan Millarf86847f2009-08-04 16:20:57 -0700236 getChildTabViewAt(index).requestFocus();
Evan Millar7911ff52009-07-21 15:55:18 -0700237
Evan Millar7911ff52009-07-21 15:55:18 -0700238 }
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());
Evan Millarf86847f2009-08-04 16:20:57 -0700273 child.setOnFocusChangeListener(this);
Evan Millar7911ff52009-07-21 15:55:18 -0700274
Evan Millar7911ff52009-07-21 15:55:18 -0700275 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 */
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700282 public void setTabSelectionListener(OnTabSelectionChangedListener listener) {
Evan Millar7911ff52009-07-21 15:55:18 -0700283 mSelectionChangedListener = listener;
284 }
285
Evan Millarf86847f2009-08-04 16:20:57 -0700286 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;
Evan Millar7911ff52009-07-21 15:55:18 -0700304 }
305 }
306 }
307 }
308
Evan Millarf86847f2009-08-04 16:20:57 -0700309 /**
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
Evan Millar7911ff52009-07-21 15:55:18 -0700342 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
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700354 public interface OnTabSelectionChangedListener {
Evan Millar7911ff52009-07-21 15:55:18 -0700355 /**
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
Evan Millar7911ff52009-07-21 15:55:18 -0700367 public void onClick(View v) {
Evan Millar76c67fc2009-08-07 09:12:49 -0700368 updateLeftMostVisible();
Evan Millar7911ff52009-07-21 15:55:18 -0700369 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}