blob: ddd101b8b5de11d1d257f27319f32c1267e06e1f [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
155 public void removeAllTabs() {
156 mTabsView.removeAllViews();
157 }
158
159 @Override
160 public void dispatchDraw(Canvas canvas) {
161 updateArrowVisibility();
162 super.dispatchDraw(canvas);
163 }
164
165 /**
166 * Sets the current tab.
167 * This method is used to bring a tab to the front of the Widget,
168 * and is used to post to the rest of the UI that a different tab
169 * has been brought to the foreground.
170 *
171 * Note, this is separate from the traditional "focus" that is
172 * employed from the view logic.
173 *
174 * For instance, if we have a list in a tabbed view, a user may be
175 * navigating up and down the list, moving the UI focus (orange
176 * highlighting) through the list items. The cursor movement does
177 * not effect the "selected" tab though, because what is being
178 * scrolled through is all on the same tab. The selected tab only
179 * changes when we navigate between tabs (moving from the list view
180 * to the next tabbed view, in this example).
181 *
182 * To move both the focus AND the selected tab at once, please use
183 * {@link #focusCurrentTab}. Normally, the view logic takes care of
184 * adjusting the focus, so unless you're circumventing the UI,
185 * you'll probably just focus your interest here.
186 *
187 * @param index The tab that you want to indicate as the selected
188 * tab (tab brought to the front of the widget)
189 *
190 * @see #focusCurrentTab
191 */
192 public void setCurrentTab(int index) {
193 if (index < 0 || index >= getTabCount()) {
194 return;
195 }
196
Evan Millar76c67fc2009-08-07 09:12:49 -0700197 mTabsView.setSelected(mSelectedTab, false);
Evan Millar7911ff52009-07-21 15:55:18 -0700198 mSelectedTab = index;
Evan Millar76c67fc2009-08-07 09:12:49 -0700199 mTabsView.setSelected(mSelectedTab, true);
Evan Millar7911ff52009-07-21 15:55:18 -0700200 }
201
202 /**
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700203 * Return index of the currently selected tab.
204 */
205 public int getCurrentTab() {
206 return mSelectedTab;
207 }
208
209 /**
Evan Millar7911ff52009-07-21 15:55:18 -0700210 * Sets the current tab and focuses the UI on it.
211 * This method makes sure that the focused tab matches the selected
212 * tab, normally at {@link #setCurrentTab}. Normally this would not
213 * be an issue if we go through the UI, since the UI is responsible
214 * for calling TabWidget.onFocusChanged(), but in the case where we
215 * are selecting the tab programmatically, we'll need to make sure
216 * focus keeps up.
217 *
218 * @param index The tab that you want focused (highlighted in orange)
219 * and selected (tab brought to the front of the widget)
220 *
221 * @see #setCurrentTab
222 */
223 public void focusCurrentTab(int index) {
Jeff Sharkey3f0b7b82009-08-12 11:28:53 -0700224 if (index < 0 || index >= getTabCount()) {
225 return;
226 }
227
Evan Millar7911ff52009-07-21 15:55:18 -0700228 setCurrentTab(index);
Evan Millarf86847f2009-08-04 16:20:57 -0700229 getChildTabViewAt(index).requestFocus();
Evan Millar7911ff52009-07-21 15:55:18 -0700230
Evan Millar7911ff52009-07-21 15:55:18 -0700231 }
232
233 /**
234 * Adds a tab to the list of tabs. The tab's indicator view is specified
235 * by a layout id. InflateException will be thrown if there is a problem
236 * inflating.
237 *
238 * @param layoutResId The layout id to be inflated to make the tab indicator.
239 */
240 public void addTab(int layoutResId) {
241 addTab(mInflater.inflate(layoutResId, mTabsView, false));
242 }
243
244 /**
245 * Adds a tab to the list of tabs. The tab's indicator view must be provided.
246 *
247 * @param child
248 */
249 public void addTab(View child) {
250 if (child == null) {
251 return;
252 }
253
254 if (child.getLayoutParams() == null) {
255 final LayoutParams lp = new LayoutParams(
256 ViewGroup.LayoutParams.WRAP_CONTENT,
257 ViewGroup.LayoutParams.WRAP_CONTENT);
258 lp.setMargins(0, 0, 0, 0);
259 child.setLayoutParams(lp);
260 }
261
262 // Ensure you can navigate to the tab with the keyboard, and you can touch it
263 child.setFocusable(true);
264 child.setClickable(true);
265 child.setOnClickListener(new TabClickListener());
Evan Millarf86847f2009-08-04 16:20:57 -0700266 child.setOnFocusChangeListener(this);
Evan Millar7911ff52009-07-21 15:55:18 -0700267
Evan Millar7911ff52009-07-21 15:55:18 -0700268 mTabsView.addView(child);
269 }
270
271 /**
272 * Provides a way for ViewContactActivity and EditContactActivity to be notified that the
273 * user clicked on a tab indicator.
274 */
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700275 public void setTabSelectionListener(OnTabSelectionChangedListener listener) {
Evan Millar7911ff52009-07-21 15:55:18 -0700276 mSelectionChangedListener = listener;
277 }
278
Evan Millarf86847f2009-08-04 16:20:57 -0700279 public void onGlobalFocusChanged(View oldFocus, View newFocus) {
280 if (isTab(oldFocus) && !isTab(newFocus)) {
281 onLoseFocus();
282 }
283 }
284
285 public void onFocusChange(View v, boolean hasFocus) {
286 if (v == this && hasFocus) {
287 onObtainFocus();
288 return;
289 }
290
291 if (hasFocus) {
292 for (int i = 0; i < getTabCount(); i++) {
293 if (getChildTabViewAt(i) == v) {
294 setCurrentTab(i);
295 mSelectionChangedListener.onTabSelectionChanged(i, false);
296 break;
Evan Millar7911ff52009-07-21 15:55:18 -0700297 }
298 }
299 }
300 }
301
Evan Millarf86847f2009-08-04 16:20:57 -0700302 /**
303 * Called when the {@link ScrollingTabWidget} gets focus. Here the
304 * widget decides which of it's tabs should have focus.
305 */
306 protected void onObtainFocus() {
307 // Setting this flag, allows the children of this View to obtain focus.
308 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
309 // Assign focus to the last selected tab.
310 focusCurrentTab(mSelectedTab);
311 mSelectionChangedListener.onTabSelectionChanged(mSelectedTab, false);
312 }
313
314 /**
315 * Called when the focus has left the {@link ScrollingTabWidget} or its
316 * descendants. At this time we want the children of this view to be marked
317 * as un-focusable, so that next time focus is moved to the widget, the widget
318 * gets control, and can assign focus where it wants.
319 */
320 protected void onLoseFocus() {
321 // Setting this flag will effectively make the tabs unfocusable. This will
322 // be toggled when the widget obtains focus again.
323 setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
324 }
325
326 public boolean isTab(View v) {
327 for (int i = 0; i < getTabCount(); i++) {
328 if (getChildTabViewAt(i) == v) {
329 return true;
330 }
331 }
332 return false;
333 }
334
Evan Millar7911ff52009-07-21 15:55:18 -0700335 private class TabClickListener implements OnClickListener {
336 public void onClick(View v) {
337 for (int i = 0; i < getTabCount(); i++) {
338 if (getChildTabViewAt(i) == v) {
339 setCurrentTab(i);
340 mSelectionChangedListener.onTabSelectionChanged(i, true);
341 break;
342 }
343 }
344 }
345 }
346
Jeff Sharkey14f61ab2009-08-05 21:02:37 -0700347 public interface OnTabSelectionChangedListener {
Evan Millar7911ff52009-07-21 15:55:18 -0700348 /**
349 * Informs the tab widget host which tab was selected. It also indicates
350 * if the tab was clicked/pressed or just focused into.
351 *
352 * @param tabIndex index of the tab that was selected
353 * @param clicked whether the selection changed due to a touch/click
354 * or due to focus entering the tab through navigation. Pass true
355 * if it was due to a press/click and false otherwise.
356 */
357 void onTabSelectionChanged(int tabIndex, boolean clicked);
358 }
359
Evan Millar7911ff52009-07-21 15:55:18 -0700360 public void onClick(View v) {
Evan Millar76c67fc2009-08-07 09:12:49 -0700361 updateLeftMostVisible();
Evan Millar7911ff52009-07-21 15:55:18 -0700362 if (v == mRightArrowView && (mLeftMostVisibleTabIndex + 1 < getTabCount())) {
363 tabScroll(true /* right */);
364 } else if (v == mLeftArrowView && mLeftMostVisibleTabIndex > 0) {
365 tabScroll(false /* left */);
366 }
367 }
368
369 /*
370 * Updates our record of the left most visible tab. We keep track of this explicitly
371 * on arrow clicks, but need to re-calibrate after focus navigation.
372 */
373 protected void updateLeftMostVisible() {
374 int viewableLeftEdge = mTabsScrollWrapper.getScrollX();
375
376 if (mLeftArrowView.getVisibility() == View.VISIBLE) {
377 viewableLeftEdge += mLeftArrowView.getWidth();
378 }
379
380 for (int i = 0; i < getTabCount(); i++) {
381 View tab = getChildTabViewAt(i);
382 int tabLeftEdge = tab.getLeft();
383 if (tabLeftEdge >= viewableLeftEdge) {
384 mLeftMostVisibleTabIndex = i;
385 break;
386 }
387 }
388 }
389
390 /**
391 * Scrolls the tabs by exactly one tab width.
392 *
393 * @param directionRight if true, scroll to the right, if false, scroll to the left.
394 */
395 protected void tabScroll(boolean directionRight) {
396 int scrollWidth = 0;
397 View newLeftMostVisibleTab = null;
398 if (directionRight) {
399 newLeftMostVisibleTab = getChildTabViewAt(++mLeftMostVisibleTabIndex);
400 } else {
401 newLeftMostVisibleTab = getChildTabViewAt(--mLeftMostVisibleTabIndex);
402 }
403
404 scrollWidth = newLeftMostVisibleTab.getLeft() - mTabsScrollWrapper.getScrollX();
405 if (mLeftMostVisibleTabIndex > 0) {
406 scrollWidth -= mLeftArrowView.getWidth();
407 }
408 mTabsScrollWrapper.smoothScrollBy(scrollWidth, 0);
409 }
410
411}