PagerTitleStrip.java revision 40f36156156b6aedaf47e65f403212136c7cd7b9
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 17package android.support.v4.view; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.database.DataSetObserver; 22import android.graphics.drawable.Drawable; 23import android.text.TextUtils.TruncateAt; 24import android.util.AttributeSet; 25import android.util.TypedValue; 26import android.view.Gravity; 27import android.view.ViewGroup; 28import android.view.ViewParent; 29import android.widget.TextView; 30 31/** 32 * PagerTitleStrip is a non-interactive indicator of the current, next, 33 * and previous pages of a {@link ViewPager}. It is intended to be used as a 34 * child view of a ViewPager widget in your XML layout. 35 * Add it as a child of a ViewPager in your layout file and set its 36 * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom 37 * of the ViewPager. The title from each page is supplied by the method 38 * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to 39 * the ViewPager. 40 * 41 * <p>For an interactive indicator, see {@link PagerTabStrip}.</p> 42 */ 43public class PagerTitleStrip extends ViewGroup implements ViewPager.Decor { 44 private static final String TAG = "PagerTitleStrip"; 45 46 ViewPager mPager; 47 TextView mPrevText; 48 TextView mCurrText; 49 TextView mNextText; 50 51 private int mLastKnownCurrentPage = -1; 52 private float mLastKnownPositionOffset = -1; 53 private int mScaledTextSpacing; 54 private int mGravity; 55 56 private boolean mUpdatingText; 57 private boolean mUpdatingPositions; 58 59 private final PageListener mPageListener = new PageListener(); 60 61 private static final int[] ATTRS = new int[] { 62 android.R.attr.textAppearance, 63 android.R.attr.textSize, 64 android.R.attr.textColor, 65 android.R.attr.gravity 66 }; 67 68 private static final int[] TEXT_ATTRS = new int[] { 69 0x0101038c // android.R.attr.textAllCaps 70 }; 71 72 private static final float SIDE_ALPHA = 0.6f; 73 private static final int TEXT_SPACING = 16; // dip 74 75 private int mNonPrimaryAlpha; 76 int mTextColor; 77 78 interface PagerTitleStripImpl { 79 void setSingleLineAllCaps(TextView text); 80 } 81 82 static class PagerTitleStripImplBase implements PagerTitleStripImpl { 83 public void setSingleLineAllCaps(TextView text) { 84 text.setSingleLine(); 85 } 86 } 87 88 static class PagerTitleStripImplIcs implements PagerTitleStripImpl { 89 public void setSingleLineAllCaps(TextView text) { 90 PagerTitleStripIcs.setSingleLineAllCaps(text); 91 } 92 } 93 94 private static final PagerTitleStripImpl IMPL; 95 static { 96 if (android.os.Build.VERSION.SDK_INT >= 14) { 97 IMPL = new PagerTitleStripImplIcs(); 98 } else { 99 IMPL = new PagerTitleStripImplBase(); 100 } 101 } 102 103 private static void setSingleLineAllCaps(TextView text) { 104 IMPL.setSingleLineAllCaps(text); 105 } 106 107 public PagerTitleStrip(Context context) { 108 this(context, null); 109 } 110 111 public PagerTitleStrip(Context context, AttributeSet attrs) { 112 super(context, attrs); 113 114 addView(mPrevText = new TextView(context)); 115 addView(mCurrText = new TextView(context)); 116 addView(mNextText = new TextView(context)); 117 118 final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); 119 final int textAppearance = a.getResourceId(0, 0); 120 if (textAppearance != 0) { 121 mPrevText.setTextAppearance(context, textAppearance); 122 mCurrText.setTextAppearance(context, textAppearance); 123 mNextText.setTextAppearance(context, textAppearance); 124 } 125 final int textSize = a.getDimensionPixelSize(1, 0); 126 if (textSize != 0) { 127 setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 128 } 129 if (a.hasValue(2)) { 130 final int textColor = a.getColor(2, 0); 131 mPrevText.setTextColor(textColor); 132 mCurrText.setTextColor(textColor); 133 mNextText.setTextColor(textColor); 134 } 135 mGravity = a.getInteger(3, Gravity.BOTTOM); 136 a.recycle(); 137 138 mTextColor = mCurrText.getTextColors().getDefaultColor(); 139 setNonPrimaryAlpha(SIDE_ALPHA); 140 141 mPrevText.setEllipsize(TruncateAt.END); 142 mCurrText.setEllipsize(TruncateAt.END); 143 mNextText.setEllipsize(TruncateAt.END); 144 145 boolean allCaps = false; 146 if (textAppearance != 0) { 147 final TypedArray ta = context.obtainStyledAttributes(textAppearance, TEXT_ATTRS); 148 allCaps = ta.getBoolean(0, false); 149 ta.recycle(); 150 } 151 152 if (allCaps) { 153 setSingleLineAllCaps(mPrevText); 154 setSingleLineAllCaps(mCurrText); 155 setSingleLineAllCaps(mNextText); 156 } else { 157 mPrevText.setSingleLine(); 158 mCurrText.setSingleLine(); 159 mNextText.setSingleLine(); 160 } 161 162 final float density = context.getResources().getDisplayMetrics().density; 163 mScaledTextSpacing = (int) (TEXT_SPACING * density); 164 } 165 166 /** 167 * Set the required spacing between title segments. 168 * 169 * @param spacingPixels Spacing between each title displayed in pixels 170 */ 171 public void setTextSpacing(int spacingPixels) { 172 mScaledTextSpacing = spacingPixels; 173 requestLayout(); 174 } 175 176 /** 177 * @return The required spacing between title segments in pixels 178 */ 179 public int getTextSpacing() { 180 return mScaledTextSpacing; 181 } 182 183 /** 184 * Set the alpha value used for non-primary page titles. 185 * 186 * @param alpha Opacity value in the range 0-1f 187 */ 188 public void setNonPrimaryAlpha(float alpha) { 189 mNonPrimaryAlpha = (int) (alpha * 255) & 0xFF; 190 final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); 191 mPrevText.setTextColor(transparentColor); 192 mNextText.setTextColor(transparentColor); 193 } 194 195 /** 196 * Set the color value used as the base color for all displayed page titles. 197 * Alpha will be ignored for non-primary page titles. See {@link #setNonPrimaryAlpha(float)}. 198 * 199 * @param color Color hex code in 0xAARRGGBB format 200 */ 201 public void setTextColor(int color) { 202 mTextColor = color; 203 mCurrText.setTextColor(color); 204 final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); 205 mPrevText.setTextColor(transparentColor); 206 mNextText.setTextColor(transparentColor); 207 } 208 209 /** 210 * Set the default text size to a given unit and value. 211 * See {@link TypedValue} for the possible dimension units. 212 * 213 * <p>Example: to set the text size to 14px, use 214 * setTextSize(TypedValue.COMPLEX_UNIT_PX, 14);</p> 215 * 216 * @param unit The desired dimension unit 217 * @param size The desired size in the given units 218 */ 219 public void setTextSize(int unit, float size) { 220 mPrevText.setTextSize(unit, size); 221 mCurrText.setTextSize(unit, size); 222 mNextText.setTextSize(unit, size); 223 } 224 225 /** 226 * Set the {@link Gravity} used to position text within the title strip. 227 * Only the vertical gravity component is used. 228 * 229 * @param gravity {@link Gravity} constant for positioning title text 230 */ 231 public void setGravity(int gravity) { 232 mGravity = gravity; 233 requestLayout(); 234 } 235 236 @Override 237 protected void onAttachedToWindow() { 238 super.onAttachedToWindow(); 239 240 final ViewParent parent = getParent(); 241 if (!(parent instanceof ViewPager)) { 242 throw new IllegalStateException( 243 "PagerTitleStrip must be a direct child of a ViewPager."); 244 } 245 246 final ViewPager pager = (ViewPager) parent; 247 final PagerAdapter adapter = pager.getAdapter(); 248 249 pager.setInternalPageChangeListener(mPageListener); 250 pager.setOnAdapterChangeListener(mPageListener); 251 mPager = pager; 252 updateAdapter(null, adapter); 253 } 254 255 @Override 256 protected void onDetachedFromWindow() { 257 super.onDetachedFromWindow(); 258 if (mPager != null) { 259 updateAdapter(mPager.getAdapter(), null); 260 mPager.setInternalPageChangeListener(null); 261 mPager.setOnAdapterChangeListener(null); 262 mPager = null; 263 } 264 } 265 266 void updateText(int currentItem, PagerAdapter adapter) { 267 final int itemCount = adapter != null ? adapter.getCount() : 0; 268 mUpdatingText = true; 269 270 CharSequence text = null; 271 if (currentItem >= 1 && adapter != null) { 272 text = adapter.getPageTitle(currentItem - 1); 273 } 274 mPrevText.setText(text); 275 276 mCurrText.setText(adapter != null && currentItem < itemCount ? 277 adapter.getPageTitle(currentItem) : null); 278 279 text = null; 280 if (currentItem + 1 < itemCount && adapter != null) { 281 text = adapter.getPageTitle(currentItem + 1); 282 } 283 mNextText.setText(text); 284 285 // Measure everything 286 final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 287 final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom(); 288 final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f), 289 MeasureSpec.AT_MOST); 290 final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); 291 mPrevText.measure(childWidthSpec, childHeightSpec); 292 mCurrText.measure(childWidthSpec, childHeightSpec); 293 mNextText.measure(childWidthSpec, childHeightSpec); 294 295 mLastKnownCurrentPage = currentItem; 296 297 if (!mUpdatingPositions) { 298 updateTextPositions(currentItem, mLastKnownPositionOffset, false); 299 } 300 301 mUpdatingText = false; 302 } 303 304 @Override 305 public void requestLayout() { 306 if (!mUpdatingText) { 307 super.requestLayout(); 308 } 309 } 310 311 void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) { 312 if (oldAdapter != null) { 313 oldAdapter.unregisterDataSetObserver(mPageListener); 314 } 315 if (newAdapter != null) { 316 newAdapter.registerDataSetObserver(mPageListener); 317 } 318 if (mPager != null) { 319 mLastKnownCurrentPage = -1; 320 mLastKnownPositionOffset = -1; 321 updateText(mPager.getCurrentItem(), newAdapter); 322 requestLayout(); 323 } 324 } 325 326 void updateTextPositions(int position, float positionOffset, boolean force) { 327 if (position != mLastKnownCurrentPage) { 328 updateText(position, mPager.getAdapter()); 329 } else if (!force && positionOffset == mLastKnownPositionOffset) { 330 return; 331 } 332 333 mUpdatingPositions = true; 334 335 final int prevWidth = mPrevText.getMeasuredWidth(); 336 final int currWidth = mCurrText.getMeasuredWidth(); 337 final int nextWidth = mNextText.getMeasuredWidth(); 338 final int halfCurrWidth = currWidth / 2; 339 340 final int stripWidth = getWidth(); 341 final int stripHeight = getHeight(); 342 final int paddingLeft = getPaddingLeft(); 343 final int paddingRight = getPaddingRight(); 344 final int paddingTop = getPaddingTop(); 345 final int paddingBottom = getPaddingBottom(); 346 final int textPaddedLeft = paddingLeft + halfCurrWidth; 347 final int textPaddedRight = paddingRight + halfCurrWidth; 348 final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight; 349 350 float currOffset = positionOffset + 0.5f; 351 if (currOffset > 1.f) { 352 currOffset -= 1.f; 353 } 354 final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset); 355 final int currLeft = currCenter - currWidth / 2; 356 final int currRight = currLeft + currWidth; 357 358 final int prevBaseline = mPrevText.getBaseline(); 359 final int currBaseline = mCurrText.getBaseline(); 360 final int nextBaseline = mNextText.getBaseline(); 361 final int maxBaseline = Math.max(Math.max(prevBaseline, currBaseline), nextBaseline); 362 final int prevTopOffset = maxBaseline - prevBaseline; 363 final int currTopOffset = maxBaseline - currBaseline; 364 final int nextTopOffset = maxBaseline - nextBaseline; 365 final int alignedPrevHeight = prevTopOffset + mPrevText.getMeasuredHeight(); 366 final int alignedCurrHeight = currTopOffset + mCurrText.getMeasuredHeight(); 367 final int alignedNextHeight = nextTopOffset + mNextText.getMeasuredHeight(); 368 final int maxTextHeight = Math.max(Math.max(alignedPrevHeight, alignedCurrHeight), 369 alignedNextHeight); 370 371 final int vgrav = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 372 373 int prevTop; 374 int currTop; 375 int nextTop; 376 switch (vgrav) { 377 default: 378 case Gravity.TOP: 379 prevTop = paddingTop + prevTopOffset; 380 currTop = paddingTop + currTopOffset; 381 nextTop = paddingTop + nextTopOffset; 382 break; 383 case Gravity.CENTER_VERTICAL: 384 final int paddedHeight = stripHeight - paddingTop - paddingBottom; 385 final int centeredTop = (paddedHeight - maxTextHeight) / 2; 386 prevTop = centeredTop + prevTopOffset; 387 currTop = centeredTop + currTopOffset; 388 nextTop = centeredTop + nextTopOffset; 389 break; 390 case Gravity.BOTTOM: 391 final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight; 392 prevTop = bottomGravTop + prevTopOffset; 393 currTop = bottomGravTop + currTopOffset; 394 nextTop = bottomGravTop + nextTopOffset; 395 break; 396 } 397 398 mCurrText.layout(currLeft, currTop, currRight, 399 currTop + mCurrText.getMeasuredHeight()); 400 401 final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth); 402 mPrevText.layout(prevLeft, prevTop, prevLeft + prevWidth, 403 prevTop + mPrevText.getMeasuredHeight()); 404 405 final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth, 406 currRight + mScaledTextSpacing); 407 mNextText.layout(nextLeft, nextTop, nextLeft + nextWidth, 408 nextTop + mNextText.getMeasuredHeight()); 409 410 mLastKnownPositionOffset = positionOffset; 411 mUpdatingPositions = false; 412 } 413 414 @Override 415 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 416 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 417 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 418 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 419 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 420 421 if (widthMode != MeasureSpec.EXACTLY) { 422 throw new IllegalStateException("Must measure with an exact width"); 423 } 424 425 int childHeight = heightSize; 426 int minHeight = getMinHeight(); 427 int padding = 0; 428 padding = getPaddingTop() + getPaddingBottom(); 429 childHeight -= padding; 430 431 final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f), 432 MeasureSpec.AT_MOST); 433 final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); 434 435 mPrevText.measure(childWidthSpec, childHeightSpec); 436 mCurrText.measure(childWidthSpec, childHeightSpec); 437 mNextText.measure(childWidthSpec, childHeightSpec); 438 439 if (heightMode == MeasureSpec.EXACTLY) { 440 setMeasuredDimension(widthSize, heightSize); 441 } else { 442 int textHeight = mCurrText.getMeasuredHeight(); 443 setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding)); 444 } 445 } 446 447 @Override 448 protected void onLayout(boolean changed, int l, int t, int r, int b) { 449 if (mPager != null) { 450 final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; 451 updateTextPositions(mLastKnownCurrentPage, offset, true); 452 } 453 } 454 455 int getMinHeight() { 456 int minHeight = 0; 457 final Drawable bg = getBackground(); 458 if (bg != null) { 459 minHeight = bg.getIntrinsicHeight(); 460 } 461 return minHeight; 462 } 463 464 private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener, 465 ViewPager.OnAdapterChangeListener { 466 private int mScrollState; 467 468 @Override 469 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 470 if (positionOffset > 0.5f) { 471 // Consider ourselves to be on the next page when we're 50% of the way there. 472 position++; 473 } 474 updateTextPositions(position, positionOffset, false); 475 } 476 477 @Override 478 public void onPageSelected(int position) { 479 if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { 480 // Only update the text here if we're not dragging or settling. 481 updateText(mPager.getCurrentItem(), mPager.getAdapter()); 482 } 483 } 484 485 @Override 486 public void onPageScrollStateChanged(int state) { 487 mScrollState = state; 488 } 489 490 @Override 491 public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) { 492 updateAdapter(oldAdapter, newAdapter); 493 } 494 495 @Override 496 public void onChanged() { 497 updateText(mPager.getCurrentItem(), mPager.getAdapter()); 498 } 499 } 500} 501