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