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