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