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