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