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