1 /* 2 * Copyright (C) 2019 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.car.developeroptions.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.car.developeroptions.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