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 
17 package com.example.android.common.view;
18 
19 import android.content.Context;
20 import android.graphics.Typeface;
21 import android.os.Build;
22 import android.support.v4.view.PagerAdapter;
23 import android.support.v4.view.ViewPager;
24 import android.util.AttributeSet;
25 import android.util.TypedValue;
26 import android.view.Gravity;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.widget.HorizontalScrollView;
30 import 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  */
48 public 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          */
getIndicatorColor(int position)59         int getIndicatorColor(int position);
60 
61         /**
62          * @return return the color of the divider drawn to the right of {@code position}.
63          */
getDividerColor(int position)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 
SlidingTabLayout(Context context)82     public SlidingTabLayout(Context context) {
83         this(context, null);
84     }
85 
SlidingTabLayout(Context context, AttributeSet attrs)86     public SlidingTabLayout(Context context, AttributeSet attrs) {
87         this(context, attrs, 0);
88     }
89 
SlidingTabLayout(Context context, AttributeSet attrs, int defStyle)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      */
setCustomTabColorizer(TabColorizer tabColorizer)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      */
setSelectedIndicatorColors(int... colors)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      */
setDividerColors(int... colors)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      */
setOnPageChangeListener(ViewPager.OnPageChangeListener listener)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      */
setCustomTabView(int layoutResId, int textViewId)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      */
setViewPager(ViewPager viewPager)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      */
createDefaultTabView(Context context)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 
populateTabStrip()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
onAttachedToWindow()228     protected void onAttachedToWindow() {
229         super.onAttachedToWindow();
230 
231         if (mViewPager != null) {
232             scrollToTab(mViewPager.getCurrentItem(), 0);
233         }
234     }
235 
scrollToTab(int tabIndex, int positionOffset)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
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)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
onPageScrollStateChanged(int state)280         public void onPageScrollStateChanged(int state) {
281             mScrollState = state;
282 
283             if (mViewPagerPageChangeListener != null) {
284                 mViewPagerPageChangeListener.onPageScrollStateChanged(state);
285             }
286         }
287 
288         @Override
onPageSelected(int position)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
onClick(View v)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