1/*
2 * Copyright (C) 2013 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 com.example.android.common.view;
18
19import android.content.Context;
20import android.graphics.Typeface;
21import android.os.Build;
22import android.support.v4.view.PagerAdapter;
23import android.support.v4.view.ViewPager;
24import android.util.AttributeSet;
25import android.util.TypedValue;
26import android.view.Gravity;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.widget.HorizontalScrollView;
30import android.widget.TextView;
31
32/**
33 * To be used with ViewPager to provide a tab indicator component which give constant feedback as to
34 * the user's scroll progress.
35 * <p>
36 * To use the component, simply add it to your view hierarchy. Then in your
37 * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call
38 * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for.
39 * <p>
40 * The colors can be customized in two ways. The first and simplest is to provide an array of colors
41 * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The
42 * alternative is via the {@link TabColorizer} interface which provides you complete control over
43 * which color is used for any individual position.
44 * <p>
45 * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)},
46 * providing the layout ID of your custom layout.
47 */
48public class SlidingTabLayout extends HorizontalScrollView {
49
50    /**
51     * Allows complete control over the colors drawn in the tab layout. Set with
52     * {@link #setCustomTabColorizer(TabColorizer)}.
53     */
54    public interface TabColorizer {
55
56        /**
57         * @return return the color of the indicator used when {@code position} is selected.
58         */
59        int getIndicatorColor(int position);
60
61        /**
62         * @return return the color of the divider drawn to the right of {@code position}.
63         */
64        int getDividerColor(int position);
65
66    }
67
68    private static final int TITLE_OFFSET_DIPS = 24;
69    private static final int TAB_VIEW_PADDING_DIPS = 16;
70    private static final int TAB_VIEW_TEXT_SIZE_SP = 12;
71
72    private int mTitleOffset;
73
74    private int mTabViewLayoutId;
75    private int mTabViewTextViewId;
76
77    private ViewPager mViewPager;
78    private ViewPager.OnPageChangeListener mViewPagerPageChangeListener;
79
80    private final SlidingTabStrip mTabStrip;
81
82    public SlidingTabLayout(Context context) {
83        this(context, null);
84    }
85
86    public SlidingTabLayout(Context context, AttributeSet attrs) {
87        this(context, attrs, 0);
88    }
89
90    public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) {
91        super(context, attrs, defStyle);
92
93        // Disable the Scroll Bar
94        setHorizontalScrollBarEnabled(false);
95        // Make sure that the Tab Strips fills this View
96        setFillViewport(true);
97
98        mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density);
99
100        mTabStrip = new SlidingTabStrip(context);
101        addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
102    }
103
104    /**
105     * Set the custom {@link TabColorizer} to be used.
106     *
107     * If you only require simple custmisation then you can use
108     * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve
109     * similar effects.
110     */
111    public void setCustomTabColorizer(TabColorizer tabColorizer) {
112        mTabStrip.setCustomTabColorizer(tabColorizer);
113    }
114
115    /**
116     * Sets the colors to be used for indicating the selected tab. These colors are treated as a
117     * circular array. Providing one color will mean that all tabs are indicated with the same color.
118     */
119    public void setSelectedIndicatorColors(int... colors) {
120        mTabStrip.setSelectedIndicatorColors(colors);
121    }
122
123    /**
124     * Sets the colors to be used for tab dividers. These colors are treated as a circular array.
125     * Providing one color will mean that all tabs are indicated with the same color.
126     */
127    public void setDividerColors(int... colors) {
128        mTabStrip.setDividerColors(colors);
129    }
130
131    /**
132     * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are
133     * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so
134     * that the layout can update it's scroll position correctly.
135     *
136     * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener)
137     */
138    public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
139        mViewPagerPageChangeListener = listener;
140    }
141
142    /**
143     * Set the custom layout to be inflated for the tab views.
144     *
145     * @param layoutResId Layout id to be inflated
146     * @param textViewId id of the {@link TextView} in the inflated view
147     */
148    public void setCustomTabView(int layoutResId, int textViewId) {
149        mTabViewLayoutId = layoutResId;
150        mTabViewTextViewId = textViewId;
151    }
152
153    /**
154     * Sets the associated view pager. Note that the assumption here is that the pager content
155     * (number of tabs and tab titles) does not change after this call has been made.
156     */
157    public void setViewPager(ViewPager viewPager) {
158        mTabStrip.removeAllViews();
159
160        mViewPager = viewPager;
161        if (viewPager != null) {
162            viewPager.setOnPageChangeListener(new InternalViewPagerListener());
163            populateTabStrip();
164        }
165    }
166
167    /**
168     * Create a default view to be used for tabs. This is called if a custom tab view is not set via
169     * {@link #setCustomTabView(int, int)}.
170     */
171    protected TextView createDefaultTabView(Context context) {
172        TextView textView = new TextView(context);
173        textView.setGravity(Gravity.CENTER);
174        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP);
175        textView.setTypeface(Typeface.DEFAULT_BOLD);
176
177        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
178            // If we're running on Honeycomb or newer, then we can use the Theme's
179            // selectableItemBackground to ensure that the View has a pressed state
180            TypedValue outValue = new TypedValue();
181            getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
182                    outValue, true);
183            textView.setBackgroundResource(outValue.resourceId);
184        }
185
186        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
187            // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style
188            textView.setAllCaps(true);
189        }
190
191        int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density);
192        textView.setPadding(padding, padding, padding, padding);
193
194        return textView;
195    }
196
197    private void populateTabStrip() {
198        final PagerAdapter adapter = mViewPager.getAdapter();
199        final View.OnClickListener tabClickListener = new TabClickListener();
200
201        for (int i = 0; i < adapter.getCount(); i++) {
202            View tabView = null;
203            TextView tabTitleView = null;
204
205            if (mTabViewLayoutId != 0) {
206                // If there is a custom tab view layout id set, try and inflate it
207                tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip,
208                        false);
209                tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId);
210            }
211
212            if (tabView == null) {
213                tabView = createDefaultTabView(getContext());
214            }
215
216            if (tabTitleView == null && TextView.class.isInstance(tabView)) {
217                tabTitleView = (TextView) tabView;
218            }
219
220            tabTitleView.setText(adapter.getPageTitle(i));
221            tabView.setOnClickListener(tabClickListener);
222
223            mTabStrip.addView(tabView);
224        }
225    }
226
227    @Override
228    protected void onAttachedToWindow() {
229        super.onAttachedToWindow();
230
231        if (mViewPager != null) {
232            scrollToTab(mViewPager.getCurrentItem(), 0);
233        }
234    }
235
236    private void scrollToTab(int tabIndex, int positionOffset) {
237        final int tabStripChildCount = mTabStrip.getChildCount();
238        if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {
239            return;
240        }
241
242        View selectedChild = mTabStrip.getChildAt(tabIndex);
243        if (selectedChild != null) {
244            int targetScrollX = selectedChild.getLeft() + positionOffset;
245
246            if (tabIndex > 0 || positionOffset > 0) {
247                // If we're not at the first child and are mid-scroll, make sure we obey the offset
248                targetScrollX -= mTitleOffset;
249            }
250
251            scrollTo(targetScrollX, 0);
252        }
253    }
254
255    private class InternalViewPagerListener implements ViewPager.OnPageChangeListener {
256        private int mScrollState;
257
258        @Override
259        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
260            int tabStripChildCount = mTabStrip.getChildCount();
261            if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
262                return;
263            }
264
265            mTabStrip.onViewPagerPageChanged(position, positionOffset);
266
267            View selectedTitle = mTabStrip.getChildAt(position);
268            int extraOffset = (selectedTitle != null)
269                    ? (int) (positionOffset * selectedTitle.getWidth())
270                    : 0;
271            scrollToTab(position, extraOffset);
272
273            if (mViewPagerPageChangeListener != null) {
274                mViewPagerPageChangeListener.onPageScrolled(position, positionOffset,
275                        positionOffsetPixels);
276            }
277        }
278
279        @Override
280        public void onPageScrollStateChanged(int state) {
281            mScrollState = state;
282
283            if (mViewPagerPageChangeListener != null) {
284                mViewPagerPageChangeListener.onPageScrollStateChanged(state);
285            }
286        }
287
288        @Override
289        public void onPageSelected(int position) {
290            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
291                mTabStrip.onViewPagerPageChanged(position, 0f);
292                scrollToTab(position, 0);
293            }
294
295            if (mViewPagerPageChangeListener != null) {
296                mViewPagerPageChangeListener.onPageSelected(position);
297            }
298        }
299
300    }
301
302    private class TabClickListener implements View.OnClickListener {
303        @Override
304        public void onClick(View v) {
305            for (int i = 0; i < mTabStrip.getChildCount(); i++) {
306                if (v == mTabStrip.getChildAt(i)) {
307                    mViewPager.setCurrentItem(i);
308                    return;
309                }
310            }
311        }
312    }
313
314}
315