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
31import java.lang.ref.WeakReference;
32
33/**
34 * PagerTitleStrip is a non-interactive indicator of the current, next,
35 * and previous pages of a {@link ViewPager}. It is intended to be used as a
36 * child view of a ViewPager widget in your XML layout.
37 * Add it as a child of a ViewPager in your layout file and set its
38 * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom
39 * of the ViewPager. The title from each page is supplied by the method
40 * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to
41 * the ViewPager.
42 *
43 * <p>For an interactive indicator, see {@link PagerTabStrip}.</p>
44 */
45public class PagerTitleStrip extends ViewGroup implements ViewPager.Decor {
46    private static final String TAG = "PagerTitleStrip";
47
48    ViewPager mPager;
49    TextView mPrevText;
50    TextView mCurrText;
51    TextView mNextText;
52
53    private int mLastKnownCurrentPage = -1;
54    private float mLastKnownPositionOffset = -1;
55    private int mScaledTextSpacing;
56    private int mGravity;
57
58    private boolean mUpdatingText;
59    private boolean mUpdatingPositions;
60
61    private final PageListener mPageListener = new PageListener();
62
63    private WeakReference<PagerAdapter> mWatchingAdapter;
64
65    private static final int[] ATTRS = new int[] {
66        android.R.attr.textAppearance,
67        android.R.attr.textSize,
68        android.R.attr.textColor,
69        android.R.attr.gravity
70    };
71
72    private static final int[] TEXT_ATTRS = new int[] {
73        0x0101038c // android.R.attr.textAllCaps
74    };
75
76    private static final float SIDE_ALPHA = 0.6f;
77    private static final int TEXT_SPACING = 16; // dip
78
79    private int mNonPrimaryAlpha;
80    int mTextColor;
81
82    interface PagerTitleStripImpl {
83        void setSingleLineAllCaps(TextView text);
84    }
85
86    static class PagerTitleStripImplBase implements PagerTitleStripImpl {
87        public void setSingleLineAllCaps(TextView text) {
88            text.setSingleLine();
89        }
90    }
91
92    static class PagerTitleStripImplIcs implements PagerTitleStripImpl {
93        public void setSingleLineAllCaps(TextView text) {
94            PagerTitleStripIcs.setSingleLineAllCaps(text);
95        }
96    }
97
98    private static final PagerTitleStripImpl IMPL;
99    static {
100        if (android.os.Build.VERSION.SDK_INT >= 14) {
101            IMPL = new PagerTitleStripImplIcs();
102        } else {
103            IMPL = new PagerTitleStripImplBase();
104        }
105    }
106
107    private static void setSingleLineAllCaps(TextView text) {
108        IMPL.setSingleLineAllCaps(text);
109    }
110
111    public PagerTitleStrip(Context context) {
112        this(context, null);
113    }
114
115    public PagerTitleStrip(Context context, AttributeSet attrs) {
116        super(context, attrs);
117
118        addView(mPrevText = new TextView(context));
119        addView(mCurrText = new TextView(context));
120        addView(mNextText = new TextView(context));
121
122        final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
123        final int textAppearance = a.getResourceId(0, 0);
124        if (textAppearance != 0) {
125            mPrevText.setTextAppearance(context, textAppearance);
126            mCurrText.setTextAppearance(context, textAppearance);
127            mNextText.setTextAppearance(context, textAppearance);
128        }
129        final int textSize = a.getDimensionPixelSize(1, 0);
130        if (textSize != 0) {
131            setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
132        }
133        if (a.hasValue(2)) {
134            final int textColor = a.getColor(2, 0);
135            mPrevText.setTextColor(textColor);
136            mCurrText.setTextColor(textColor);
137            mNextText.setTextColor(textColor);
138        }
139        mGravity = a.getInteger(3, Gravity.BOTTOM);
140        a.recycle();
141
142        mTextColor = mCurrText.getTextColors().getDefaultColor();
143        setNonPrimaryAlpha(SIDE_ALPHA);
144
145        mPrevText.setEllipsize(TruncateAt.END);
146        mCurrText.setEllipsize(TruncateAt.END);
147        mNextText.setEllipsize(TruncateAt.END);
148
149        boolean allCaps = false;
150        if (textAppearance != 0) {
151            final TypedArray ta = context.obtainStyledAttributes(textAppearance, TEXT_ATTRS);
152            allCaps = ta.getBoolean(0, false);
153            ta.recycle();
154        }
155
156        if (allCaps) {
157            setSingleLineAllCaps(mPrevText);
158            setSingleLineAllCaps(mCurrText);
159            setSingleLineAllCaps(mNextText);
160        } else {
161            mPrevText.setSingleLine();
162            mCurrText.setSingleLine();
163            mNextText.setSingleLine();
164        }
165
166        final float density = context.getResources().getDisplayMetrics().density;
167        mScaledTextSpacing = (int) (TEXT_SPACING * density);
168    }
169
170    /**
171     * Set the required spacing between title segments.
172     *
173     * @param spacingPixels Spacing between each title displayed in pixels
174     */
175    public void setTextSpacing(int spacingPixels) {
176        mScaledTextSpacing = spacingPixels;
177        requestLayout();
178    }
179
180    /**
181     * @return The required spacing between title segments in pixels
182     */
183    public int getTextSpacing() {
184        return mScaledTextSpacing;
185    }
186
187    /**
188     * Set the alpha value used for non-primary page titles.
189     *
190     * @param alpha Opacity value in the range 0-1f
191     */
192    public void setNonPrimaryAlpha(float alpha) {
193        mNonPrimaryAlpha = (int) (alpha * 255) & 0xFF;
194        final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF);
195        mPrevText.setTextColor(transparentColor);
196        mNextText.setTextColor(transparentColor);
197    }
198
199    /**
200     * Set the color value used as the base color for all displayed page titles.
201     * Alpha will be ignored for non-primary page titles. See {@link #setNonPrimaryAlpha(float)}.
202     *
203     * @param color Color hex code in 0xAARRGGBB format
204     */
205    public void setTextColor(int color) {
206        mTextColor = color;
207        mCurrText.setTextColor(color);
208        final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF);
209        mPrevText.setTextColor(transparentColor);
210        mNextText.setTextColor(transparentColor);
211    }
212
213    /**
214     * Set the default text size to a given unit and value.
215     * See {@link TypedValue} for the possible dimension units.
216     *
217     * <p>Example: to set the text size to 14px, use
218     * setTextSize(TypedValue.COMPLEX_UNIT_PX, 14);</p>
219     *
220     * @param unit The desired dimension unit
221     * @param size The desired size in the given units
222     */
223    public void setTextSize(int unit, float size) {
224        mPrevText.setTextSize(unit, size);
225        mCurrText.setTextSize(unit, size);
226        mNextText.setTextSize(unit, size);
227    }
228
229    /**
230     * Set the {@link Gravity} used to position text within the title strip.
231     * Only the vertical gravity component is used.
232     *
233     * @param gravity {@link Gravity} constant for positioning title text
234     */
235    public void setGravity(int gravity) {
236        mGravity = gravity;
237        requestLayout();
238    }
239
240    @Override
241    protected void onAttachedToWindow() {
242        super.onAttachedToWindow();
243
244        final ViewParent parent = getParent();
245        if (!(parent instanceof ViewPager)) {
246            throw new IllegalStateException(
247                    "PagerTitleStrip must be a direct child of a ViewPager.");
248        }
249
250        final ViewPager pager = (ViewPager) parent;
251        final PagerAdapter adapter = pager.getAdapter();
252
253        pager.setInternalPageChangeListener(mPageListener);
254        pager.setOnAdapterChangeListener(mPageListener);
255        mPager = pager;
256        updateAdapter(mWatchingAdapter != null ? mWatchingAdapter.get() : null, adapter);
257    }
258
259    @Override
260    protected void onDetachedFromWindow() {
261        super.onDetachedFromWindow();
262        if (mPager != null) {
263            updateAdapter(mPager.getAdapter(), null);
264            mPager.setInternalPageChangeListener(null);
265            mPager.setOnAdapterChangeListener(null);
266            mPager = null;
267        }
268    }
269
270    void updateText(int currentItem, PagerAdapter adapter) {
271        final int itemCount = adapter != null ? adapter.getCount() : 0;
272        mUpdatingText = true;
273
274        CharSequence text = null;
275        if (currentItem >= 1 && adapter != null) {
276            text = adapter.getPageTitle(currentItem - 1);
277        }
278        mPrevText.setText(text);
279
280        mCurrText.setText(adapter != null && currentItem < itemCount ?
281                adapter.getPageTitle(currentItem) : null);
282
283        text = null;
284        if (currentItem + 1 < itemCount && adapter != null) {
285            text = adapter.getPageTitle(currentItem + 1);
286        }
287        mNextText.setText(text);
288
289        // Measure everything
290        final int width = getWidth() - getPaddingLeft() - getPaddingRight();
291        final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom();
292        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f),
293                MeasureSpec.AT_MOST);
294        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST);
295        mPrevText.measure(childWidthSpec, childHeightSpec);
296        mCurrText.measure(childWidthSpec, childHeightSpec);
297        mNextText.measure(childWidthSpec, childHeightSpec);
298
299        mLastKnownCurrentPage = currentItem;
300
301        if (!mUpdatingPositions) {
302            updateTextPositions(currentItem, mLastKnownPositionOffset, false);
303        }
304
305        mUpdatingText = false;
306    }
307
308    @Override
309    public void requestLayout() {
310        if (!mUpdatingText) {
311            super.requestLayout();
312        }
313    }
314
315    void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
316        if (oldAdapter != null) {
317            oldAdapter.unregisterDataSetObserver(mPageListener);
318            mWatchingAdapter = null;
319        }
320        if (newAdapter != null) {
321            newAdapter.registerDataSetObserver(mPageListener);
322            mWatchingAdapter = new WeakReference<PagerAdapter>(newAdapter);
323        }
324        if (mPager != null) {
325            mLastKnownCurrentPage = -1;
326            mLastKnownPositionOffset = -1;
327            updateText(mPager.getCurrentItem(), newAdapter);
328            requestLayout();
329        }
330    }
331
332    void updateTextPositions(int position, float positionOffset, boolean force) {
333        if (position != mLastKnownCurrentPage) {
334            updateText(position, mPager.getAdapter());
335        } else if (!force && positionOffset == mLastKnownPositionOffset) {
336            return;
337        }
338
339        mUpdatingPositions = true;
340
341        final int prevWidth = mPrevText.getMeasuredWidth();
342        final int currWidth = mCurrText.getMeasuredWidth();
343        final int nextWidth = mNextText.getMeasuredWidth();
344        final int halfCurrWidth = currWidth / 2;
345
346        final int stripWidth = getWidth();
347        final int stripHeight = getHeight();
348        final int paddingLeft = getPaddingLeft();
349        final int paddingRight = getPaddingRight();
350        final int paddingTop = getPaddingTop();
351        final int paddingBottom = getPaddingBottom();
352        final int textPaddedLeft = paddingLeft + halfCurrWidth;
353        final int textPaddedRight = paddingRight + halfCurrWidth;
354        final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight;
355
356        float currOffset = positionOffset + 0.5f;
357        if (currOffset > 1.f) {
358            currOffset -= 1.f;
359        }
360        final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset);
361        final int currLeft = currCenter - currWidth / 2;
362        final int currRight = currLeft + currWidth;
363
364        final int prevBaseline = mPrevText.getBaseline();
365        final int currBaseline = mCurrText.getBaseline();
366        final int nextBaseline = mNextText.getBaseline();
367        final int maxBaseline = Math.max(Math.max(prevBaseline, currBaseline), nextBaseline);
368        final int prevTopOffset = maxBaseline - prevBaseline;
369        final int currTopOffset = maxBaseline - currBaseline;
370        final int nextTopOffset = maxBaseline - nextBaseline;
371        final int alignedPrevHeight = prevTopOffset + mPrevText.getMeasuredHeight();
372        final int alignedCurrHeight = currTopOffset + mCurrText.getMeasuredHeight();
373        final int alignedNextHeight = nextTopOffset + mNextText.getMeasuredHeight();
374        final int maxTextHeight = Math.max(Math.max(alignedPrevHeight, alignedCurrHeight),
375                alignedNextHeight);
376
377        final int vgrav = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
378
379        int prevTop;
380        int currTop;
381        int nextTop;
382        switch (vgrav) {
383            default:
384            case Gravity.TOP:
385                prevTop = paddingTop + prevTopOffset;
386                currTop = paddingTop + currTopOffset;
387                nextTop = paddingTop + nextTopOffset;
388                break;
389            case Gravity.CENTER_VERTICAL:
390                final int paddedHeight = stripHeight - paddingTop - paddingBottom;
391                final int centeredTop = (paddedHeight - maxTextHeight) / 2;
392                prevTop = centeredTop + prevTopOffset;
393                currTop = centeredTop + currTopOffset;
394                nextTop = centeredTop + nextTopOffset;
395                break;
396            case Gravity.BOTTOM:
397                final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight;
398                prevTop = bottomGravTop + prevTopOffset;
399                currTop = bottomGravTop + currTopOffset;
400                nextTop = bottomGravTop + nextTopOffset;
401                break;
402        }
403
404        mCurrText.layout(currLeft, currTop, currRight,
405                currTop + mCurrText.getMeasuredHeight());
406
407        final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth);
408        mPrevText.layout(prevLeft, prevTop, prevLeft + prevWidth,
409                prevTop + mPrevText.getMeasuredHeight());
410
411        final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth,
412                currRight + mScaledTextSpacing);
413        mNextText.layout(nextLeft, nextTop, nextLeft + nextWidth,
414                nextTop + mNextText.getMeasuredHeight());
415
416        mLastKnownPositionOffset = positionOffset;
417        mUpdatingPositions = false;
418    }
419
420    @Override
421    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
422        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
423        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
424        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
425        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
426
427        if (widthMode != MeasureSpec.EXACTLY) {
428            throw new IllegalStateException("Must measure with an exact width");
429        }
430
431        int childHeight = heightSize;
432        int minHeight = getMinHeight();
433        int padding = 0;
434        padding = getPaddingTop() + getPaddingBottom();
435        childHeight -= padding;
436
437        final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f),
438                MeasureSpec.AT_MOST);
439        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST);
440
441        mPrevText.measure(childWidthSpec, childHeightSpec);
442        mCurrText.measure(childWidthSpec, childHeightSpec);
443        mNextText.measure(childWidthSpec, childHeightSpec);
444
445        if (heightMode == MeasureSpec.EXACTLY) {
446            setMeasuredDimension(widthSize, heightSize);
447        } else {
448            int textHeight = mCurrText.getMeasuredHeight();
449            setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding));
450        }
451    }
452
453    @Override
454    protected void onLayout(boolean changed, int l, int t, int r, int b) {
455        if (mPager != null) {
456            final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0;
457            updateTextPositions(mLastKnownCurrentPage, offset, true);
458        }
459    }
460
461    int getMinHeight() {
462        int minHeight = 0;
463        final Drawable bg = getBackground();
464        if (bg != null) {
465            minHeight = bg.getIntrinsicHeight();
466        }
467        return minHeight;
468    }
469
470    private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener,
471            ViewPager.OnAdapterChangeListener {
472        private int mScrollState;
473
474        @Override
475        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
476            if (positionOffset > 0.5f) {
477                // Consider ourselves to be on the next page when we're 50% of the way there.
478                position++;
479            }
480            updateTextPositions(position, positionOffset, false);
481        }
482
483        @Override
484        public void onPageSelected(int position) {
485            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
486                // Only update the text here if we're not dragging or settling.
487                updateText(mPager.getCurrentItem(), mPager.getAdapter());
488
489                final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0;
490                updateTextPositions(mPager.getCurrentItem(), offset, true);
491            }
492        }
493
494        @Override
495        public void onPageScrollStateChanged(int state) {
496            mScrollState = state;
497        }
498
499        @Override
500        public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
501            updateAdapter(oldAdapter, newAdapter);
502        }
503
504        @Override
505        public void onChanged() {
506            updateText(mPager.getCurrentItem(), mPager.getAdapter());
507
508            final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0;
509            updateTextPositions(mPager.getCurrentItem(), offset, true);
510        }
511    }
512}
513