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