1/* 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.util.AttributeSet; 22import android.view.LayoutInflater; 23import android.view.View; 24import android.view.ViewGroup; 25import android.view.ViewTreeObserver; 26import android.view.View.OnClickListener; 27import android.view.View.OnFocusChangeListener; 28import android.widget.HorizontalScrollView; 29import android.widget.ImageView; 30import android.widget.RelativeLayout; 31 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 36 implements OnClickListener, ViewTreeObserver.OnGlobalFocusChangeListener, 37 OnFocusChangeListener { 38 39 private static final String TAG = "ScrollingTabWidget"; 40 41 private OnTabSelectionChangedListener mSelectionChangedListener; 42 private int mSelectedTab = 0; 43 private ImageView mLeftArrowView; 44 private ImageView mRightArrowView; 45 private HorizontalScrollView mTabsScrollWrapper; 46 private TabStripView mTabsView; 47 private LayoutInflater mInflater; 48 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 66 setFocusable(true); 67 setOnFocusChangeListener(this); 68 if (!hasFocus()) { 69 setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS); 70 } 71 72 mLeftArrowView = (ImageView) mInflater.inflate(R.layout.tab_left_arrow, this, false); 73 mLeftArrowView.setOnClickListener(this); 74 mRightArrowView = (ImageView) mInflater.inflate(R.layout.tab_right_arrow, this, false); 75 mRightArrowView.setOnClickListener(this); 76 mTabsScrollWrapper = (HorizontalScrollView) mInflater.inflate( 77 R.layout.tab_layout, this, false); 78 mTabsView = (TabStripView) mTabsScrollWrapper.findViewById(android.R.id.tabs); 79 View accountNameView = mInflater.inflate(R.layout.tab_account_name, this, false); 80 81 mLeftArrowView.setVisibility(View.INVISIBLE); 82 mRightArrowView.setVisibility(View.INVISIBLE); 83 84 addView(mTabsScrollWrapper); 85 addView(mLeftArrowView); 86 addView(mRightArrowView); 87 addView(accountNameView); 88 } 89 90 @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 108 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) { 140 return mTabsView.getChildAt(index); 141 } 142 143 /** 144 * Returns the number of tab indicator views. 145 * 146 * @return the number of tab indicator views. 147 */ 148 public int getTabCount() { 149 return mTabsView.getChildCount(); 150 } 151 152 /** 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 160 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 202 if (mSelectedTab < getTabCount()) { 203 mTabsView.setSelected(mSelectedTab, false); 204 } 205 mSelectedTab = index; 206 mTabsView.setSelected(mSelectedTab, true); 207 } 208 209 /** 210 * Return index of the currently selected tab. 211 */ 212 public int getCurrentTab() { 213 return mSelectedTab; 214 } 215 216 /** 217 * 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) { 231 if (index < 0 || index >= getTabCount()) { 232 return; 233 } 234 235 setCurrentTab(index); 236 getChildTabViewAt(index).requestFocus(); 237 238 } 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()); 273 child.setOnFocusChangeListener(this); 274 275 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 */ 282 public void setTabSelectionListener(OnTabSelectionChangedListener listener) { 283 mSelectionChangedListener = listener; 284 } 285 286 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; 304 } 305 } 306 } 307 } 308 309 /** 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 342 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 354 public interface OnTabSelectionChangedListener { 355 /** 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 367 public void onClick(View v) { 368 updateLeftMostVisible(); 369 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} 419