ScrollingTabContainerView.java revision da10fdd1400ecfd8d7f2e55651dd528d0614dfc5
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.AdapterView; 32import android.widget.BaseAdapter; 33import android.widget.HorizontalScrollView; 34import android.widget.ImageView; 35import android.widget.LinearLayout; 36import android.widget.ListView; 37import android.widget.Spinner; 38import android.widget.TextView; 39 40/** 41 * This widget implements the dynamic action bar tab behavior that can change across different 42 * configurations or circumstances. 43 */ 44public class ScrollingTabContainerView extends HorizontalScrollView 45 implements AdapterView.OnItemClickListener { 46 47 private static final String TAG = "ScrollingTabContainerView"; 48 Runnable mTabSelector; 49 private TabClickListener mTabClickListener; 50 51 private LinearLayout mTabLayout; 52 private Spinner 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.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 } 180 181 public void setContentHeight(int contentHeight) { 182 mContentHeight = contentHeight; 183 requestLayout(); 184 } 185 186 private Spinner createSpinner() { 187 final Spinner spinner = new Spinner(getContext(), null, 188 R.attr.actionDropDownStyle); 189 spinner.setLayoutParams(new LinearLayout.LayoutParams( 190 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.FILL_PARENT)); 191 spinner.setOnItemClickListener(this); 192 return spinner; 193 } 194 195 protected void onConfigurationChanged(Configuration newConfig) { 196 ActionBarPolicy abp = ActionBarPolicy.get(getContext()); 197 // Action bar can change size on configuration changes. 198 // Reread the desired height from the theme-specified style. 199 setContentHeight(abp.getTabContainerHeight()); 200 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 201 } 202 203 public void animateToTab(final int position) { 204 final View tabView = mTabLayout.getChildAt(position); 205 if (mTabSelector != null) { 206 removeCallbacks(mTabSelector); 207 } 208 mTabSelector = new Runnable() { 209 public void run() { 210 final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2; 211 smoothScrollTo(scrollPos, 0); 212 mTabSelector = null; 213 } 214 }; 215 post(mTabSelector); 216 } 217 218 @Override 219 public void onAttachedToWindow() { 220 super.onAttachedToWindow(); 221 if (mTabSelector != null) { 222 // Re-post the selector we saved 223 post(mTabSelector); 224 } 225 } 226 227 @Override 228 public void onDetachedFromWindow() { 229 super.onDetachedFromWindow(); 230 if (mTabSelector != null) { 231 removeCallbacks(mTabSelector); 232 } 233 } 234 235 private TabView createTabView(ActionBar.Tab tab, boolean forAdapter) { 236 final TabView tabView = (TabView) mInflater.inflate(R.layout.action_bar_tab, mTabLayout, 237 false); 238 tabView.attach(this, tab, forAdapter); 239 240 if (forAdapter) { 241 tabView.setBackgroundDrawable(null); 242 tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.FILL_PARENT, 243 mContentHeight)); 244 } else { 245 tabView.setFocusable(true); 246 247 if (mTabClickListener == null) { 248 mTabClickListener = new TabClickListener(); 249 } 250 tabView.setOnClickListener(mTabClickListener); 251 } 252 return tabView; 253 } 254 255 public void addTab(ActionBar.Tab tab, boolean setSelected) { 256 TabView tabView = createTabView(tab, false); 257 mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, 258 LayoutParams.FILL_PARENT, 1)); 259 if (mTabSpinner != null) { 260 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 261 } 262 if (setSelected) { 263 tabView.setSelected(true); 264 } 265 if (mAllowCollapse) { 266 requestLayout(); 267 } 268 } 269 270 public void addTab(ActionBar.Tab tab, int position, boolean setSelected) { 271 final TabView tabView = createTabView(tab, false); 272 mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams( 273 0, LayoutParams.FILL_PARENT, 1)); 274 if (mTabSpinner != null) { 275 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 276 } 277 if (setSelected) { 278 tabView.setSelected(true); 279 } 280 if (mAllowCollapse) { 281 requestLayout(); 282 } 283 } 284 285 public void updateTab(int position) { 286 ((TabView) mTabLayout.getChildAt(position)).update(); 287 if (mTabSpinner != null) { 288 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 289 } 290 if (mAllowCollapse) { 291 requestLayout(); 292 } 293 } 294 295 public void removeTabAt(int position) { 296 mTabLayout.removeViewAt(position); 297 if (mTabSpinner != null) { 298 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 299 } 300 if (mAllowCollapse) { 301 requestLayout(); 302 } 303 } 304 305 public void removeAllTabs() { 306 mTabLayout.removeAllViews(); 307 if (mTabSpinner != null) { 308 ((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged(); 309 } 310 if (mAllowCollapse) { 311 requestLayout(); 312 } 313 } 314 315 @Override 316 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 317 TabView tabView = (TabView) view; 318 tabView.getTab().select(); 319 } 320 321 public static class TabView extends LinearLayout { 322 323 private ActionBar.Tab mTab; 324 private TextView mTextView; 325 private ImageView mIconView; 326 private View mCustomView; 327 private ScrollingTabContainerView mParent; 328 329 public TabView(Context context, AttributeSet attrs) { 330 super(context, attrs); 331 } 332 333 void attach(ScrollingTabContainerView parent, ActionBar.Tab tab, boolean forList) { 334 mParent = parent; 335 mTab = tab; 336 337 if (forList) { 338 setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); 339 } 340 341 update(); 342 } 343 344 public void bindTab(ActionBar.Tab tab) { 345 mTab = tab; 346 update(); 347 } 348 349 @Override 350 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 351 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 352 353 int maxTabWidth = mParent != null ? mParent.mMaxTabWidth : 0; 354 355 // Re-measure if we went beyond our maximum size. 356 if (maxTabWidth > 0 && getMeasuredWidth() > maxTabWidth) { 357 super.onMeasure(MeasureSpec.makeMeasureSpec(maxTabWidth, MeasureSpec.EXACTLY), 358 heightMeasureSpec); 359 } 360 } 361 362 public void update() { 363 final ActionBar.Tab tab = mTab; 364 final View custom = tab.getCustomView(); 365 if (custom != null) { 366 final ViewParent customParent = custom.getParent(); 367 if (customParent != this) { 368 if (customParent != null) { 369 ((ViewGroup) customParent).removeView(custom); 370 } 371 addView(custom); 372 } 373 mCustomView = custom; 374 if (mTextView != null) { 375 mTextView.setVisibility(GONE); 376 } 377 if (mIconView != null) { 378 mIconView.setVisibility(GONE); 379 mIconView.setImageDrawable(null); 380 } 381 } else { 382 if (mCustomView != null) { 383 removeView(mCustomView); 384 mCustomView = null; 385 } 386 387 final Drawable icon = tab.getIcon(); 388 final CharSequence text = tab.getText(); 389 390 if (icon != null) { 391 if (mIconView == null) { 392 ImageView iconView = new ImageView(getContext()); 393 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 394 LayoutParams.WRAP_CONTENT); 395 lp.gravity = Gravity.CENTER_VERTICAL; 396 iconView.setLayoutParams(lp); 397 addView(iconView, 0); 398 mIconView = iconView; 399 } 400 mIconView.setImageDrawable(icon); 401 mIconView.setVisibility(VISIBLE); 402 } else if (mIconView != null) { 403 mIconView.setVisibility(GONE); 404 mIconView.setImageDrawable(null); 405 } 406 407 if (text != null) { 408 if (mTextView == null) { 409 TextView textView = new TextView(getContext(), null, 410 R.attr.actionBarTabTextStyle); 411 textView.setEllipsize(TruncateAt.END); 412 LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, 413 LayoutParams.WRAP_CONTENT); 414 lp.gravity = Gravity.CENTER_VERTICAL; 415 textView.setLayoutParams(lp); 416 addView(textView); 417 mTextView = textView; 418 } 419 mTextView.setText(text); 420 mTextView.setVisibility(VISIBLE); 421 } else if (mTextView != null) { 422 mTextView.setVisibility(GONE); 423 mTextView.setText(null); 424 } 425 426 if (mIconView != null) { 427 mIconView.setContentDescription(tab.getContentDescription()); 428 } 429 } 430 } 431 432 public ActionBar.Tab getTab() { 433 return mTab; 434 } 435 } 436 437 private class TabAdapter extends BaseAdapter { 438 439 @Override 440 public int getCount() { 441 return mTabLayout.getChildCount(); 442 } 443 444 @Override 445 public Object getItem(int position) { 446 return ((TabView) mTabLayout.getChildAt(position)).getTab(); 447 } 448 449 @Override 450 public long getItemId(int position) { 451 return position; 452 } 453 454 @Override 455 public View getView(int position, View convertView, ViewGroup parent) { 456 if (convertView == null) { 457 convertView = createTabView((ActionBar.Tab) getItem(position), true); 458 } else { 459 ((TabView) convertView).bindTab((ActionBar.Tab) getItem(position)); 460 } 461 return convertView; 462 } 463 } 464 465 private class TabClickListener implements OnClickListener { 466 467 public void onClick(View view) { 468 TabView tabView = (TabView) view; 469 tabView.getTab().select(); 470 final int tabCount = mTabLayout.getChildCount(); 471 for (int i = 0; i < tabCount; i++) { 472 final View child = mTabLayout.getChildAt(i); 473 child.setSelected(child == view); 474 } 475 } 476 } 477 478} 479 480