PagerTitleStrip.java revision 8fffe01871be1806a1bdefa1f7213b660fcf5ac0
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 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 {
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 mFirstLayout = true;
53    private boolean mUpdatingText;
54    private boolean mUpdatingPositions;
55
56    private final PageListener mPageListener = new PageListener();
57
58    private static final int[] ATTRS = new int[] {
59        android.R.attr.textAppearance,
60        android.R.attr.textColor,
61        android.R.attr.textSize
62    };
63
64    private static final int SIDE_ALPHA = 0x99; // single-byte alpha, 0 = invisible, FF = opaque
65    private static final int TEXT_SPACING = 16; // dip
66
67    public PagerTitleStrip(Context context) {
68        this(context, null);
69    }
70
71    public PagerTitleStrip(Context context, AttributeSet attrs) {
72        super(context, attrs);
73
74        addView(mPrevText = new TextView(context));
75        addView(mCurrText = new TextView(context));
76        addView(mNextText = new TextView(context));
77
78        final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
79        final int textAppearance = a.getResourceId(0, 0);
80        if (textAppearance != 0) {
81            mPrevText.setTextAppearance(context, textAppearance);
82            mCurrText.setTextAppearance(context, textAppearance);
83            mNextText.setTextAppearance(context, textAppearance);
84        }
85        if (a.hasValue(1)) {
86            final int textColor = a.getColor(1, 0);
87            mPrevText.setTextColor(textColor);
88            mCurrText.setTextColor(textColor);
89            mNextText.setTextColor(textColor);
90        }
91        final int textSize = a.getDimensionPixelSize(2, 0);
92        if (textSize != 0) {
93            mPrevText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
94            mCurrText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
95            mNextText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
96        }
97        a.recycle();
98
99        final int defaultColor = mPrevText.getTextColors().getDefaultColor();
100        final int transparentColor = (SIDE_ALPHA << 24) | (defaultColor & 0xFFFFFF);
101        mPrevText.setTextColor(transparentColor);
102        mNextText.setTextColor(transparentColor);
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    @Override
116    protected void onAttachedToWindow() {
117        super.onAttachedToWindow();
118
119        final ViewParent parent = getParent();
120        if (!(parent instanceof ViewPager)) {
121            throw new IllegalStateException(
122                    "PagerTitleStrip must be a direct child of a ViewPager.");
123        }
124
125        final ViewPager pager = (ViewPager) parent;
126        final PagerAdapter adapter = pager.getAdapter();
127
128        pager.setInternalPageChangeListener(mPageListener);
129        pager.setOnAdapterChangeListener(mPageListener);
130        mPager = pager;
131        updateAdapter(null, adapter);
132    }
133
134    @Override
135    protected void onDetachedFromWindow() {
136        updateAdapter(mPager.getAdapter(), null);
137        mPager.setInternalPageChangeListener(null);
138        mPager.setOnAdapterChangeListener(null);
139        mPager = null;
140    }
141
142    void updateText(int currentItem, PagerAdapter adapter) {
143        final int itemCount = adapter != null ? adapter.getCount() : 0;
144        mUpdatingText = true;
145
146        CharSequence text = null;
147        if (currentItem >= 1 && adapter != null) {
148            text = adapter.getPageTitle(currentItem - 1);
149        }
150        mPrevText.setText(text);
151
152        mCurrText.setText(adapter != null ? adapter.getPageTitle(currentItem) : null);
153
154        text = null;
155        if (currentItem + 1 < itemCount && adapter != null) {
156            text = adapter.getPageTitle(currentItem + 1);
157        }
158        mNextText.setText(text);
159
160        // Measure everything
161        final int width = getWidth() - getPaddingLeft() - getPaddingRight();
162        final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom();
163        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f),
164                MeasureSpec.AT_MOST);
165        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
166        mPrevText.measure(childWidthSpec, childHeightSpec);
167        mCurrText.measure(childWidthSpec, childHeightSpec);
168        mNextText.measure(childWidthSpec, childHeightSpec);
169
170        mLastKnownCurrentPage = currentItem;
171
172        if (!mUpdatingPositions) {
173            updateTextPositions(currentItem, mLastKnownPositionOffset);
174        }
175
176        mUpdatingText = false;
177    }
178
179    @Override
180    public void requestLayout() {
181        if (!mUpdatingText) {
182            super.requestLayout();
183        }
184    }
185
186    void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
187        if (oldAdapter != null) {
188            oldAdapter.unregisterDataSetObserver(mPageListener);
189        }
190        if (newAdapter != null) {
191            newAdapter.registerDataSetObserver(mPageListener);
192        }
193        if (mPager != null) {
194            mLastKnownCurrentPage = -1;
195            mLastKnownPositionOffset = -1;
196            updateText(mPager.getCurrentItem(), newAdapter);
197        }
198    }
199
200    void updateTextPositions(int position, float positionOffset) {
201        if (position != mLastKnownCurrentPage) {
202            updateText(position, mPager.getAdapter());
203        } else if (positionOffset == mLastKnownPositionOffset) {
204            return;
205        }
206
207        mUpdatingPositions = true;
208
209        final int prevWidth = mPrevText.getMeasuredWidth();
210        final int currWidth = mCurrText.getMeasuredWidth();
211        final int nextWidth = mNextText.getMeasuredWidth();
212        final int halfCurrWidth = currWidth / 2;
213
214        final int stripWidth = getWidth();
215        final int paddingLeft = getPaddingLeft();
216        final int paddingRight = getPaddingRight();
217        final int paddingTop = getPaddingTop();
218        final int textPaddedLeft = paddingLeft + halfCurrWidth;
219        final int textPaddedRight = paddingRight + halfCurrWidth;
220        final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight;
221
222        float currOffset = positionOffset + 0.5f;
223        if (currOffset > 1.f) {
224            currOffset -= 1.f;
225        }
226        final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset);
227        final int currLeft = currCenter - currWidth / 2;
228        final int currRight = currLeft + currWidth;
229
230        mCurrText.layout(currLeft, paddingTop, currRight,
231                paddingTop + mCurrText.getMeasuredHeight());
232
233        final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth);
234        mPrevText.layout(prevLeft, paddingTop, prevLeft + prevWidth,
235                paddingTop + mPrevText.getMeasuredHeight());
236
237        final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth,
238                currRight + mScaledTextSpacing);
239        mNextText.layout(nextLeft, paddingTop, nextLeft + nextWidth,
240                paddingTop + mNextText.getMeasuredHeight());
241
242        mLastKnownPositionOffset = positionOffset;
243        mUpdatingPositions = false;
244    }
245
246    @Override
247    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
248        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
249        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
250        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
251        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
252
253        if (widthMode != MeasureSpec.EXACTLY) {
254            throw new IllegalStateException("Must measure with an exact width");
255        }
256
257        int childHeight = heightSize;
258        int minHeight = 0;
259        int padding = 0;
260        final Drawable bg = getBackground();
261        if (bg != null) {
262            minHeight = bg.getIntrinsicHeight();
263        }
264        padding = getPaddingTop() + getPaddingBottom();
265        childHeight -= padding;
266
267        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f),
268                MeasureSpec.AT_MOST);
269        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, heightMode);
270
271        mPrevText.measure(childWidthSpec, childHeightSpec);
272        mCurrText.measure(childWidthSpec, childHeightSpec);
273        mNextText.measure(childWidthSpec, childHeightSpec);
274
275        if (heightMode == MeasureSpec.EXACTLY) {
276            setMeasuredDimension(widthSize, heightSize);
277        } else {
278            int textHeight = mCurrText.getMeasuredHeight();
279            setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding));
280        }
281    }
282
283    @Override
284    protected void onLayout(boolean changed, int l, int t, int r, int b) {
285        if (mPager != null) {
286            updateTextPositions(mPager.getCurrentItem(), 0.f);
287        }
288    }
289
290    private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener,
291            ViewPager.OnAdapterChangeListener {
292        private int mScrollState;
293
294        @Override
295        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
296            if (positionOffset > 0.5f) {
297                // Consider ourselves to be on the next page when we're 50% of the way there.
298                position++;
299            }
300            updateTextPositions(position, positionOffset);
301        }
302
303        @Override
304        public void onPageSelected(int position) {
305            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
306                // Only update the text here if we're not dragging or settling.
307                updateText(mPager.getCurrentItem(), mPager.getAdapter());
308            }
309        }
310
311        @Override
312        public void onPageScrollStateChanged(int state) {
313            mScrollState = state;
314        }
315
316        @Override
317        public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
318            updateAdapter(oldAdapter, newAdapter);
319        }
320
321        @Override
322        public void onChanged() {
323            updateText(mPager.getCurrentItem(), mPager.getAdapter());
324        }
325    }
326}
327