ScrollingTabContainerView.java revision b9ead4a91599ca63e947f74f83b67a58bda64a82
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 final TabAdapter adapter = new TabAdapter(mContext); 154 adapter.setDropDownViewContext(mTabSpinner.getPopupContext()); 155 mTabSpinner.setAdapter(adapter); 156 } 157 if (mTabSelector != null) { 158 removeCallbacks(mTabSelector); 159 mTabSelector = null; 160 } 161 mTabSpinner.setSelection(mSelectedTabIndex); 162 } 163 164 private boolean performExpand() { 165 if (!isCollapsed()) return false; 166 167 removeView(mTabSpinner); 168 addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 169 ViewGroup.LayoutParams.MATCH_PARENT)); 170 setTabSelected(mTabSpinner.getSelectedItemPosition()); 171 return false; 172 } 173 174 public void setTabSelected(int position) { 175 mSelectedTabIndex = position; 176 final int tabCount = mTabLayout.getChildCount(); 177 for (int i = 0; i < tabCount; i++) { 178 final View child = mTabLayout.getChildAt(i); 179 final boolean isSelected = i == position; 180 child.setSelected(isSelected); 181 if (isSelected) { 182 animateToTab(position); 183 } 184 } 185 if (mTabSpinner != null && position >= 0) { 186 mTabSpinner.setSelection(position); 187 } 188 } 189 190 public void setContentHeight(int contentHeight) { 191 mContentHeight = contentHeight; 192 requestLayout(); 193 } 194 195 private LinearLayout createTabLayout() { 196 final LinearLayout tabLayout = new LinearLayout(getContext(), null, 197 com.android.internal.R.attr.actionBarTabBarStyle); 198 tabLayout.setMeasureWithLargestChildEnabled(true); 199 tabLayout.setGravity(Gravity.CENTER); 200 tabLayout.setLayoutParams(new LinearLayout.LayoutParams( 201 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); 202 return tabLayout; 203 } 204 205 private Spinner createSpinner() { 206 final Spinner spinner = new Spinner(getContext(), null, 207 com.android.internal.R.attr.actionDropDownStyle); 208 spinner.setLayoutParams(new LinearLayout.LayoutParams( 209 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); 210 spinner.setOnItemClickListenerInt(this); 211 return spinner; 212 } 213 214 @Override 215 protected void onConfigurationChanged(Configuration newConfig) { 216 super.onConfigurationChanged(newConfig); 217 218 ActionBarPolicy abp = ActionBarPolicy.get(getContext()); 219 // Action bar can change size on configuration changes. 220 // Reread the desired height from the theme-specified style. 221 setContentHeight(abp.getTabContainerHeight()); 222 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 223 } 224 225 public void animateToVisibility(int visibility) { 226 if (mVisibilityAnim != null) { 227 mVisibilityAnim.cancel(); 228 } 229 if (visibility == VISIBLE) { 230 if (getVisibility() != VISIBLE) { 231 setAlpha(0); 232 } 233 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1); 234 anim.setDuration(FADE_DURATION); 235 anim.setInterpolator(sAlphaInterpolator); 236 237 anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); 238 anim.start(); 239 } else { 240 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0); 241 anim.setDuration(FADE_DURATION); 242 anim.setInterpolator(sAlphaInterpolator); 243 244 anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); 245 anim.start(); 246 } 247 } 248 249 public void animateToTab(final int position) { 250 final View tabView = mTabLayout.getChildAt(position); 251 if (mTabSelector != null) { 252 removeCallbacks(mTabSelector); 253 } 254 mTabSelector = new Runnable() { 255 public void run() { 256 final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2; 257 smoothScrollTo(scrollPos, 0); 258 mTabSelector = null; 259 } 260 }; 261 post(mTabSelector); 262 } 263 264 @Override 265 public void onAttachedToWindow() { 266 super.onAttachedToWindow(); 267 if (mTabSelector != null) { 268 // Re-post the selector we saved 269 post(mTabSelector); 270 } 271 } 272 273 @Override 274 public void onDetachedFromWindow() { 275 super.onDetachedFromWindow(); 276 if (mTabSelector != null) { 277 removeCallbacks(mTabSelector); 278 } 279 } 280 281 private TabView createTabView(Context context, ActionBar.Tab tab, boolean forAdapter) { 282 final TabView tabView = new TabView(context, tab, forAdapter); 283 if (forAdapter) { 284 tabView.setBackgroundDrawable(null); 285 tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, 286 mContentHeight)); 287 } else { 288 tabView.setFocusable(true); 289 290 if (mTabClickListener == null) { 291 mTabClickListener = new TabClickListener(); 292 } 293 tabView.setOnClickListener(mTabClickListener); 294 } 295 return tabView; 296 } 297 298 public void addTab(ActionBar.Tab tab, boolean setSelected) { 299 TabView tabView = createTabView(mContext, tab, false); 300 mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, 301 LayoutParams.MATCH_PARENT, 1)); 302 if (mTabSpinner != null) { 303 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 304 } 305 if (setSelected) { 306 tabView.setSelected(true); 307 } 308 if (mAllowCollapse) { 309 requestLayout(); 310 } 311 } 312 313 public void addTab(ActionBar.Tab tab, int position, boolean setSelected) { 314 final TabView tabView = createTabView(mContext, tab, false); 315 mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams( 316 0, LayoutParams.MATCH_PARENT, 1)); 317 if (mTabSpinner != null) { 318 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 319 } 320 if (setSelected) { 321 tabView.setSelected(true); 322 } 323 if (mAllowCollapse) { 324 requestLayout(); 325 } 326 } 327 328 public void updateTab(int position) { 329 ((TabView) mTabLayout.getChildAt(position)).update(); 330 if (mTabSpinner != null) { 331 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 332 } 333 if (mAllowCollapse) { 334 requestLayout(); 335 } 336 } 337 338 public void removeTabAt(int position) { 339 mTabLayout.removeViewAt(position); 340 if (mTabSpinner != null) { 341 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 342 } 343 if (mAllowCollapse) { 344 requestLayout(); 345 } 346 } 347 348 public void removeAllTabs() { 349 mTabLayout.removeAllViews(); 350 if (mTabSpinner != null) { 351 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 352 } 353 if (mAllowCollapse) { 354 requestLayout(); 355 } 356 } 357 358 @Override 359 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 360 TabView tabView = (TabView) view; 361 tabView.getTab().select(); 362 } 363 364 private class TabView extends LinearLayout implements OnLongClickListener { 365 private ActionBar.Tab mTab; 366 private TextView mTextView; 367 private ImageView mIconView; 368 private View mCustomView; 369 370 public TabView(Context context, ActionBar.Tab tab, boolean forList) { 371 super(context, null, com.android.internal.R.attr.actionBarTabStyle); 372 mTab = tab; 373 374 if (forList) { 375 setGravity(Gravity.START | Gravity.CENTER_VERTICAL); 376 } 377 378 update(); 379 } 380 381 public void bindTab(ActionBar.Tab tab) { 382 mTab = tab; 383 update(); 384 } 385 386 @Override 387 public void setSelected(boolean selected) { 388 final boolean changed = (isSelected() != selected); 389 super.setSelected(selected); 390 if (changed && selected) { 391 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 392 } 393 } 394 395 @Override 396 public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { 397 super.onInitializeAccessibilityEventInternal(event); 398 // This view masquerades as an action bar tab. 399 event.setClassName(ActionBar.Tab.class.getName()); 400 } 401 402 @Override 403 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 404 super.onInitializeAccessibilityNodeInfoInternal(info); 405 // This view masquerades as an action bar tab. 406 info.setClassName(ActionBar.Tab.class.getName()); 407 } 408 409 @Override 410 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 411 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 412 413 // Re-measure if we went beyond our maximum size. 414 if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) { 415 super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY), 416 heightMeasureSpec); 417 } 418 } 419 420 public void update() { 421 final ActionBar.Tab tab = mTab; 422 final View custom = tab.getCustomView(); 423 if (custom != null) { 424 final ViewParent customParent = custom.getParent(); 425 if (customParent != this) { 426 if (customParent != null) ((ViewGroup) customParent).removeView(custom); 427 addView(custom); 428 } 429 mCustomView = custom; 430 if (mTextView != null) mTextView.setVisibility(GONE); 431 if (mIconView != null) { 432 mIconView.setVisibility(GONE); 433 mIconView.setImageDrawable(null); 434 } 435 } else { 436 if (mCustomView != null) { 437 removeView(mCustomView); 438 mCustomView = null; 439 } 440 441 final Drawable icon = tab.getIcon(); 442 final CharSequence text = tab.getText(); 443 444 if (icon != null) { 445 if (mIconView == null) { 446 ImageView iconView = new ImageView(getContext()); 447 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 448 LayoutParams.WRAP_CONTENT); 449 lp.gravity = Gravity.CENTER_VERTICAL; 450 iconView.setLayoutParams(lp); 451 addView(iconView, 0); 452 mIconView = iconView; 453 } 454 mIconView.setImageDrawable(icon); 455 mIconView.setVisibility(VISIBLE); 456 } else if (mIconView != null) { 457 mIconView.setVisibility(GONE); 458 mIconView.setImageDrawable(null); 459 } 460 461 final boolean hasText = !TextUtils.isEmpty(text); 462 if (hasText) { 463 if (mTextView == null) { 464 TextView textView = new TextView(getContext(), null, 465 com.android.internal.R.attr.actionBarTabTextStyle); 466 textView.setEllipsize(TruncateAt.END); 467 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 468 LayoutParams.WRAP_CONTENT); 469 lp.gravity = Gravity.CENTER_VERTICAL; 470 textView.setLayoutParams(lp); 471 addView(textView); 472 mTextView = textView; 473 } 474 mTextView.setText(text); 475 mTextView.setVisibility(VISIBLE); 476 } else if (mTextView != null) { 477 mTextView.setVisibility(GONE); 478 mTextView.setText(null); 479 } 480 481 if (mIconView != null) { 482 mIconView.setContentDescription(tab.getContentDescription()); 483 } 484 485 if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) { 486 setOnLongClickListener(this); 487 } else { 488 setOnLongClickListener(null); 489 setLongClickable(false); 490 } 491 } 492 } 493 494 public boolean onLongClick(View v) { 495 final int[] screenPos = new int[2]; 496 getLocationOnScreen(screenPos); 497 498 final Context context = getContext(); 499 final int width = getWidth(); 500 final int height = getHeight(); 501 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 502 503 Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), 504 Toast.LENGTH_SHORT); 505 // Show under the tab 506 cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 507 (screenPos[0] + width / 2) - screenWidth / 2, height); 508 509 cheatSheet.show(); 510 return true; 511 } 512 513 public ActionBar.Tab getTab() { 514 return mTab; 515 } 516 } 517 518 private class TabAdapter extends BaseAdapter { 519 private Context mDropDownContext; 520 521 public TabAdapter(Context context) { 522 setDropDownViewContext(context); 523 } 524 525 public void setDropDownViewContext(Context context) { 526 mDropDownContext = context; 527 } 528 529 @Override 530 public int getCount() { 531 return mTabLayout.getChildCount(); 532 } 533 534 @Override 535 public Object getItem(int position) { 536 return ((TabView) mTabLayout.getChildAt(position)).getTab(); 537 } 538 539 @Override 540 public long getItemId(int position) { 541 return position; 542 } 543 544 @Override 545 public View getView(int position, View convertView, ViewGroup parent) { 546 if (convertView == null) { 547 convertView = createTabView(mContext, (ActionBar.Tab) getItem(position), true); 548 } else { 549 ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); 550 } 551 return convertView; 552 } 553 554 @Override 555 public View getDropDownView(int position, View convertView, ViewGroup parent) { 556 if (convertView == null) { 557 convertView = createTabView(mDropDownContext, 558 (ActionBar.Tab) getItem(position), true); 559 } else { 560 ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); 561 } 562 return convertView; 563 } 564 } 565 566 private class TabClickListener implements OnClickListener { 567 public void onClick(View view) { 568 TabView tabView = (TabView) view; 569 tabView.getTab().select(); 570 final int tabCount = mTabLayout.getChildCount(); 571 for (int i = 0; i < tabCount; i++) { 572 final View child = mTabLayout.getChildAt(i); 573 child.setSelected(child == view); 574 } 575 } 576 } 577 578 protected class VisibilityAnimListener implements Animator.AnimatorListener { 579 private boolean mCanceled = false; 580 private int mFinalVisibility; 581 582 public VisibilityAnimListener withFinalVisibility(int visibility) { 583 mFinalVisibility = visibility; 584 return this; 585 } 586 587 @Override 588 public void onAnimationStart(Animator animation) { 589 setVisibility(VISIBLE); 590 mVisibilityAnim = animation; 591 mCanceled = false; 592 } 593 594 @Override 595 public void onAnimationEnd(Animator animation) { 596 if (mCanceled) return; 597 598 mVisibilityAnim = null; 599 setVisibility(mFinalVisibility); 600 } 601 602 @Override 603 public void onAnimationCancel(Animator animation) { 604 mCanceled = true; 605 } 606 607 @Override 608 public void onAnimationRepeat(Animator animation) { 609 } 610 } 611} 612