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