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