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