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