PagerTitleStrip.java revision 23b42ec742c2047d6bb9b364c9609e6e0af13b9d
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        updateAdapter(mPager.getAdapter(), null);
202        mPager.setInternalPageChangeListener(null);
203        mPager.setOnAdapterChangeListener(null);
204        mPager = null;
205    }
206
207    void updateText(int currentItem, PagerAdapter adapter) {
208        final int itemCount = adapter != null ? adapter.getCount() : 0;
209        mUpdatingText = true;
210
211        CharSequence text = null;
212        if (currentItem >= 1 && adapter != null) {
213            text = adapter.getPageTitle(currentItem - 1);
214        }
215        mPrevText.setText(text);
216
217        mCurrText.setText(adapter != null ? adapter.getPageTitle(currentItem) : null);
218
219        text = null;
220        if (currentItem + 1 < itemCount && adapter != null) {
221            text = adapter.getPageTitle(currentItem + 1);
222        }
223        mNextText.setText(text);
224
225        // Measure everything
226        final int width = getWidth() - getPaddingLeft() - getPaddingRight();
227        final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom();
228        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f),
229                MeasureSpec.AT_MOST);
230        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST);
231        mPrevText.measure(childWidthSpec, childHeightSpec);
232        mCurrText.measure(childWidthSpec, childHeightSpec);
233        mNextText.measure(childWidthSpec, childHeightSpec);
234
235        mLastKnownCurrentPage = currentItem;
236
237        if (!mUpdatingPositions) {
238            updateTextPositions(currentItem, mLastKnownPositionOffset);
239        }
240
241        mUpdatingText = false;
242    }
243
244    @Override
245    public void requestLayout() {
246        if (!mUpdatingText) {
247            super.requestLayout();
248        }
249    }
250
251    void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
252        if (oldAdapter != null) {
253            oldAdapter.unregisterDataSetObserver(mPageListener);
254        }
255        if (newAdapter != null) {
256            newAdapter.registerDataSetObserver(mPageListener);
257        }
258        if (mPager != null) {
259            mLastKnownCurrentPage = -1;
260            mLastKnownPositionOffset = -1;
261            updateText(mPager.getCurrentItem(), newAdapter);
262            requestLayout();
263        }
264    }
265
266    void updateTextPositions(int position, float positionOffset) {
267        if (position != mLastKnownCurrentPage) {
268            updateText(position, mPager.getAdapter());
269        } else if (positionOffset == mLastKnownPositionOffset) {
270            return;
271        }
272
273        mUpdatingPositions = true;
274
275        final int prevWidth = mPrevText.getMeasuredWidth();
276        final int currWidth = mCurrText.getMeasuredWidth();
277        final int nextWidth = mNextText.getMeasuredWidth();
278        final int halfCurrWidth = currWidth / 2;
279
280        final int stripWidth = getWidth();
281        final int stripHeight = getHeight();
282        final int paddingLeft = getPaddingLeft();
283        final int paddingRight = getPaddingRight();
284        final int paddingTop = getPaddingTop();
285        final int paddingBottom = getPaddingBottom();
286        final int textPaddedLeft = paddingLeft + halfCurrWidth;
287        final int textPaddedRight = paddingRight + halfCurrWidth;
288        final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight;
289
290        float currOffset = positionOffset + 0.5f;
291        if (currOffset > 1.f) {
292            currOffset -= 1.f;
293        }
294        final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset);
295        final int currLeft = currCenter - currWidth / 2;
296        final int currRight = currLeft + currWidth;
297
298        final int prevBaseline = mPrevText.getBaseline();
299        final int currBaseline = mCurrText.getBaseline();
300        final int nextBaseline = mNextText.getBaseline();
301        final int maxBaseline = Math.max(Math.max(prevBaseline, currBaseline), nextBaseline);
302        final int prevTopOffset = maxBaseline - prevBaseline;
303        final int currTopOffset = maxBaseline - currBaseline;
304        final int nextTopOffset = maxBaseline - nextBaseline;
305        final int alignedPrevHeight = prevTopOffset + mPrevText.getMeasuredHeight();
306        final int alignedCurrHeight = currTopOffset + mCurrText.getMeasuredHeight();
307        final int alignedNextHeight = nextTopOffset + mNextText.getMeasuredHeight();
308        final int maxTextHeight = Math.max(Math.max(alignedPrevHeight, alignedCurrHeight),
309                alignedNextHeight);
310
311        final int vgrav = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
312
313        int prevTop;
314        int currTop;
315        int nextTop;
316        switch (vgrav) {
317            default:
318            case Gravity.TOP:
319                prevTop = paddingTop + prevTopOffset;
320                currTop = paddingTop + currTopOffset;
321                nextTop = paddingTop + nextTopOffset;
322                break;
323            case Gravity.CENTER_VERTICAL:
324                final int paddedHeight = stripHeight - paddingTop - paddingBottom;
325                final int centeredTop = (paddedHeight - maxTextHeight) / 2;
326                prevTop = centeredTop + prevTopOffset;
327                currTop = centeredTop + currTopOffset;
328                nextTop = centeredTop + nextTopOffset;
329                break;
330            case Gravity.BOTTOM:
331                final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight;
332                prevTop = bottomGravTop + prevTopOffset;
333                currTop = bottomGravTop + currTopOffset;
334                nextTop = bottomGravTop + nextTopOffset;
335                break;
336        }
337
338        mCurrText.layout(currLeft, currTop, currRight,
339                currTop + mCurrText.getMeasuredHeight());
340
341        final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth);
342        mPrevText.layout(prevLeft, prevTop, prevLeft + prevWidth,
343                prevTop + mPrevText.getMeasuredHeight());
344
345        final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth,
346                currRight + mScaledTextSpacing);
347        mNextText.layout(nextLeft, nextTop, nextLeft + nextWidth,
348                nextTop + mNextText.getMeasuredHeight());
349
350        mLastKnownPositionOffset = positionOffset;
351        mUpdatingPositions = false;
352    }
353
354    @Override
355    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
356        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
357        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
358        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
359        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
360
361        if (widthMode != MeasureSpec.EXACTLY) {
362            throw new IllegalStateException("Must measure with an exact width");
363        }
364
365        int childHeight = heightSize;
366        int minHeight = 0;
367        int padding = 0;
368        final Drawable bg = getBackground();
369        if (bg != null) {
370            minHeight = bg.getIntrinsicHeight();
371        }
372        padding = getPaddingTop() + getPaddingBottom();
373        childHeight -= padding;
374
375        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f),
376                MeasureSpec.AT_MOST);
377        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST);
378
379        mPrevText.measure(childWidthSpec, childHeightSpec);
380        mCurrText.measure(childWidthSpec, childHeightSpec);
381        mNextText.measure(childWidthSpec, childHeightSpec);
382
383        if (heightMode == MeasureSpec.EXACTLY) {
384            setMeasuredDimension(widthSize, heightSize);
385        } else {
386            int textHeight = mCurrText.getMeasuredHeight();
387            setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding));
388        }
389    }
390
391    @Override
392    protected void onLayout(boolean changed, int l, int t, int r, int b) {
393        if (mPager != null) {
394            updateTextPositions(mPager.getCurrentItem(), 0.f);
395        }
396    }
397
398    private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener,
399            ViewPager.OnAdapterChangeListener {
400        private int mScrollState;
401
402        @Override
403        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
404            if (positionOffset > 0.5f) {
405                // Consider ourselves to be on the next page when we're 50% of the way there.
406                position++;
407            }
408            updateTextPositions(position, positionOffset);
409        }
410
411        @Override
412        public void onPageSelected(int position) {
413            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
414                // Only update the text here if we're not dragging or settling.
415                updateText(mPager.getCurrentItem(), mPager.getAdapter());
416            }
417        }
418
419        @Override
420        public void onPageScrollStateChanged(int state) {
421            mScrollState = state;
422        }
423
424        @Override
425        public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
426            updateAdapter(oldAdapter, newAdapter);
427        }
428
429        @Override
430        public void onChanged() {
431            updateText(mPager.getCurrentItem(), mPager.getAdapter());
432        }
433    }
434}
435