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