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