1/* 2 * Copyright (C) 2011 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 */ 16package com.android.internal.widget; 17 18import com.android.internal.view.ActionBarPolicy; 19 20import android.animation.Animator; 21import android.animation.ObjectAnimator; 22import android.animation.TimeInterpolator; 23import android.app.ActionBar; 24import android.content.Context; 25import android.content.res.Configuration; 26import android.graphics.drawable.Drawable; 27import android.text.TextUtils; 28import android.text.TextUtils.TruncateAt; 29import android.view.Gravity; 30import android.view.View; 31import android.view.ViewGroup; 32import android.view.ViewParent; 33import android.view.accessibility.AccessibilityEvent; 34import android.view.accessibility.AccessibilityNodeInfo; 35import android.view.animation.DecelerateInterpolator; 36import android.widget.AdapterView; 37import android.widget.BaseAdapter; 38import android.widget.HorizontalScrollView; 39import android.widget.ImageView; 40import android.widget.LinearLayout; 41import android.widget.ListView; 42import android.widget.Spinner; 43import android.widget.TextView; 44import android.widget.Toast; 45 46/** 47 * This widget implements the dynamic action bar tab behavior that can change 48 * across different configurations or circumstances. 49 */ 50public class ScrollingTabContainerView extends HorizontalScrollView 51 implements AdapterView.OnItemClickListener { 52 private static final String TAG = "ScrollingTabContainerView"; 53 Runnable mTabSelector; 54 private TabClickListener mTabClickListener; 55 56 private LinearLayout mTabLayout; 57 private Spinner mTabSpinner; 58 private boolean mAllowCollapse; 59 60 int mMaxTabWidth; 61 int mStackedTabMaxWidth; 62 private int mContentHeight; 63 private int mSelectedTabIndex; 64 65 protected Animator mVisibilityAnim; 66 protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener(); 67 68 private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator(); 69 70 private static final int FADE_DURATION = 200; 71 72 public ScrollingTabContainerView(Context context) { 73 super(context); 74 setHorizontalScrollBarEnabled(false); 75 76 ActionBarPolicy abp = ActionBarPolicy.get(context); 77 setContentHeight(abp.getTabContainerHeight()); 78 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 79 80 mTabLayout = createTabLayout(); 81 addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 82 ViewGroup.LayoutParams.MATCH_PARENT)); 83 } 84 85 @Override 86 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 87 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 88 final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY; 89 setFillViewport(lockedExpanded); 90 91 final int childCount = mTabLayout.getChildCount(); 92 if (childCount > 1 && 93 (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) { 94 if (childCount > 2) { 95 mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f); 96 } else { 97 mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2; 98 } 99 mMaxTabWidth = Math.min(mMaxTabWidth, mStackedTabMaxWidth); 100 } else { 101 mMaxTabWidth = -1; 102 } 103 104 heightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY); 105 106 final boolean canCollapse = !lockedExpanded && mAllowCollapse; 107 108 if (canCollapse) { 109 // See if we should expand 110 mTabLayout.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec); 111 if (mTabLayout.getMeasuredWidth() > MeasureSpec.getSize(widthMeasureSpec)) { 112 performCollapse(); 113 } else { 114 performExpand(); 115 } 116 } else { 117 performExpand(); 118 } 119 120 final int oldWidth = getMeasuredWidth(); 121 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 122 final int newWidth = getMeasuredWidth(); 123 124 if (lockedExpanded && oldWidth != newWidth) { 125 // Recenter the tab display if we're at a new (scrollable) size. 126 setTabSelected(mSelectedTabIndex); 127 } 128 } 129 130 /** 131 * Indicates whether this view is collapsed into a dropdown menu instead 132 * of traditional tabs. 133 * @return true if showing as a spinner 134 */ 135 private boolean isCollapsed() { 136 return mTabSpinner != null && mTabSpinner.getParent() == this; 137 } 138 139 public void setAllowCollapse(boolean allowCollapse) { 140 mAllowCollapse = allowCollapse; 141 } 142 143 private void performCollapse() { 144 if (isCollapsed()) return; 145 146 if (mTabSpinner == null) { 147 mTabSpinner = createSpinner(); 148 } 149 removeView(mTabLayout); 150 addView(mTabSpinner, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 151 ViewGroup.LayoutParams.MATCH_PARENT)); 152 if (mTabSpinner.getAdapter() == null) { 153 mTabSpinner.setAdapter(new TabAdapter()); 154 } 155 if (mTabSelector != null) { 156 removeCallbacks(mTabSelector); 157 mTabSelector = null; 158 } 159 mTabSpinner.setSelection(mSelectedTabIndex); 160 } 161 162 private boolean performExpand() { 163 if (!isCollapsed()) return false; 164 165 removeView(mTabSpinner); 166 addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 167 ViewGroup.LayoutParams.MATCH_PARENT)); 168 setTabSelected(mTabSpinner.getSelectedItemPosition()); 169 return false; 170 } 171 172 public void setTabSelected(int position) { 173 mSelectedTabIndex = position; 174 final int tabCount = mTabLayout.getChildCount(); 175 for (int i = 0; i < tabCount; i++) { 176 final View child = mTabLayout.getChildAt(i); 177 final boolean isSelected = i == position; 178 child.setSelected(isSelected); 179 if (isSelected) { 180 animateToTab(position); 181 } 182 } 183 if (mTabSpinner != null && position >= 0) { 184 mTabSpinner.setSelection(position); 185 } 186 } 187 188 public void setContentHeight(int contentHeight) { 189 mContentHeight = contentHeight; 190 requestLayout(); 191 } 192 193 private LinearLayout createTabLayout() { 194 final LinearLayout tabLayout = new LinearLayout(getContext(), null, 195 com.android.internal.R.attr.actionBarTabBarStyle); 196 tabLayout.setMeasureWithLargestChildEnabled(true); 197 tabLayout.setGravity(Gravity.CENTER); 198 tabLayout.setLayoutParams(new LinearLayout.LayoutParams( 199 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); 200 return tabLayout; 201 } 202 203 private Spinner createSpinner() { 204 final Spinner spinner = new Spinner(getContext(), null, 205 com.android.internal.R.attr.actionDropDownStyle); 206 spinner.setLayoutParams(new LinearLayout.LayoutParams( 207 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); 208 spinner.setOnItemClickListenerInt(this); 209 return spinner; 210 } 211 212 @Override 213 protected void onConfigurationChanged(Configuration newConfig) { 214 super.onConfigurationChanged(newConfig); 215 216 ActionBarPolicy abp = ActionBarPolicy.get(getContext()); 217 // Action bar can change size on configuration changes. 218 // Reread the desired height from the theme-specified style. 219 setContentHeight(abp.getTabContainerHeight()); 220 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 221 } 222 223 public void animateToVisibility(int visibility) { 224 if (mVisibilityAnim != null) { 225 mVisibilityAnim.cancel(); 226 } 227 if (visibility == VISIBLE) { 228 if (getVisibility() != VISIBLE) { 229 setAlpha(0); 230 } 231 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1); 232 anim.setDuration(FADE_DURATION); 233 anim.setInterpolator(sAlphaInterpolator); 234 235 anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); 236 anim.start(); 237 } else { 238 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0); 239 anim.setDuration(FADE_DURATION); 240 anim.setInterpolator(sAlphaInterpolator); 241 242 anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); 243 anim.start(); 244 } 245 } 246 247 public void animateToTab(final int position) { 248 final View tabView = mTabLayout.getChildAt(position); 249 if (mTabSelector != null) { 250 removeCallbacks(mTabSelector); 251 } 252 mTabSelector = new Runnable() { 253 public void run() { 254 final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2; 255 smoothScrollTo(scrollPos, 0); 256 mTabSelector = null; 257 } 258 }; 259 post(mTabSelector); 260 } 261 262 @Override 263 public void onAttachedToWindow() { 264 super.onAttachedToWindow(); 265 if (mTabSelector != null) { 266 // Re-post the selector we saved 267 post(mTabSelector); 268 } 269 } 270 271 @Override 272 public void onDetachedFromWindow() { 273 super.onDetachedFromWindow(); 274 if (mTabSelector != null) { 275 removeCallbacks(mTabSelector); 276 } 277 } 278 279 private TabView createTabView(ActionBar.Tab tab, boolean forAdapter) { 280 final TabView tabView = new TabView(getContext(), tab, forAdapter); 281 if (forAdapter) { 282 tabView.setBackgroundDrawable(null); 283 tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, 284 mContentHeight)); 285 } else { 286 tabView.setFocusable(true); 287 288 if (mTabClickListener == null) { 289 mTabClickListener = new TabClickListener(); 290 } 291 tabView.setOnClickListener(mTabClickListener); 292 } 293 return tabView; 294 } 295 296 public void addTab(ActionBar.Tab tab, boolean setSelected) { 297 TabView tabView = createTabView(tab, false); 298 mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, 299 LayoutParams.MATCH_PARENT, 1)); 300 if (mTabSpinner != null) { 301 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 302 } 303 if (setSelected) { 304 tabView.setSelected(true); 305 } 306 if (mAllowCollapse) { 307 requestLayout(); 308 } 309 } 310 311 public void addTab(ActionBar.Tab tab, int position, boolean setSelected) { 312 final TabView tabView = createTabView(tab, false); 313 mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams( 314 0, LayoutParams.MATCH_PARENT, 1)); 315 if (mTabSpinner != null) { 316 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 317 } 318 if (setSelected) { 319 tabView.setSelected(true); 320 } 321 if (mAllowCollapse) { 322 requestLayout(); 323 } 324 } 325 326 public void updateTab(int position) { 327 ((TabView) mTabLayout.getChildAt(position)).update(); 328 if (mTabSpinner != null) { 329 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 330 } 331 if (mAllowCollapse) { 332 requestLayout(); 333 } 334 } 335 336 public void removeTabAt(int position) { 337 mTabLayout.removeViewAt(position); 338 if (mTabSpinner != null) { 339 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 340 } 341 if (mAllowCollapse) { 342 requestLayout(); 343 } 344 } 345 346 public void removeAllTabs() { 347 mTabLayout.removeAllViews(); 348 if (mTabSpinner != null) { 349 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 350 } 351 if (mAllowCollapse) { 352 requestLayout(); 353 } 354 } 355 356 @Override 357 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 358 TabView tabView = (TabView) view; 359 tabView.getTab().select(); 360 } 361 362 private class TabView extends LinearLayout implements OnLongClickListener { 363 private ActionBar.Tab mTab; 364 private TextView mTextView; 365 private ImageView mIconView; 366 private View mCustomView; 367 368 public TabView(Context context, ActionBar.Tab tab, boolean forList) { 369 super(context, null, com.android.internal.R.attr.actionBarTabStyle); 370 mTab = tab; 371 372 if (forList) { 373 setGravity(Gravity.START | Gravity.CENTER_VERTICAL); 374 } 375 376 update(); 377 } 378 379 public void bindTab(ActionBar.Tab tab) { 380 mTab = tab; 381 update(); 382 } 383 384 @Override 385 public void setSelected(boolean selected) { 386 final boolean changed = (isSelected() != selected); 387 super.setSelected(selected); 388 if (changed && selected) { 389 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 390 } 391 } 392 393 @Override 394 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 395 super.onInitializeAccessibilityEvent(event); 396 // This view masquerades as an action bar tab. 397 event.setClassName(ActionBar.Tab.class.getName()); 398 } 399 400 @Override 401 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 402 super.onInitializeAccessibilityNodeInfo(info); 403 // This view masquerades as an action bar tab. 404 info.setClassName(ActionBar.Tab.class.getName()); 405 } 406 407 @Override 408 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 409 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 410 411 // Re-measure if we went beyond our maximum size. 412 if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) { 413 super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY), 414 heightMeasureSpec); 415 } 416 } 417 418 public void update() { 419 final ActionBar.Tab tab = mTab; 420 final View custom = tab.getCustomView(); 421 if (custom != null) { 422 final ViewParent customParent = custom.getParent(); 423 if (customParent != this) { 424 if (customParent != null) ((ViewGroup) customParent).removeView(custom); 425 addView(custom); 426 } 427 mCustomView = custom; 428 if (mTextView != null) mTextView.setVisibility(GONE); 429 if (mIconView != null) { 430 mIconView.setVisibility(GONE); 431 mIconView.setImageDrawable(null); 432 } 433 } else { 434 if (mCustomView != null) { 435 removeView(mCustomView); 436 mCustomView = null; 437 } 438 439 final Drawable icon = tab.getIcon(); 440 final CharSequence text = tab.getText(); 441 442 if (icon != null) { 443 if (mIconView == null) { 444 ImageView iconView = new ImageView(getContext()); 445 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 446 LayoutParams.WRAP_CONTENT); 447 lp.gravity = Gravity.CENTER_VERTICAL; 448 iconView.setLayoutParams(lp); 449 addView(iconView, 0); 450 mIconView = iconView; 451 } 452 mIconView.setImageDrawable(icon); 453 mIconView.setVisibility(VISIBLE); 454 } else if (mIconView != null) { 455 mIconView.setVisibility(GONE); 456 mIconView.setImageDrawable(null); 457 } 458 459 final boolean hasText = !TextUtils.isEmpty(text); 460 if (hasText) { 461 if (mTextView == null) { 462 TextView textView = new TextView(getContext(), null, 463 com.android.internal.R.attr.actionBarTabTextStyle); 464 textView.setEllipsize(TruncateAt.END); 465 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 466 LayoutParams.WRAP_CONTENT); 467 lp.gravity = Gravity.CENTER_VERTICAL; 468 textView.setLayoutParams(lp); 469 addView(textView); 470 mTextView = textView; 471 } 472 mTextView.setText(text); 473 mTextView.setVisibility(VISIBLE); 474 } else if (mTextView != null) { 475 mTextView.setVisibility(GONE); 476 mTextView.setText(null); 477 } 478 479 if (mIconView != null) { 480 mIconView.setContentDescription(tab.getContentDescription()); 481 } 482 483 if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) { 484 setOnLongClickListener(this); 485 } else { 486 setOnLongClickListener(null); 487 setLongClickable(false); 488 } 489 } 490 } 491 492 public boolean onLongClick(View v) { 493 final int[] screenPos = new int[2]; 494 getLocationOnScreen(screenPos); 495 496 final Context context = getContext(); 497 final int width = getWidth(); 498 final int height = getHeight(); 499 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 500 501 Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), 502 Toast.LENGTH_SHORT); 503 // Show under the tab 504 cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 505 (screenPos[0] + width / 2) - screenWidth / 2, height); 506 507 cheatSheet.show(); 508 return true; 509 } 510 511 public ActionBar.Tab getTab() { 512 return mTab; 513 } 514 } 515 516 private class TabAdapter extends BaseAdapter { 517 @Override 518 public int getCount() { 519 return mTabLayout.getChildCount(); 520 } 521 522 @Override 523 public Object getItem(int position) { 524 return ((TabView) mTabLayout.getChildAt(position)).getTab(); 525 } 526 527 @Override 528 public long getItemId(int position) { 529 return position; 530 } 531 532 @Override 533 public View getView(int position, View convertView, ViewGroup parent) { 534 if (convertView == null) { 535 convertView = createTabView((ActionBar.Tab) getItem(position), true); 536 } else { 537 ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); 538 } 539 return convertView; 540 } 541 } 542 543 private class TabClickListener implements OnClickListener { 544 public void onClick(View view) { 545 TabView tabView = (TabView) view; 546 tabView.getTab().select(); 547 final int tabCount = mTabLayout.getChildCount(); 548 for (int i = 0; i < tabCount; i++) { 549 final View child = mTabLayout.getChildAt(i); 550 child.setSelected(child == view); 551 } 552 } 553 } 554 555 protected class VisibilityAnimListener implements Animator.AnimatorListener { 556 private boolean mCanceled = false; 557 private int mFinalVisibility; 558 559 public VisibilityAnimListener withFinalVisibility(int visibility) { 560 mFinalVisibility = visibility; 561 return this; 562 } 563 564 @Override 565 public void onAnimationStart(Animator animation) { 566 setVisibility(VISIBLE); 567 mVisibilityAnim = animation; 568 mCanceled = false; 569 } 570 571 @Override 572 public void onAnimationEnd(Animator animation) { 573 if (mCanceled) return; 574 575 mVisibilityAnim = null; 576 setVisibility(mFinalVisibility); 577 } 578 579 @Override 580 public void onAnimationCancel(Animator animation) { 581 mCanceled = true; 582 } 583 584 @Override 585 public void onAnimationRepeat(Animator animation) { 586 } 587 } 588} 589