blob: 9f4aee873b67aeca26211068779b270e715f1a27 [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 /**
204 * Sets the current tab and focuses the UI on it.
205 * This method makes sure that the focused tab matches the selected
206 * tab, normally at {@link #setCurrentTab}. Normally this would not
207 * be an issue if we go through the UI, since the UI is responsible
208 * for calling TabWidget.onFocusChanged(), but in the case where we
209 * are selecting the tab programmatically, we'll need to make sure
210 * focus keeps up.
211 *
212 * @param index The tab that you want focused (highlighted in orange)
213 * and selected (tab brought to the front of the widget)
214 *
215 * @see #setCurrentTab
216 */
217 public void focusCurrentTab(int index) {
Evan Millar7911ff52009-07-21 15:55:18 -0700218 setCurrentTab(index);
Evan Millarf86847f2009-08-04 16:20:57 -0700219 getChildTabViewAt(index).requestFocus();
Evan Millar7911ff52009-07-21 15:55:18 -0700220
Evan Millar7911ff52009-07-21 15:55:18 -0700221 }
222
223 /**
224 * Adds a tab to the list of tabs. The tab's indicator view is specified
225 * by a layout id. InflateException will be thrown if there is a problem
226 * inflating.
227 *
228 * @param layoutResId The layout id to be inflated to make the tab indicator.
229 */
230 public void addTab(int layoutResId) {
231 addTab(mInflater.inflate(layoutResId, mTabsView, false));
232 }
233
234 /**
235 * Adds a tab to the list of tabs. The tab's indicator view must be provided.
236 *
237 * @param child
238 */
239 public void addTab(View child) {
240 if (child == null) {
241 return;
242 }
243
244 if (child.getLayoutParams() == null) {
245 final LayoutParams lp = new LayoutParams(
246 ViewGroup.LayoutParams.WRAP_CONTENT,
247 ViewGroup.LayoutParams.WRAP_CONTENT);
248 lp.setMargins(0, 0, 0, 0);
249 child.setLayoutParams(lp);
250 }
251
252 // Ensure you can navigate to the tab with the keyboard, and you can touch it
253 child.setFocusable(true);
254 child.setClickable(true);
255 child.setOnClickListener(new TabClickListener());
Evan Millarf86847f2009-08-04 16:20:57 -0700256 child.setOnFocusChangeListener(this);
Evan Millar7911ff52009-07-21 15:55:18 -0700257
258 // If we already have at least one tab, then add a divider before adding the next tab.
259 if (getTabCount() > 0) {
260 View divider = new View(mContext);
261 final LayoutParams lp = new LayoutParams(
262 mDividerDrawable.getIntrinsicWidth(),
263 ViewGroup.LayoutParams.FILL_PARENT);
264 lp.setMargins(0, 0, 0, 0);
265 divider.setLayoutParams(lp);
266 divider.setBackgroundDrawable(mDividerDrawable);
267 mTabsView.addView(divider);
268 }
269 mTabsView.addView(child);
270 }
271
272 /**
273 * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
274 * user clicked on a tab indicator.
275 */
276 void setTabSelectionListener(OnTabSelectionChangedListener listener) {
277 mSelectionChangedListener = listener;
278 }
279
Evan Millarf86847f2009-08-04 16:20:57 -0700280 public void onGlobalFocusChanged(View oldFocus, View newFocus) {
281 if (isTab(oldFocus) && !isTab(newFocus)) {
282 onLoseFocus();
283 }
284 }
285
286 public void onFocusChange(View v, boolean hasFocus) {
287 if (v == this && hasFocus) {
288 onObtainFocus();
289 return;
290 }
291
292 if (hasFocus) {
293 for (int i = 0; i < getTabCount(); i++) {
294 if (getChildTabViewAt(i) == v) {
295 setCurrentTab(i);
296 mSelectionChangedListener.onTabSelectionChanged(i, false);
297 break;
Evan Millar7911ff52009-07-21 15:55:18 -0700298 }
299 }
300 }
301 }
302
Evan Millarf86847f2009-08-04 16:20:57 -0700303 /**
304 * Called when the {@link ScrollingTabWidget} gets focus. Here the
305 * widget decides which of it's tabs should have focus.
306 */
307 protected void onObtainFocus() {
308 // Setting this flag, allows the children of this View to obtain focus.
309 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
310 // Assign focus to the last selected tab.
311 focusCurrentTab(mSelectedTab);
312 mSelectionChangedListener.onTabSelectionChanged(mSelectedTab, false);
313 }
314
315 /**
316 * Called when the focus has left the {@link ScrollingTabWidget} or its
317 * descendants. At this time we want the children of this view to be marked
318 * as un-focusable, so that next time focus is moved to the widget, the widget
319 * gets control, and can assign focus where it wants.
320 */
321 protected void onLoseFocus() {
322 // Setting this flag will effectively make the tabs unfocusable. This will
323 // be toggled when the widget obtains focus again.
324 setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
325 }
326
327 public boolean isTab(View v) {
328 for (int i = 0; i < getTabCount(); i++) {
329 if (getChildTabViewAt(i) == v) {
330 return true;
331 }
332 }
333 return false;
334 }
335
Evan Millar7911ff52009-07-21 15:55:18 -0700336 private class TabClickListener implements OnClickListener {
337 public void onClick(View v) {
338 for (int i = 0; i < getTabCount(); i++) {
339 if (getChildTabViewAt(i) == v) {
340 setCurrentTab(i);
341 mSelectionChangedListener.onTabSelectionChanged(i, true);
342 break;
343 }
344 }
345 }
346 }
347
348 static interface OnTabSelectionChangedListener {
349 /**
350 * Informs the tab widget host which tab was selected. It also indicates
351 * if the tab was clicked/pressed or just focused into.
352 *
353 * @param tabIndex index of the tab that was selected
354 * @param clicked whether the selection changed due to a touch/click
355 * or due to focus entering the tab through navigation. Pass true
356 * if it was due to a press/click and false otherwise.
357 */
358 void onTabSelectionChanged(int tabIndex, boolean clicked);
359 }
360
361 @Override
362 public boolean dispatchKeyEvent(KeyEvent event) {
363 boolean handled = super.dispatchKeyEvent(event);
364 if (event.getAction() == KeyEvent.ACTION_DOWN) {
365 switch (event.getKeyCode()) {
366 case KeyEvent.KEYCODE_DPAD_LEFT:
367 case KeyEvent.KEYCODE_DPAD_RIGHT:
368 // If tabs move from left/right events we must update mLeftMostVisibleTabIndex.
369 updateLeftMostVisible();
370 break;
371 }
372 }
373
374 return handled;
375 }
376
377 public void onClick(View v) {
378 if (v == mRightArrowView && (mLeftMostVisibleTabIndex + 1 < getTabCount())) {
379 tabScroll(true /* right */);
380 } else if (v == mLeftArrowView && mLeftMostVisibleTabIndex > 0) {
381 tabScroll(false /* left */);
382 }
383 }
384
385 /*
386 * Updates our record of the left most visible tab. We keep track of this explicitly
387 * on arrow clicks, but need to re-calibrate after focus navigation.
388 */
389 protected void updateLeftMostVisible() {
390 int viewableLeftEdge = mTabsScrollWrapper.getScrollX();
391
392 if (mLeftArrowView.getVisibility() == View.VISIBLE) {
393 viewableLeftEdge += mLeftArrowView.getWidth();
394 }
395
396 for (int i = 0; i < getTabCount(); i++) {
397 View tab = getChildTabViewAt(i);
398 int tabLeftEdge = tab.getLeft();
399 if (tabLeftEdge >= viewableLeftEdge) {
400 mLeftMostVisibleTabIndex = i;
401 break;
402 }
403 }
404 }
405
406 /**
407 * Scrolls the tabs by exactly one tab width.
408 *
409 * @param directionRight if true, scroll to the right, if false, scroll to the left.
410 */
411 protected void tabScroll(boolean directionRight) {
412 int scrollWidth = 0;
413 View newLeftMostVisibleTab = null;
414 if (directionRight) {
415 newLeftMostVisibleTab = getChildTabViewAt(++mLeftMostVisibleTabIndex);
416 } else {
417 newLeftMostVisibleTab = getChildTabViewAt(--mLeftMostVisibleTabIndex);
418 }
419
420 scrollWidth = newLeftMostVisibleTab.getLeft() - mTabsScrollWrapper.getScrollX();
421 if (mLeftMostVisibleTabIndex > 0) {
422 scrollWidth -= mLeftArrowView.getWidth();
423 }
424 mTabsScrollWrapper.smoothScrollBy(scrollWidth, 0);
425 }
426
427}