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 com.android.internal.view.ActionBarPolicy; 19 20 import android.animation.Animator; 21 import android.animation.ObjectAnimator; 22 import android.animation.TimeInterpolator; 23 import android.app.ActionBar; 24 import android.content.Context; 25 import android.content.res.Configuration; 26 import android.graphics.drawable.Drawable; 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.animation.DecelerateInterpolator; 33 import android.widget.AdapterView; 34 import android.widget.BaseAdapter; 35 import android.widget.HorizontalScrollView; 36 import android.widget.ImageView; 37 import android.widget.LinearLayout; 38 import android.widget.ListView; 39 import android.widget.Spinner; 40 import 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 */ 46 public class ScrollingTabContainerView extends HorizontalScrollView 47 implements AdapterView.OnItemClickListener { 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 int mStackedTabMaxWidth; 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 ScrollingTabContainerView(Context context)68 public ScrollingTabContainerView(Context context) { 69 super(context); 70 setHorizontalScrollBarEnabled(false); 71 72 ActionBarPolicy abp = ActionBarPolicy.get(context); 73 setContentHeight(abp.getTabContainerHeight()); 74 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 75 76 mTabLayout = createTabLayout(); 77 addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 78 ViewGroup.LayoutParams.MATCH_PARENT)); 79 } 80 81 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 mMaxTabWidth = Math.min(mMaxTabWidth, mStackedTabMaxWidth); 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 */ isCollapsed()131 private boolean isCollapsed() { 132 return mTabSpinner != null && mTabSpinner.getParent() == this; 133 } 134 setAllowCollapse(boolean allowCollapse)135 public void setAllowCollapse(boolean allowCollapse) { 136 mAllowCollapse = allowCollapse; 137 } 138 performCollapse()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 performExpand()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 setTabSelected(int position)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 setContentHeight(int contentHeight)181 public void setContentHeight(int contentHeight) { 182 mContentHeight = contentHeight; 183 requestLayout(); 184 } 185 createTabLayout()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.setGravity(Gravity.CENTER); 191 tabLayout.setLayoutParams(new LinearLayout.LayoutParams( 192 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); 193 return tabLayout; 194 } 195 createSpinner()196 private Spinner createSpinner() { 197 final Spinner spinner = new Spinner(getContext(), null, 198 com.android.internal.R.attr.actionDropDownStyle); 199 spinner.setLayoutParams(new LinearLayout.LayoutParams( 200 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); 201 spinner.setOnItemClickListenerInt(this); 202 return spinner; 203 } 204 205 @Override onConfigurationChanged(Configuration newConfig)206 protected void onConfigurationChanged(Configuration newConfig) { 207 super.onConfigurationChanged(newConfig); 208 209 ActionBarPolicy abp = ActionBarPolicy.get(getContext()); 210 // Action bar can change size on configuration changes. 211 // Reread the desired height from the theme-specified style. 212 setContentHeight(abp.getTabContainerHeight()); 213 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 214 } 215 animateToVisibility(int visibility)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 animateToTab(final int position)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 onAttachedToWindow()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 onDetachedFromWindow()265 public void onDetachedFromWindow() { 266 super.onDetachedFromWindow(); 267 if (mTabSelector != null) { 268 removeCallbacks(mTabSelector); 269 } 270 } 271 createTabView(ActionBar.Tab tab, boolean forAdapter)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 addTab(ActionBar.Tab tab, boolean setSelected)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 addTab(ActionBar.Tab tab, int position, boolean setSelected)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 updateTab(int position)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 removeTabAt(int position)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 removeAllTabs()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 onItemClick(AdapterView<?> parent, View view, int position, long id)350 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 351 TabView tabView = (TabView) view; 352 tabView.getTab().select(); 353 } 354 355 private class TabView extends LinearLayout { 356 private ActionBar.Tab mTab; 357 private TextView mTextView; 358 private ImageView mIconView; 359 private View mCustomView; 360 TabView(Context context, ActionBar.Tab tab, boolean forList)361 public TabView(Context context, ActionBar.Tab tab, boolean forList) { 362 super(context, null, com.android.internal.R.attr.actionBarTabStyle); 363 mTab = tab; 364 365 if (forList) { 366 setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); 367 } 368 369 update(); 370 } 371 bindTab(ActionBar.Tab tab)372 public void bindTab(ActionBar.Tab tab) { 373 mTab = tab; 374 update(); 375 } 376 377 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)378 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 379 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 380 381 // Re-measure if we went beyond our maximum size. 382 if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) { 383 super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY), 384 heightMeasureSpec); 385 } 386 } 387 update()388 public void update() { 389 final ActionBar.Tab tab = mTab; 390 final View custom = tab.getCustomView(); 391 if (custom != null) { 392 final ViewParent customParent = custom.getParent(); 393 if (customParent != this) { 394 if (customParent != null) ((ViewGroup) customParent).removeView(custom); 395 addView(custom); 396 } 397 mCustomView = custom; 398 if (mTextView != null) mTextView.setVisibility(GONE); 399 if (mIconView != null) { 400 mIconView.setVisibility(GONE); 401 mIconView.setImageDrawable(null); 402 } 403 } else { 404 if (mCustomView != null) { 405 removeView(mCustomView); 406 mCustomView = null; 407 } 408 409 final Drawable icon = tab.getIcon(); 410 final CharSequence text = tab.getText(); 411 412 if (icon != null) { 413 if (mIconView == null) { 414 ImageView iconView = new ImageView(getContext()); 415 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 416 LayoutParams.WRAP_CONTENT); 417 lp.gravity = Gravity.CENTER_VERTICAL; 418 iconView.setLayoutParams(lp); 419 addView(iconView, 0); 420 mIconView = iconView; 421 } 422 mIconView.setImageDrawable(icon); 423 mIconView.setVisibility(VISIBLE); 424 } else if (mIconView != null) { 425 mIconView.setVisibility(GONE); 426 mIconView.setImageDrawable(null); 427 } 428 429 if (text != null) { 430 if (mTextView == null) { 431 TextView textView = new TextView(getContext(), null, 432 com.android.internal.R.attr.actionBarTabTextStyle); 433 textView.setEllipsize(TruncateAt.END); 434 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 435 LayoutParams.WRAP_CONTENT); 436 lp.gravity = Gravity.CENTER_VERTICAL; 437 textView.setLayoutParams(lp); 438 addView(textView); 439 mTextView = textView; 440 } 441 mTextView.setText(text); 442 mTextView.setVisibility(VISIBLE); 443 } else if (mTextView != null) { 444 mTextView.setVisibility(GONE); 445 mTextView.setText(null); 446 } 447 448 if (mIconView != null) { 449 mIconView.setContentDescription(tab.getContentDescription()); 450 } 451 } 452 } 453 getTab()454 public ActionBar.Tab getTab() { 455 return mTab; 456 } 457 } 458 459 private class TabAdapter extends BaseAdapter { 460 @Override getCount()461 public int getCount() { 462 return mTabLayout.getChildCount(); 463 } 464 465 @Override getItem(int position)466 public Object getItem(int position) { 467 return ((TabView) mTabLayout.getChildAt(position)).getTab(); 468 } 469 470 @Override getItemId(int position)471 public long getItemId(int position) { 472 return position; 473 } 474 475 @Override getView(int position, View convertView, ViewGroup parent)476 public View getView(int position, View convertView, ViewGroup parent) { 477 if (convertView == null) { 478 convertView = createTabView((ActionBar.Tab) getItem(position), true); 479 } else { 480 ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); 481 } 482 return convertView; 483 } 484 } 485 486 private class TabClickListener implements OnClickListener { onClick(View view)487 public void onClick(View view) { 488 TabView tabView = (TabView) view; 489 tabView.getTab().select(); 490 final int tabCount = mTabLayout.getChildCount(); 491 for (int i = 0; i < tabCount; i++) { 492 final View child = mTabLayout.getChildAt(i); 493 child.setSelected(child == view); 494 } 495 } 496 } 497 498 protected class VisibilityAnimListener implements Animator.AnimatorListener { 499 private boolean mCanceled = false; 500 private int mFinalVisibility; 501 withFinalVisibility(int visibility)502 public VisibilityAnimListener withFinalVisibility(int visibility) { 503 mFinalVisibility = visibility; 504 return this; 505 } 506 507 @Override onAnimationStart(Animator animation)508 public void onAnimationStart(Animator animation) { 509 setVisibility(VISIBLE); 510 mVisibilityAnim = animation; 511 mCanceled = false; 512 } 513 514 @Override onAnimationEnd(Animator animation)515 public void onAnimationEnd(Animator animation) { 516 if (mCanceled) return; 517 518 mVisibilityAnim = null; 519 setVisibility(mFinalVisibility); 520 } 521 522 @Override onAnimationCancel(Animator animation)523 public void onAnimationCancel(Animator animation) { 524 mCanceled = true; 525 } 526 527 @Override onAnimationRepeat(Animator animation)528 public void onAnimationRepeat(Animator animation) { 529 } 530 } 531 } 532