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