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