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 android.support.v7.internal.widget; 17 18import android.content.Context; 19import android.content.res.Configuration; 20import android.graphics.drawable.Drawable; 21import android.support.v7.app.ActionBar; 22import android.support.v7.appcompat.R; 23import android.support.v7.internal.view.ActionBarPolicy; 24import android.text.TextUtils.TruncateAt; 25import android.util.AttributeSet; 26import android.view.Gravity; 27import android.view.LayoutInflater; 28import android.view.View; 29import android.view.ViewGroup; 30import android.view.ViewParent; 31import android.widget.BaseAdapter; 32import android.widget.HorizontalScrollView; 33import android.widget.ImageView; 34import android.widget.LinearLayout; 35import android.widget.ListView; 36import android.widget.TextView; 37 38/** 39 * This widget implements the dynamic action bar tab behavior that can change across different 40 * configurations or circumstances. 41 * 42 * @hide 43 */ 44public class ScrollingTabContainerView extends HorizontalScrollView 45 implements AdapterViewICS.OnItemClickListener { 46 47 private static final String TAG = "ScrollingTabContainerView"; 48 Runnable mTabSelector; 49 private TabClickListener mTabClickListener; 50 51 private LinearLayout mTabLayout; 52 private SpinnerICS mTabSpinner; 53 private boolean mAllowCollapse; 54 55 private final LayoutInflater mInflater; 56 57 int mMaxTabWidth; 58 int mStackedTabMaxWidth; 59 private int mContentHeight; 60 private int mSelectedTabIndex; 61 62 public ScrollingTabContainerView(Context context) { 63 super(context); 64 mInflater = LayoutInflater.from(context); 65 66 setHorizontalScrollBarEnabled(false); 67 68 ActionBarPolicy abp = ActionBarPolicy.get(context); 69 setContentHeight(abp.getTabContainerHeight()); 70 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 71 72 mTabLayout = (LinearLayout) mInflater.inflate(R.layout.abc_action_bar_tabbar, this, false); 73 addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 74 ViewGroup.LayoutParams.FILL_PARENT)); 75 } 76 77 @Override 78 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 79 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 80 final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY; 81 setFillViewport(lockedExpanded); 82 83 final int childCount = mTabLayout.getChildCount(); 84 if (childCount > 1 && 85 (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) { 86 if (childCount > 2) { 87 mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f); 88 } else { 89 mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2; 90 } 91 mMaxTabWidth = Math.min(mMaxTabWidth, mStackedTabMaxWidth); 92 } else { 93 mMaxTabWidth = -1; 94 } 95 96 heightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY); 97 98 final boolean canCollapse = !lockedExpanded && mAllowCollapse; 99 100 if (canCollapse) { 101 // See if we should expand 102 mTabLayout.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec); 103 if (mTabLayout.getMeasuredWidth() > MeasureSpec.getSize(widthMeasureSpec)) { 104 performCollapse(); 105 } else { 106 performExpand(); 107 } 108 } else { 109 performExpand(); 110 } 111 112 final int oldWidth = getMeasuredWidth(); 113 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 114 final int newWidth = getMeasuredWidth(); 115 116 if (lockedExpanded && oldWidth != newWidth) { 117 // Recenter the tab display if we're at a new (scrollable) size. 118 setTabSelected(mSelectedTabIndex); 119 } 120 } 121 122 /** 123 * Indicates whether this view is collapsed into a dropdown menu instead of traditional tabs. 124 * 125 * @return true if showing as a spinner 126 */ 127 private boolean isCollapsed() { 128 return mTabSpinner != null && mTabSpinner.getParent() == this; 129 } 130 131 public void setAllowCollapse(boolean allowCollapse) { 132 mAllowCollapse = allowCollapse; 133 } 134 135 private void performCollapse() { 136 if (isCollapsed()) { 137 return; 138 } 139 140 if (mTabSpinner == null) { 141 mTabSpinner = createSpinner(); 142 } 143 removeView(mTabLayout); 144 addView(mTabSpinner, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 145 ViewGroup.LayoutParams.FILL_PARENT)); 146 if (mTabSpinner.getAdapter() == null) { 147 mTabSpinner.setAdapter(new TabAdapter()); 148 } 149 if (mTabSelector != null) { 150 removeCallbacks(mTabSelector); 151 mTabSelector = null; 152 } 153 mTabSpinner.setSelection(mSelectedTabIndex); 154 } 155 156 private boolean performExpand() { 157 if (!isCollapsed()) { 158 return false; 159 } 160 161 removeView(mTabSpinner); 162 addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 163 ViewGroup.LayoutParams.FILL_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 if (mTabSpinner != null && position >= 0) { 180 mTabSpinner.setSelection(position); 181 } 182 } 183 184 public void setContentHeight(int contentHeight) { 185 mContentHeight = contentHeight; 186 requestLayout(); 187 } 188 189 private SpinnerICS createSpinner() { 190 final SpinnerICS spinner = new SpinnerICS(getContext(), null, 191 R.attr.actionDropDownStyle); 192 spinner.setLayoutParams(new LinearLayout.LayoutParams( 193 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.FILL_PARENT)); 194 spinner.setOnItemClickListenerInt(this); 195 return spinner; 196 } 197 198 protected void onConfigurationChanged(Configuration newConfig) { 199 ActionBarPolicy abp = ActionBarPolicy.get(getContext()); 200 // Action bar can change size on configuration changes. 201 // Reread the desired height from the theme-specified style. 202 setContentHeight(abp.getTabContainerHeight()); 203 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 204 } 205 206 public void animateToTab(final int position) { 207 final View tabView = mTabLayout.getChildAt(position); 208 if (mTabSelector != null) { 209 removeCallbacks(mTabSelector); 210 } 211 mTabSelector = new Runnable() { 212 public void run() { 213 final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2; 214 smoothScrollTo(scrollPos, 0); 215 mTabSelector = null; 216 } 217 }; 218 post(mTabSelector); 219 } 220 221 @Override 222 public void onAttachedToWindow() { 223 super.onAttachedToWindow(); 224 if (mTabSelector != null) { 225 // Re-post the selector we saved 226 post(mTabSelector); 227 } 228 } 229 230 @Override 231 public void onDetachedFromWindow() { 232 super.onDetachedFromWindow(); 233 if (mTabSelector != null) { 234 removeCallbacks(mTabSelector); 235 } 236 } 237 238 private TabView createTabView(ActionBar.Tab tab, boolean forAdapter) { 239 final TabView tabView = (TabView) mInflater.inflate(R.layout.abc_action_bar_tab, mTabLayout, 240 false); 241 tabView.attach(this, tab, forAdapter); 242 243 if (forAdapter) { 244 tabView.setBackgroundDrawable(null); 245 tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.FILL_PARENT, 246 mContentHeight)); 247 } else { 248 tabView.setFocusable(true); 249 250 if (mTabClickListener == null) { 251 mTabClickListener = new TabClickListener(); 252 } 253 tabView.setOnClickListener(mTabClickListener); 254 } 255 return tabView; 256 } 257 258 public void addTab(ActionBar.Tab tab, boolean setSelected) { 259 TabView tabView = createTabView(tab, false); 260 mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, 261 LayoutParams.FILL_PARENT, 1)); 262 if (mTabSpinner != null) { 263 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 264 } 265 if (setSelected) { 266 tabView.setSelected(true); 267 } 268 if (mAllowCollapse) { 269 requestLayout(); 270 } 271 } 272 273 public void addTab(ActionBar.Tab tab, int position, boolean setSelected) { 274 final TabView tabView = createTabView(tab, false); 275 mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams( 276 0, LayoutParams.FILL_PARENT, 1)); 277 if (mTabSpinner != null) { 278 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 279 } 280 if (setSelected) { 281 tabView.setSelected(true); 282 } 283 if (mAllowCollapse) { 284 requestLayout(); 285 } 286 } 287 288 public void updateTab(int position) { 289 ((TabView) mTabLayout.getChildAt(position)).update(); 290 if (mTabSpinner != null) { 291 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 292 } 293 if (mAllowCollapse) { 294 requestLayout(); 295 } 296 } 297 298 public void removeTabAt(int position) { 299 mTabLayout.removeViewAt(position); 300 if (mTabSpinner != null) { 301 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 302 } 303 if (mAllowCollapse) { 304 requestLayout(); 305 } 306 } 307 308 public void removeAllTabs() { 309 mTabLayout.removeAllViews(); 310 if (mTabSpinner != null) { 311 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 312 } 313 if (mAllowCollapse) { 314 requestLayout(); 315 } 316 } 317 318 @Override 319 public void onItemClick(AdapterViewICS<?> parent, View view, int position, long id) { 320 TabView tabView = (TabView) view; 321 tabView.getTab().select(); 322 } 323 324 /** 325 * @hide 326 */ 327 public static class TabView extends LinearLayout { 328 329 private ActionBar.Tab mTab; 330 private TextView mTextView; 331 private ImageView mIconView; 332 private View mCustomView; 333 private ScrollingTabContainerView mParent; 334 335 public TabView(Context context, AttributeSet attrs) { 336 super(context, attrs); 337 } 338 339 void attach(ScrollingTabContainerView parent, ActionBar.Tab tab, boolean forList) { 340 mParent = parent; 341 mTab = tab; 342 343 if (forList) { 344 setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); 345 } 346 347 update(); 348 } 349 350 public void bindTab(ActionBar.Tab tab) { 351 mTab = tab; 352 update(); 353 } 354 355 @Override 356 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 357 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 358 359 int maxTabWidth = mParent != null ? mParent.mMaxTabWidth : 0; 360 361 // Re-measure if we went beyond our maximum size. 362 if (maxTabWidth > 0 && getMeasuredWidth() > maxTabWidth) { 363 super.onMeasure(MeasureSpec.makeMeasureSpec(maxTabWidth, MeasureSpec.EXACTLY), 364 heightMeasureSpec); 365 } 366 } 367 368 public void update() { 369 final ActionBar.Tab tab = mTab; 370 final View custom = tab.getCustomView(); 371 if (custom != null) { 372 final ViewParent customParent = custom.getParent(); 373 if (customParent != this) { 374 if (customParent != null) { 375 ((ViewGroup) customParent).removeView(custom); 376 } 377 addView(custom); 378 } 379 mCustomView = custom; 380 if (mTextView != null) { 381 mTextView.setVisibility(GONE); 382 } 383 if (mIconView != null) { 384 mIconView.setVisibility(GONE); 385 mIconView.setImageDrawable(null); 386 } 387 } else { 388 if (mCustomView != null) { 389 removeView(mCustomView); 390 mCustomView = null; 391 } 392 393 final Drawable icon = tab.getIcon(); 394 final CharSequence text = tab.getText(); 395 396 if (icon != null) { 397 if (mIconView == null) { 398 ImageView iconView = new ImageView(getContext()); 399 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 400 LayoutParams.WRAP_CONTENT); 401 lp.gravity = Gravity.CENTER_VERTICAL; 402 iconView.setLayoutParams(lp); 403 addView(iconView, 0); 404 mIconView = iconView; 405 } 406 mIconView.setImageDrawable(icon); 407 mIconView.setVisibility(VISIBLE); 408 } else if (mIconView != null) { 409 mIconView.setVisibility(GONE); 410 mIconView.setImageDrawable(null); 411 } 412 413 if (text != null) { 414 if (mTextView == null) { 415 TextView textView = new CompatTextView(getContext(), null, 416 R.attr.actionBarTabTextStyle); 417 textView.setEllipsize(TruncateAt.END); 418 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 419 LayoutParams.WRAP_CONTENT); 420 lp.gravity = Gravity.CENTER_VERTICAL; 421 textView.setLayoutParams(lp); 422 addView(textView); 423 mTextView = textView; 424 } 425 mTextView.setText(text); 426 mTextView.setVisibility(VISIBLE); 427 } else if (mTextView != null) { 428 mTextView.setVisibility(GONE); 429 mTextView.setText(null); 430 } 431 432 if (mIconView != null) { 433 mIconView.setContentDescription(tab.getContentDescription()); 434 } 435 } 436 } 437 438 public ActionBar.Tab getTab() { 439 return mTab; 440 } 441 } 442 443 private class TabAdapter extends BaseAdapter { 444 445 @Override 446 public int getCount() { 447 return mTabLayout.getChildCount(); 448 } 449 450 @Override 451 public Object getItem(int position) { 452 return ((TabView) mTabLayout.getChildAt(position)).getTab(); 453 } 454 455 @Override 456 public long getItemId(int position) { 457 return position; 458 } 459 460 @Override 461 public View getView(int position, View convertView, ViewGroup parent) { 462 if (convertView == null) { 463 convertView = createTabView((ActionBar.Tab) getItem(position), true); 464 } else { 465 ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); 466 } 467 return convertView; 468 } 469 } 470 471 private class TabClickListener implements OnClickListener { 472 473 public void onClick(View view) { 474 TabView tabView = (TabView) view; 475 tabView.getTab().select(); 476 final int tabCount = mTabLayout.getChildCount(); 477 for (int i = 0; i < tabCount; i++) { 478 final View child = mTabLayout.getChildAt(i); 479 child.setSelected(child == view); 480 } 481 } 482 } 483 484} 485 486