• 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.text.TextUtils.TruncateAt;
24 import android.util.AttributeSet;
25 import android.util.TypedValue;
26 import android.view.Gravity;
27 import android.view.ViewGroup;
28 import android.view.ViewParent;
29 import android.widget.TextView;
30 
31 import 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  */
45 public 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 {
setSingleLineAllCaps(TextView text)83         void setSingleLineAllCaps(TextView text);
84     }
85 
86     static class PagerTitleStripImplBase implements PagerTitleStripImpl {
setSingleLineAllCaps(TextView text)87         public void setSingleLineAllCaps(TextView text) {
88             text.setSingleLine();
89         }
90     }
91 
92     static class PagerTitleStripImplIcs implements PagerTitleStripImpl {
setSingleLineAllCaps(TextView text)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 
setSingleLineAllCaps(TextView text)107     private static void setSingleLineAllCaps(TextView text) {
108         IMPL.setSingleLineAllCaps(text);
109     }
110 
PagerTitleStrip(Context context)111     public PagerTitleStrip(Context context) {
112         this(context, null);
113     }
114 
PagerTitleStrip(Context context, AttributeSet attrs)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      */
setTextSpacing(int spacingPixels)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      */
getTextSpacing()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      */
setNonPrimaryAlpha(float alpha)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      */
setTextColor(int color)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      */
setTextSize(int unit, float size)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      */
setGravity(int gravity)235     public void setGravity(int gravity) {
236         mGravity = gravity;
237         requestLayout();
238     }
239 
240     @Override
onAttachedToWindow()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
onDetachedFromWindow()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 
updateText(int currentItem, PagerAdapter adapter)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
requestLayout()309     public void requestLayout() {
310         if (!mUpdatingText) {
311             super.requestLayout();
312         }
313     }
314 
updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter)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 
updateTextPositions(int position, float positionOffset, boolean force)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int l, int t, int r, int b)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 
getMinHeight()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
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)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
onPageSelected(int position)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
onPageScrollStateChanged(int state)495         public void onPageScrollStateChanged(int state) {
496             mScrollState = state;
497         }
498 
499         @Override
onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter)500         public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) {
501             updateAdapter(oldAdapter, newAdapter);
502         }
503 
504         @Override
onChanged()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