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