1/*
2 * Copyright (C) 2012 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.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.support.annotation.ColorInt;
25import android.support.annotation.ColorRes;
26import android.support.annotation.DrawableRes;
27import android.support.v4.content.ContextCompat;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewConfiguration;
32
33/**
34 * PagerTabStrip is an 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 a non-interactive indicator, see {@link PagerTitleStrip}.</p>
44 */
45public class PagerTabStrip extends PagerTitleStrip {
46    private static final String TAG = "PagerTabStrip";
47
48    private static final int INDICATOR_HEIGHT = 3; // dp
49    private static final int MIN_PADDING_BOTTOM = INDICATOR_HEIGHT + 3; // dp
50    private static final int TAB_PADDING = 16; // dp
51    private static final int TAB_SPACING = 32; // dp
52    private static final int MIN_TEXT_SPACING = TAB_SPACING + TAB_PADDING * 2; // dp
53    private static final int FULL_UNDERLINE_HEIGHT = 1; // dp
54    private static final int MIN_STRIP_HEIGHT = 32; // dp
55
56    private int mIndicatorColor;
57    private int mIndicatorHeight;
58
59    private int mMinPaddingBottom;
60    private int mMinTextSpacing;
61    private int mMinStripHeight;
62
63    private int mTabPadding;
64
65    private final Paint mTabPaint = new Paint();
66    private final Rect mTempRect = new Rect();
67
68    private int mTabAlpha = 0xFF;
69
70    private boolean mDrawFullUnderline = false;
71    private boolean mDrawFullUnderlineSet = false;
72    private int mFullUnderlineHeight;
73
74    private boolean mIgnoreTap;
75    private float mInitialMotionX;
76    private float mInitialMotionY;
77    private int mTouchSlop;
78
79    public PagerTabStrip(Context context) {
80        this(context, null);
81    }
82
83    public PagerTabStrip(Context context, AttributeSet attrs) {
84        super(context, attrs);
85
86        mIndicatorColor = mTextColor;
87        mTabPaint.setColor(mIndicatorColor);
88
89        // Note: this follows the rules for Resources#getDimensionPixelOffset/Size:
90        //       sizes round up, offsets round down.
91        final float density = context.getResources().getDisplayMetrics().density;
92        mIndicatorHeight = (int) (INDICATOR_HEIGHT * density + 0.5f);
93        mMinPaddingBottom = (int) (MIN_PADDING_BOTTOM * density + 0.5f);
94        mMinTextSpacing = (int) (MIN_TEXT_SPACING * density);
95        mTabPadding = (int) (TAB_PADDING * density + 0.5f);
96        mFullUnderlineHeight = (int) (FULL_UNDERLINE_HEIGHT * density + 0.5f);
97        mMinStripHeight = (int) (MIN_STRIP_HEIGHT * density + 0.5f);
98        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
99
100        // Enforce restrictions
101        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
102        setTextSpacing(getTextSpacing());
103
104        setWillNotDraw(false);
105
106        mPrevText.setFocusable(true);
107        mPrevText.setOnClickListener(new OnClickListener() {
108            @Override
109            public void onClick(View v) {
110                mPager.setCurrentItem(mPager.getCurrentItem() - 1);
111            }
112        });
113
114        mNextText.setFocusable(true);
115        mNextText.setOnClickListener(new OnClickListener() {
116            @Override
117            public void onClick(View v) {
118                mPager.setCurrentItem(mPager.getCurrentItem() + 1);
119            }
120        });
121
122        if (getBackground() == null) {
123            mDrawFullUnderline = true;
124        }
125    }
126
127    /**
128     * Set the color of the tab indicator bar.
129     *
130     * @param color Color to set as an 0xRRGGBB value. The high byte (alpha) is ignored.
131     */
132    public void setTabIndicatorColor(@ColorInt int color) {
133        mIndicatorColor = color;
134        mTabPaint.setColor(mIndicatorColor);
135        invalidate();
136    }
137
138    /**
139     * Set the color of the tab indicator bar from a color resource.
140     *
141     * @param resId Resource ID of a color resource to load
142     */
143    public void setTabIndicatorColorResource(@ColorRes int resId) {
144        setTabIndicatorColor(ContextCompat.getColor(getContext(), resId));
145    }
146
147    /**
148     * @return The current tab indicator color as an 0xRRGGBB value.
149     */
150    @ColorInt
151    public int getTabIndicatorColor() {
152        return mIndicatorColor;
153    }
154
155    @Override
156    public void setPadding(int left, int top, int right, int bottom) {
157        if (bottom < mMinPaddingBottom) {
158            bottom = mMinPaddingBottom;
159        }
160        super.setPadding(left, top, right, bottom);
161    }
162
163    @Override
164    public void setTextSpacing(int textSpacing) {
165        if (textSpacing < mMinTextSpacing) {
166            textSpacing = mMinTextSpacing;
167        }
168        super.setTextSpacing(textSpacing);
169    }
170
171    @Override
172    public void setBackgroundDrawable(Drawable d) {
173        super.setBackgroundDrawable(d);
174        if (!mDrawFullUnderlineSet) {
175            mDrawFullUnderline = d == null;
176        }
177    }
178
179    @Override
180    public void setBackgroundColor(@ColorInt int color) {
181        super.setBackgroundColor(color);
182        if (!mDrawFullUnderlineSet) {
183            mDrawFullUnderline = (color & 0xFF000000) == 0;
184        }
185    }
186
187    @Override
188    public void setBackgroundResource(@DrawableRes int resId) {
189        super.setBackgroundResource(resId);
190        if (!mDrawFullUnderlineSet) {
191            mDrawFullUnderline = resId == 0;
192        }
193    }
194
195    /**
196     * Set whether this tab strip should draw a full-width underline in the
197     * current tab indicator color.
198     *
199     * @param drawFull true to draw a full-width underline, false otherwise
200     */
201    public void setDrawFullUnderline(boolean drawFull) {
202        mDrawFullUnderline = drawFull;
203        mDrawFullUnderlineSet = true;
204        invalidate();
205    }
206
207    /**
208     * Return whether or not this tab strip will draw a full-width underline.
209     * This defaults to true if no background is set.
210     *
211     * @return true if this tab strip will draw a full-width underline in the
212     * current tab indicator color.
213     */
214    public boolean getDrawFullUnderline() {
215        return mDrawFullUnderline;
216    }
217
218    @Override
219    int getMinHeight() {
220        return Math.max(super.getMinHeight(), mMinStripHeight);
221    }
222
223    @Override
224    public boolean onTouchEvent(MotionEvent ev) {
225        final int action = ev.getAction();
226        if (action != MotionEvent.ACTION_DOWN && mIgnoreTap) {
227            return false;
228        }
229
230        // Any tap within touch slop to either side of the current item
231        // will scroll to prev/next.
232        final float x = ev.getX();
233        final float y = ev.getY();
234        switch (action) {
235            case MotionEvent.ACTION_DOWN:
236                mInitialMotionX = x;
237                mInitialMotionY = y;
238                mIgnoreTap = false;
239                break;
240
241            case MotionEvent.ACTION_MOVE:
242                if (Math.abs(x - mInitialMotionX) > mTouchSlop
243                        || Math.abs(y - mInitialMotionY) > mTouchSlop) {
244                    mIgnoreTap = true;
245                }
246                break;
247
248            case MotionEvent.ACTION_UP:
249                if (x < mCurrText.getLeft() - mTabPadding) {
250                    mPager.setCurrentItem(mPager.getCurrentItem() - 1);
251                } else if (x > mCurrText.getRight() + mTabPadding) {
252                    mPager.setCurrentItem(mPager.getCurrentItem() + 1);
253                }
254                break;
255        }
256
257        return true;
258    }
259
260    @Override
261    protected void onDraw(Canvas canvas) {
262        super.onDraw(canvas);
263
264        final int height = getHeight();
265        final int bottom = height;
266        final int left = mCurrText.getLeft() - mTabPadding;
267        final int right = mCurrText.getRight() + mTabPadding;
268        final int top = bottom - mIndicatorHeight;
269
270        mTabPaint.setColor(mTabAlpha << 24 | (mIndicatorColor & 0xFFFFFF));
271        canvas.drawRect(left, top, right, bottom, mTabPaint);
272
273        if (mDrawFullUnderline) {
274            mTabPaint.setColor(0xFF << 24 | (mIndicatorColor & 0xFFFFFF));
275            canvas.drawRect(getPaddingLeft(), height - mFullUnderlineHeight,
276                    getWidth() - getPaddingRight(), height, mTabPaint);
277        }
278    }
279
280    @Override
281    void updateTextPositions(int position, float positionOffset, boolean force) {
282        final Rect r = mTempRect;
283        int bottom = getHeight();
284        int left = mCurrText.getLeft() - mTabPadding;
285        int right = mCurrText.getRight() + mTabPadding;
286        int top = bottom - mIndicatorHeight;
287
288        r.set(left, top, right, bottom);
289
290        super.updateTextPositions(position, positionOffset, force);
291        mTabAlpha = (int) (Math.abs(positionOffset - 0.5f) * 2 * 0xFF);
292
293        left = mCurrText.getLeft() - mTabPadding;
294        right = mCurrText.getRight() + mTabPadding;
295        r.union(left, top, right, bottom);
296
297        invalidate(r);
298    }
299}
300