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