PagerTitleStrip.java revision 552468d2a1e3cbf74f3a6dcbb68fe079aed56cf9
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.ViewGroup;
27import android.view.ViewParent;
28import android.widget.TextView;
29
30/**
31 * PagerTitleStrip is a non-interactive indicator of the current, next,
32 * and previous pages of a {@link ViewPager}. It is intended to be used as a
33 * child view of a ViewPager widget in your XML layout.
34 * Add it as a child of a ViewPager in your layout file and set its
35 * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom
36 * of the ViewPager. The title from each page is supplied by the method
37 * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to
38 * the ViewPager.
39 */
40public class PagerTitleStrip extends ViewGroup implements ViewPager.Decor {
41    private static final String TAG = "PagerTitleStrip";
42
43    ViewPager mPager;
44    private TextView mPrevText;
45    private TextView mCurrText;
46    private TextView mNextText;
47
48    private int mLastKnownCurrentPage = -1;
49    private float mLastKnownPositionOffset = -1;
50    private int mScaledTextSpacing;
51
52    private boolean mUpdatingText;
53    private boolean mUpdatingPositions;
54
55    private final PageListener mPageListener = new PageListener();
56
57    private static final int[] ATTRS = new int[] {
58        android.R.attr.textAppearance,
59        android.R.attr.textSize,
60        android.R.attr.textColor,
61    };
62
63    private static final float SIDE_ALPHA = 0.6f;
64    private static final int TEXT_SPACING = 16; // dip
65
66    private int mNonPrimaryAlpha;
67    private int mTextColor;
68
69    public PagerTitleStrip(Context context) {
70        this(context, null);
71    }
72
73    public PagerTitleStrip(Context context, AttributeSet attrs) {
74        super(context, attrs);
75
76        addView(mPrevText = new TextView(context));
77        addView(mCurrText = new TextView(context));
78        addView(mNextText = new TextView(context));
79
80        final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
81        final int textAppearance = a.getResourceId(0, 0);
82        if (textAppearance != 0) {
83            mPrevText.setTextAppearance(context, textAppearance);
84            mCurrText.setTextAppearance(context, textAppearance);
85            mNextText.setTextAppearance(context, textAppearance);
86        }
87        if (a.hasValue(1)) {
88            final int textColor = a.getColor(2, 0);
89            mPrevText.setTextColor(textColor);
90            mCurrText.setTextColor(textColor);
91            mNextText.setTextColor(textColor);
92        }
93        final int textSize = a.getDimensionPixelSize(1, 0);
94        if (textSize != 0) {
95            setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
96        }
97        a.recycle();
98
99        mTextColor = mCurrText.getTextColors().getDefaultColor();
100        setNonPrimaryAlpha(SIDE_ALPHA);
101
102        mPrevText.setEllipsize(TruncateAt.END);
103        mCurrText.setEllipsize(TruncateAt.END);
104        mNextText.setEllipsize(TruncateAt.END);
105        mPrevText.setSingleLine();
106        mCurrText.setSingleLine();
107        mNextText.setSingleLine();
108
109        final float density = context.getResources().getDisplayMetrics().density;
110        mScaledTextSpacing = (int) (TEXT_SPACING * density);
111    }
112
113    /**
114     * Set the required spacing between title segments.
115     *
116     * @param spacingPixels Spacing between each title displayed in pixels
117     */
118    public void setTextSpacing(int spacingPixels) {
119        mScaledTextSpacing = spacingPixels;
120        requestLayout();
121    }
122
123    /**
124     * Set the alpha value used for non-primary page titles.
125     *
126     * @param alpha Opacity value in the range 0-1f
127     */
128    public void setNonPrimaryAlpha(float alpha) {
129        mNonPrimaryAlpha = (int) (alpha * 255) & 0xFF;
130        final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF);
131        mPrevText.setTextColor(transparentColor);
132        mNextText.setTextColor(transparentColor);
133    }
134
135    /**
136     * Set the color value used as the base color for all displayed page titles.
137     * Alpha will be ignored for non-primary page titles. See {@link #setNonPrimaryAlpha(float)}.
138     *
139     * @param color Color hex code in 0xAARRGGBB format
140     */
141    public void setTextColor(int color) {
142        mTextColor = color;
143        mCurrText.setTextColor(color);
144        final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF);
145        mPrevText.setTextColor(transparentColor);
146        mNextText.setTextColor(transparentColor);
147    }
148
149    /**
150     * Set the default text size to a given unit and value.
151     * See {@link TypedValue} for the possible dimension units.
152     *
153     * <p>Example: to set the text size to 14px, use
154     * setTextSize(TypedValue.COMPLEX_UNIT_PX, 14);</p>
155     *
156     * @param unit The desired dimension unit
157     * @param size The desired size in the given units
158     */
159    public void setTextSize(int unit, float size) {
160        mPrevText.setTextSize(unit, size);
161        mCurrText.setTextSize(unit, size);
162        mNextText.setTextSize(unit, size);
163    }
164
165    @Override
166    protected void onAttachedToWindow() {
167        super.onAttachedToWindow();
168
169        final ViewParent parent = getParent();
170        if (!(parent instanceof ViewPager)) {
171            throw new IllegalStateException(
172                    "PagerTitleStrip must be a direct child of a ViewPager.");
173        }
174
175        final ViewPager pager = (ViewPager) parent;
176        final PagerAdapter adapter = pager.getAdapter();
177
178        pager.setInternalPageChangeListener(mPageListener);
179        pager.setOnAdapterChangeListener(mPageListener);
180        mPager = pager;
181        updateAdapter(null, adapter);
182    }
183
184    @Override
185    protected void onDetachedFromWindow() {
186        updateAdapter(mPager.getAdapter(), null);
187        mPager.setInternalPageChangeListener(null);
188        mPager.setOnAdapterChangeListener(null);
189        mPager = null;
190    }
191
192    void updateText(int currentItem, PagerAdapter adapter) {
193        final int itemCount = adapter != null ? adapter.getCount() : 0;
194        mUpdatingText = true;
195
196        CharSequence text = null;
197        if (currentItem >= 1 && adapter != null) {
198            text = adapter.getPageTitle(currentItem - 1);
199        }
200        mPrevText.setText(text);
201
202        mCurrText.setText(adapter != null ? adapter.getPageTitle(currentItem) : null);
203
204        text = null;
205        if (currentItem + 1 < itemCount && adapter != null) {
206            text = adapter.getPageTitle(currentItem + 1);
207        }
208        mNextText.setText(text);
209
210        // Measure everything
211        final int width = getWidth() - getPaddingLeft() - getPaddingRight();
212        final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom();
213        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f),
214                MeasureSpec.AT_MOST);
215        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
216        mPrevText.measure(childWidthSpec, childHeightSpec);
217        mCurrText.measure(childWidthSpec, childHeightSpec);
218        mNextText.measure(childWidthSpec, childHeightSpec);
219
220        mLastKnownCurrentPage = currentItem;
221
222        if (!mUpdatingPositions) {
223            updateTextPositions(currentItem, mLastKnownPositionOffset);
224        }
225
226        mUpdatingText = false;
227    }
228
229    @Override
230    public void requestLayout() {
231        if (!mUpdatingText) {
232            super.requestLayout();
233        }
234    }
235
236    void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
237        if (oldAdapter != null) {
238            oldAdapter.unregisterDataSetObserver(mPageListener);
239        }
240        if (newAdapter != null) {
241            newAdapter.registerDataSetObserver(mPageListener);
242        }
243        if (mPager != null) {
244            mLastKnownCurrentPage = -1;
245            mLastKnownPositionOffset = -1;
246            updateText(mPager.getCurrentItem(), newAdapter);
247            requestLayout();
248        }
249    }
250
251    void updateTextPositions(int position, float positionOffset) {
252        if (position != mLastKnownCurrentPage) {
253            updateText(position, mPager.getAdapter());
254        } else if (positionOffset == mLastKnownPositionOffset) {
255            return;
256        }
257
258        mUpdatingPositions = true;
259
260        final int prevWidth = mPrevText.getMeasuredWidth();
261        final int currWidth = mCurrText.getMeasuredWidth();
262        final int nextWidth = mNextText.getMeasuredWidth();
263        final int halfCurrWidth = currWidth / 2;
264
265        final int stripWidth = getWidth();
266        final int paddingLeft = getPaddingLeft();
267        final int paddingRight = getPaddingRight();
268        final int paddingTop = getPaddingTop();
269        final int textPaddedLeft = paddingLeft + halfCurrWidth;
270        final int textPaddedRight = paddingRight + halfCurrWidth;
271        final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight;
272
273        float currOffset = positionOffset + 0.5f;
274        if (currOffset > 1.f) {
275            currOffset -= 1.f;
276        }
277        final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset);
278        final int currLeft = currCenter - currWidth / 2;
279        final int currRight = currLeft + currWidth;
280
281        mCurrText.layout(currLeft, paddingTop, currRight,
282                paddingTop + mCurrText.getMeasuredHeight());
283
284        final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth);
285        mPrevText.layout(prevLeft, paddingTop, prevLeft + prevWidth,
286                paddingTop + mPrevText.getMeasuredHeight());
287
288        final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth,
289                currRight + mScaledTextSpacing);
290        mNextText.layout(nextLeft, paddingTop, nextLeft + nextWidth,
291                paddingTop + mNextText.getMeasuredHeight());
292
293        mLastKnownPositionOffset = positionOffset;
294        mUpdatingPositions = false;
295    }
296
297    @Override
298    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
299        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
300        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
301        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
302        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
303
304        if (widthMode != MeasureSpec.EXACTLY) {
305            throw new IllegalStateException("Must measure with an exact width");
306        }
307
308        int childHeight = heightSize;
309        int minHeight = 0;
310        int padding = 0;
311        final Drawable bg = getBackground();
312        if (bg != null) {
313            minHeight = bg.getIntrinsicHeight();
314        }
315        padding = getPaddingTop() + getPaddingBottom();
316        childHeight -= padding;
317
318        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f),
319                MeasureSpec.AT_MOST);
320        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, heightMode);
321
322        mPrevText.measure(childWidthSpec, childHeightSpec);
323        mCurrText.measure(childWidthSpec, childHeightSpec);
324        mNextText.measure(childWidthSpec, childHeightSpec);
325
326        if (heightMode == MeasureSpec.EXACTLY) {
327            setMeasuredDimension(widthSize, heightSize);
328        } else {
329            int textHeight = mCurrText.getMeasuredHeight();
330            setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding));
331        }
332    }
333
334    @Override
335    protected void onLayout(boolean changed, int l, int t, int r, int b) {
336        if (mPager != null) {
337            updateTextPositions(mPager.getCurrentItem(), 0.f);
338        }
339    }
340
341    private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener,
342            ViewPager.OnAdapterChangeListener {
343        private int mScrollState;
344
345        @Override
346        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
347            if (positionOffset > 0.5f) {
348                // Consider ourselves to be on the next page when we're 50% of the way there.
349                position++;
350            }
351            updateTextPositions(position, positionOffset);
352        }
353
354        @Override
355        public void onPageSelected(int position) {
356            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
357                // Only update the text here if we're not dragging or settling.
358                updateText(mPager.getCurrentItem(), mPager.getAdapter());
359            }
360        }
361
362        @Override
363        public void onPageScrollStateChanged(int state) {
364            mScrollState = state;
365        }
366
367        @Override
368        public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
369            updateAdapter(oldAdapter, newAdapter);
370        }
371
372        @Override
373        public void onChanged() {
374            updateText(mPager.getCurrentItem(), mPager.getAdapter());
375        }
376    }
377}
378