1 /*
2  * Copyright (C) 2016 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.android.settings.widget;
18 
19 import android.content.Context;
20 import android.util.AttributeSet;
21 import android.view.Gravity;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.widget.FrameLayout;
25 import android.widget.LinearLayout;
26 import android.widget.TextView;
27 
28 import androidx.viewpager.widget.PagerAdapter;
29 
30 import com.android.settings.R;
31 
32 /**
33  * To be used with ViewPager to provide a tab indicator component which give constant feedback as
34  * to the user's scroll progress.
35  */
36 public final class SlidingTabLayout extends FrameLayout implements View.OnClickListener {
37 
38     private final LinearLayout mTitleView;
39     private final View mIndicatorView;
40     private final LayoutInflater mLayoutInflater;
41 
42     private RtlCompatibleViewPager mViewPager;
43     private int mSelectedPosition;
44     private float mSelectionOffset;
45 
SlidingTabLayout(Context context, AttributeSet attrs)46     public SlidingTabLayout(Context context, AttributeSet attrs) {
47         super(context, attrs);
48         mLayoutInflater = LayoutInflater.from(context);
49         mTitleView = new LinearLayout(context);
50         mTitleView.setGravity(Gravity.CENTER_HORIZONTAL);
51         mIndicatorView = mLayoutInflater.inflate(R.layout.sliding_tab_indicator_view, this, false);
52 
53         addView(mTitleView, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
54         addView(mIndicatorView, mIndicatorView.getLayoutParams());
55     }
56 
57     /**
58      * Sets the associated view pager. Note that the assumption here is that the pager content
59      * (number of tabs and tab titles) does not change after this call has been made.
60      */
setViewPager(RtlCompatibleViewPager viewPager)61     public void setViewPager(RtlCompatibleViewPager viewPager) {
62         mTitleView.removeAllViews();
63 
64         mViewPager = viewPager;
65         if (viewPager != null) {
66             viewPager.addOnPageChangeListener(new InternalViewPagerListener());
67             populateTabStrip();
68         }
69     }
70 
71     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)72     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
73         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
74         final int titleCount = mTitleView.getChildCount();
75         if (titleCount > 0) {
76             final int width = MeasureSpec.makeMeasureSpec(
77                     mTitleView.getMeasuredWidth() / titleCount, MeasureSpec.EXACTLY);
78             final int height = MeasureSpec.makeMeasureSpec(
79                     mIndicatorView.getMeasuredHeight(), MeasureSpec.EXACTLY);
80             mIndicatorView.measure(width, height);
81         }
82     }
83 
84     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)85     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
86         if (mTitleView.getChildCount() > 0) {
87             final int indicatorBottom = getMeasuredHeight();
88             final int indicatorHeight = mIndicatorView.getMeasuredHeight();
89             final int indicatorWidth = mIndicatorView.getMeasuredWidth();
90             final int totalWidth = getMeasuredWidth();
91             final int leftPadding = getPaddingLeft();
92             final int rightPadding = getPaddingRight();
93 
94             mTitleView.layout(leftPadding, 0, mTitleView.getMeasuredWidth() + rightPadding,
95                     mTitleView.getMeasuredHeight());
96             // IndicatorView should start on the right when RTL mode is enabled
97             if (isRtlMode()) {
98                 mIndicatorView.layout(totalWidth - indicatorWidth,
99                         indicatorBottom - indicatorHeight, totalWidth,
100                         indicatorBottom);
101             } else {
102                 mIndicatorView.layout(0, indicatorBottom - indicatorHeight,
103                         indicatorWidth, indicatorBottom);
104             }
105         }
106     }
107 
108     @Override
onClick(View v)109     public void onClick(View v) {
110         final int titleCount = mTitleView.getChildCount();
111         for (int i = 0; i < titleCount; i++) {
112             if (v == mTitleView.getChildAt(i)) {
113                 mViewPager.setCurrentItem(i);
114                 return;
115             }
116         }
117     }
118 
onViewPagerPageChanged(int position, float positionOffset)119     private void onViewPagerPageChanged(int position, float positionOffset) {
120         mSelectedPosition = position;
121         mSelectionOffset = positionOffset;
122         // Translation should be reversed in RTL mode
123         final int leftIndicator = isRtlMode() ? -getIndicatorLeft() : getIndicatorLeft();
124         mIndicatorView.setTranslationX(leftIndicator);
125     }
126 
populateTabStrip()127     private void populateTabStrip() {
128         final PagerAdapter adapter = mViewPager.getAdapter();
129 
130         for (int i = 0; i < adapter.getCount(); i++) {
131             final TextView tabTitleView = (TextView) mLayoutInflater.inflate(
132                     R.layout.sliding_tab_title_view, mTitleView, false);
133 
134             tabTitleView.setText(adapter.getPageTitle(i));
135             tabTitleView.setOnClickListener(this);
136 
137             mTitleView.addView(tabTitleView);
138             tabTitleView.setSelected(i == mViewPager.getCurrentItem());
139         }
140     }
141 
getIndicatorLeft()142     private int getIndicatorLeft() {
143         View selectedTitle = mTitleView.getChildAt(mSelectedPosition);
144         int left = selectedTitle.getLeft();
145         if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) {
146             View nextTitle = mTitleView.getChildAt(mSelectedPosition + 1);
147             left = (int) (mSelectionOffset * nextTitle.getLeft()
148                     + (1.0f - mSelectionOffset) * left);
149         }
150         return left;
151     }
152 
isRtlMode()153     private boolean isRtlMode() {
154         return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
155     }
156 
157     private final class InternalViewPagerListener implements
158             RtlCompatibleViewPager.OnPageChangeListener {
159         private int mScrollState;
160 
161         @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)162         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
163             final int titleCount = mTitleView.getChildCount();
164             if ((titleCount == 0) || (position < 0) || (position >= titleCount)) {
165                 return;
166             }
167             onViewPagerPageChanged(position, positionOffset);
168         }
169 
170         @Override
onPageScrollStateChanged(int state)171         public void onPageScrollStateChanged(int state) {
172             mScrollState = state;
173         }
174 
175         @Override
onPageSelected(int position)176         public void onPageSelected(int position) {
177             position = mViewPager.getRtlAwareIndex(position);
178             if (mScrollState == RtlCompatibleViewPager.SCROLL_STATE_IDLE) {
179                 onViewPagerPageChanged(position, 0f);
180             }
181             final int titleCount = mTitleView.getChildCount();
182             for (int i = 0; i < titleCount; i++) {
183                 mTitleView.getChildAt(i).setSelected(position == i);
184             }
185         }
186     }
187 }
188