/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v4.view; import android.content.Context; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.TextView; import java.lang.ref.WeakReference; /** * PagerTitleStrip is a non-interactive indicator of the current, next, * and previous pages of a {@link ViewPager}. It is intended to be used as a * child view of a ViewPager widget in your XML layout. * Add it as a child of a ViewPager in your layout file and set its * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom * of the ViewPager. The title from each page is supplied by the method * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to * the ViewPager. * *

For an interactive indicator, see {@link PagerTabStrip}.

*/ public class PagerTitleStrip extends ViewGroup implements ViewPager.Decor { private static final String TAG = "PagerTitleStrip"; ViewPager mPager; TextView mPrevText; TextView mCurrText; TextView mNextText; private int mLastKnownCurrentPage = -1; private float mLastKnownPositionOffset = -1; private int mScaledTextSpacing; private int mGravity; private boolean mUpdatingText; private boolean mUpdatingPositions; private final PageListener mPageListener = new PageListener(); private WeakReference mWatchingAdapter; private static final int[] ATTRS = new int[] { android.R.attr.textAppearance, android.R.attr.textSize, android.R.attr.textColor, android.R.attr.gravity }; private static final int[] TEXT_ATTRS = new int[] { 0x0101038c // android.R.attr.textAllCaps }; private static final float SIDE_ALPHA = 0.6f; private static final int TEXT_SPACING = 16; // dip private int mNonPrimaryAlpha; int mTextColor; interface PagerTitleStripImpl { void setSingleLineAllCaps(TextView text); } static class PagerTitleStripImplBase implements PagerTitleStripImpl { public void setSingleLineAllCaps(TextView text) { text.setSingleLine(); } } static class PagerTitleStripImplIcs implements PagerTitleStripImpl { public void setSingleLineAllCaps(TextView text) { PagerTitleStripIcs.setSingleLineAllCaps(text); } } private static final PagerTitleStripImpl IMPL; static { if (android.os.Build.VERSION.SDK_INT >= 14) { IMPL = new PagerTitleStripImplIcs(); } else { IMPL = new PagerTitleStripImplBase(); } } private static void setSingleLineAllCaps(TextView text) { IMPL.setSingleLineAllCaps(text); } public PagerTitleStrip(Context context) { this(context, null); } public PagerTitleStrip(Context context, AttributeSet attrs) { super(context, attrs); addView(mPrevText = new TextView(context)); addView(mCurrText = new TextView(context)); addView(mNextText = new TextView(context)); final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); final int textAppearance = a.getResourceId(0, 0); if (textAppearance != 0) { mPrevText.setTextAppearance(context, textAppearance); mCurrText.setTextAppearance(context, textAppearance); mNextText.setTextAppearance(context, textAppearance); } final int textSize = a.getDimensionPixelSize(1, 0); if (textSize != 0) { setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); } if (a.hasValue(2)) { final int textColor = a.getColor(2, 0); mPrevText.setTextColor(textColor); mCurrText.setTextColor(textColor); mNextText.setTextColor(textColor); } mGravity = a.getInteger(3, Gravity.BOTTOM); a.recycle(); mTextColor = mCurrText.getTextColors().getDefaultColor(); setNonPrimaryAlpha(SIDE_ALPHA); mPrevText.setEllipsize(TruncateAt.END); mCurrText.setEllipsize(TruncateAt.END); mNextText.setEllipsize(TruncateAt.END); boolean allCaps = false; if (textAppearance != 0) { final TypedArray ta = context.obtainStyledAttributes(textAppearance, TEXT_ATTRS); allCaps = ta.getBoolean(0, false); ta.recycle(); } if (allCaps) { setSingleLineAllCaps(mPrevText); setSingleLineAllCaps(mCurrText); setSingleLineAllCaps(mNextText); } else { mPrevText.setSingleLine(); mCurrText.setSingleLine(); mNextText.setSingleLine(); } final float density = context.getResources().getDisplayMetrics().density; mScaledTextSpacing = (int) (TEXT_SPACING * density); } /** * Set the required spacing between title segments. * * @param spacingPixels Spacing between each title displayed in pixels */ public void setTextSpacing(int spacingPixels) { mScaledTextSpacing = spacingPixels; requestLayout(); } /** * @return The required spacing between title segments in pixels */ public int getTextSpacing() { return mScaledTextSpacing; } /** * Set the alpha value used for non-primary page titles. * * @param alpha Opacity value in the range 0-1f */ public void setNonPrimaryAlpha(@FloatRange(from=0.0, to=1.0) float alpha) { mNonPrimaryAlpha = (int) (alpha * 255) & 0xFF; final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); mPrevText.setTextColor(transparentColor); mNextText.setTextColor(transparentColor); } /** * Set the color value used as the base color for all displayed page titles. * Alpha will be ignored for non-primary page titles. See {@link #setNonPrimaryAlpha(float)}. * * @param color Color hex code in 0xAARRGGBB format */ public void setTextColor(@ColorInt int color) { mTextColor = color; mCurrText.setTextColor(color); final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); mPrevText.setTextColor(transparentColor); mNextText.setTextColor(transparentColor); } /** * Set the default text size to a given unit and value. * See {@link TypedValue} for the possible dimension units. * *

Example: to set the text size to 14px, use * setTextSize(TypedValue.COMPLEX_UNIT_PX, 14);

* * @param unit The desired dimension unit * @param size The desired size in the given units */ public void setTextSize(int unit, float size) { mPrevText.setTextSize(unit, size); mCurrText.setTextSize(unit, size); mNextText.setTextSize(unit, size); } /** * Set the {@link Gravity} used to position text within the title strip. * Only the vertical gravity component is used. * * @param gravity {@link Gravity} constant for positioning title text */ public void setGravity(int gravity) { mGravity = gravity; requestLayout(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); final ViewParent parent = getParent(); if (!(parent instanceof ViewPager)) { throw new IllegalStateException( "PagerTitleStrip must be a direct child of a ViewPager."); } final ViewPager pager = (ViewPager) parent; final PagerAdapter adapter = pager.getAdapter(); pager.setInternalPageChangeListener(mPageListener); pager.setOnAdapterChangeListener(mPageListener); mPager = pager; updateAdapter(mWatchingAdapter != null ? mWatchingAdapter.get() : null, adapter); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mPager != null) { updateAdapter(mPager.getAdapter(), null); mPager.setInternalPageChangeListener(null); mPager.setOnAdapterChangeListener(null); mPager = null; } } void updateText(int currentItem, PagerAdapter adapter) { final int itemCount = adapter != null ? adapter.getCount() : 0; mUpdatingText = true; CharSequence text = null; if (currentItem >= 1 && adapter != null) { text = adapter.getPageTitle(currentItem - 1); } mPrevText.setText(text); mCurrText.setText(adapter != null && currentItem < itemCount ? adapter.getPageTitle(currentItem) : null); text = null; if (currentItem + 1 < itemCount && adapter != null) { text = adapter.getPageTitle(currentItem + 1); } mNextText.setText(text); // Measure everything final int width = getWidth() - getPaddingLeft() - getPaddingRight(); final int maxWidth = Math.max(0, (int) (width * 0.8f)); final int childWidthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST); final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom(); final int maxHeight = Math.max(0, childHeight); final int childHeightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); mPrevText.measure(childWidthSpec, childHeightSpec); mCurrText.measure(childWidthSpec, childHeightSpec); mNextText.measure(childWidthSpec, childHeightSpec); mLastKnownCurrentPage = currentItem; if (!mUpdatingPositions) { updateTextPositions(currentItem, mLastKnownPositionOffset, false); } mUpdatingText = false; } @Override public void requestLayout() { if (!mUpdatingText) { super.requestLayout(); } } void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) { if (oldAdapter != null) { oldAdapter.unregisterDataSetObserver(mPageListener); mWatchingAdapter = null; } if (newAdapter != null) { newAdapter.registerDataSetObserver(mPageListener); mWatchingAdapter = new WeakReference(newAdapter); } if (mPager != null) { mLastKnownCurrentPage = -1; mLastKnownPositionOffset = -1; updateText(mPager.getCurrentItem(), newAdapter); requestLayout(); } } void updateTextPositions(int position, float positionOffset, boolean force) { if (position != mLastKnownCurrentPage) { updateText(position, mPager.getAdapter()); } else if (!force && positionOffset == mLastKnownPositionOffset) { return; } mUpdatingPositions = true; final int prevWidth = mPrevText.getMeasuredWidth(); final int currWidth = mCurrText.getMeasuredWidth(); final int nextWidth = mNextText.getMeasuredWidth(); final int halfCurrWidth = currWidth / 2; final int stripWidth = getWidth(); final int stripHeight = getHeight(); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); final int textPaddedLeft = paddingLeft + halfCurrWidth; final int textPaddedRight = paddingRight + halfCurrWidth; final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight; float currOffset = positionOffset + 0.5f; if (currOffset > 1.f) { currOffset -= 1.f; } final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset); final int currLeft = currCenter - currWidth / 2; final int currRight = currLeft + currWidth; final int prevBaseline = mPrevText.getBaseline(); final int currBaseline = mCurrText.getBaseline(); final int nextBaseline = mNextText.getBaseline(); final int maxBaseline = Math.max(Math.max(prevBaseline, currBaseline), nextBaseline); final int prevTopOffset = maxBaseline - prevBaseline; final int currTopOffset = maxBaseline - currBaseline; final int nextTopOffset = maxBaseline - nextBaseline; final int alignedPrevHeight = prevTopOffset + mPrevText.getMeasuredHeight(); final int alignedCurrHeight = currTopOffset + mCurrText.getMeasuredHeight(); final int alignedNextHeight = nextTopOffset + mNextText.getMeasuredHeight(); final int maxTextHeight = Math.max(Math.max(alignedPrevHeight, alignedCurrHeight), alignedNextHeight); final int vgrav = mGravity & Gravity.VERTICAL_GRAVITY_MASK; int prevTop; int currTop; int nextTop; switch (vgrav) { default: case Gravity.TOP: prevTop = paddingTop + prevTopOffset; currTop = paddingTop + currTopOffset; nextTop = paddingTop + nextTopOffset; break; case Gravity.CENTER_VERTICAL: final int paddedHeight = stripHeight - paddingTop - paddingBottom; final int centeredTop = (paddedHeight - maxTextHeight) / 2; prevTop = centeredTop + prevTopOffset; currTop = centeredTop + currTopOffset; nextTop = centeredTop + nextTopOffset; break; case Gravity.BOTTOM: final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight; prevTop = bottomGravTop + prevTopOffset; currTop = bottomGravTop + currTopOffset; nextTop = bottomGravTop + nextTopOffset; break; } mCurrText.layout(currLeft, currTop, currRight, currTop + mCurrText.getMeasuredHeight()); final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth); mPrevText.layout(prevLeft, prevTop, prevLeft + prevWidth, prevTop + mPrevText.getMeasuredHeight()); final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth, currRight + mScaledTextSpacing); mNextText.layout(nextLeft, nextTop, nextLeft + nextWidth, nextTop + mNextText.getMeasuredHeight()); mLastKnownPositionOffset = positionOffset; mUpdatingPositions = false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("Must measure with an exact width"); } int childHeight = heightSize; int minHeight = getMinHeight(); int padding = 0; padding = getPaddingTop() + getPaddingBottom(); childHeight -= padding; final int maxWidth = Math.max(0, (int) (widthSize * 0.8f)); final int childWidthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST); final int maxHeight = Math.min(0, childHeight); final int childHeightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); mPrevText.measure(childWidthSpec, childHeightSpec); mCurrText.measure(childWidthSpec, childHeightSpec); mNextText.measure(childWidthSpec, childHeightSpec); if (heightMode == MeasureSpec.EXACTLY) { setMeasuredDimension(widthSize, heightSize); } else { int textHeight = mCurrText.getMeasuredHeight(); setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding)); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mPager != null) { final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; updateTextPositions(mLastKnownCurrentPage, offset, true); } } int getMinHeight() { int minHeight = 0; final Drawable bg = getBackground(); if (bg != null) { minHeight = bg.getIntrinsicHeight(); } return minHeight; } private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener, ViewPager.OnAdapterChangeListener { private int mScrollState; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (positionOffset > 0.5f) { // Consider ourselves to be on the next page when we're 50% of the way there. position++; } updateTextPositions(position, positionOffset, false); } @Override public void onPageSelected(int position) { if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { // Only update the text here if we're not dragging or settling. updateText(mPager.getCurrentItem(), mPager.getAdapter()); final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; updateTextPositions(mPager.getCurrentItem(), offset, true); } } @Override public void onPageScrollStateChanged(int state) { mScrollState = state; } @Override public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) { updateAdapter(oldAdapter, newAdapter); } @Override public void onChanged() { updateText(mPager.getCurrentItem(), mPager.getAdapter()); final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; updateTextPositions(mPager.getCurrentItem(), offset, true); } } }