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