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 
17 package android.support.v4.view;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.database.DataSetObserver;
22 import android.graphics.drawable.Drawable;
23 import android.support.annotation.ColorInt;
24 import android.support.annotation.FloatRange;
25 import android.text.TextUtils.TruncateAt;
26 import android.util.AttributeSet;
27 import android.util.TypedValue;
28 import android.view.Gravity;
29 import android.view.ViewGroup;
30 import android.view.ViewParent;
31 import android.widget.TextView;
32 
33 import 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
48 public 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 {
setSingleLineAllCaps(TextView text)86         void setSingleLineAllCaps(TextView text);
87     }
88 
89     static class PagerTitleStripImplBase implements PagerTitleStripImpl {
setSingleLineAllCaps(TextView text)90         public void setSingleLineAllCaps(TextView text) {
91             text.setSingleLine();
92         }
93     }
94 
95     static class PagerTitleStripImplIcs implements PagerTitleStripImpl {
setSingleLineAllCaps(TextView text)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 
setSingleLineAllCaps(TextView text)110     private static void setSingleLineAllCaps(TextView text) {
111         IMPL.setSingleLineAllCaps(text);
112     }
113 
PagerTitleStrip(Context context)114     public PagerTitleStrip(Context context) {
115         this(context, null);
116     }
117 
PagerTitleStrip(Context context, AttributeSet attrs)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      */
setTextSpacing(int spacingPixels)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      */
getTextSpacing()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      */
setNonPrimaryAlpha(@loatRangefrom=0.0, to=1.0) float alpha)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      */
setTextColor(@olorInt int color)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      */
setTextSize(int unit, float size)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      */
setGravity(int gravity)238     public void setGravity(int gravity) {
239         mGravity = gravity;
240         requestLayout();
241     }
242 
243     @Override
onAttachedToWindow()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
onDetachedFromWindow()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 
updateText(int currentItem, PagerAdapter adapter)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
requestLayout()313     public void requestLayout() {
314         if (!mUpdatingText) {
315             super.requestLayout();
316         }
317     }
318 
updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter)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 
updateTextPositions(int position, float positionOffset, boolean force)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int l, int t, int r, int b)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 
getMinHeight()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
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)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
onPageSelected(int position)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
onPageScrollStateChanged(int state)502         public void onPageScrollStateChanged(int state) {
503             mScrollState = state;
504         }
505 
506         @Override
onAdapterChanged(ViewPager viewPager, PagerAdapter oldAdapter, PagerAdapter newAdapter)507         public void onAdapterChanged(ViewPager viewPager, PagerAdapter oldAdapter,
508                 PagerAdapter newAdapter) {
509             updateAdapter(oldAdapter, newAdapter);
510         }
511 
512         @Override
onChanged()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