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