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