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