ViewPagerとかと併用してTabLayoutを使う場合に、特定のタブにアイコンを表示する時にどうすればいいかなーという時のメモ。1
今のところは Tab#setCustomView() でcustome layout resourceをセットするのが大体のケースにおいて現実解となりそう。
また、こちらの調査はDesign Support Libraryのコードを呼んだもので、AndroidXでどうなっているかは保証できません。 (ざっと見た感じだとそんなに変わってなさそうだった。2)
運よく setIcon() が使える場合
custom viewを作らなくても運よく setIcon() で済む場合がある。setIcon()
はその名の通りTabにアイコンを設定してくれる。ただし、このAPIを使う場合だとアイコンの位置はテキストの上に固定されている。
ちなみにここのImageViewの定義はこちら。
<ImageView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="24dp" android:layout_height="24dp" android:scaleType="centerInside"/>
TextViewの定義はこちら。
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" android:gravity="center" android:maxLines="2"/>
ちなみにTextViewの方はこちらのstyleが設定されている。
<style name="TextAppearance.Design.Tab" parent="TextAppearance.AppCompat.Button"> <item name="android:textSize">14sp</item> <item name="android:textColor">?android:textColorSecondary</item> <item name="textAllCaps">true</item> </style>
で、このImageViewとTextViewがTabView
に addView()されるのだが、TabView
は LinearLayout
をextendsしているので上記のように縦に並んだデザインになる。3
大抵の場合はアイコンをテキスト横に配置したい
縦じゃなくてテキストの横にアイコンを並べたいケースが多いと思う。(こんな感じで↓)
この場合だとlayout xmlを書いて setCustomView()
に渡してやるとできる。
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <ImageView android:id="@+id/icon" android:layout_width="8dp" android:layout_height="8dp" android:src="@drawable/circle" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@android:id/text1"/> <TextView android:id="@android:id/text1" style="@style/TabTextAppearance" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="4dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/icon" app:layout_constraintEnd_toEndOf="parent" tools:text="TAB"/> </android.support.constraint.ConstraintLayout>
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val viewPager = findViewById<ViewPager>(R.id.view_pager) viewPager.adapter = ViewPagerAdapter(supportFragmentManager) val tab = findViewById<TabLayout>(R.id.tab) tab.setupWithViewPager(viewPager) tab.getTabAt(0)?.setCustomView(R.layout.view_custom_view) } private class ViewPagerAdapter(fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager) { override fun getCount() = 2 override fun getItem(p0: Int) = MainFragment() override fun getPageTitle(position: Int) = when (position) { 0 -> "one" 1 -> "two" else -> "other" } } }
ただし、注意すべきポイントがある。
TextViewのid
上記のxmlの通り、TextViewに設定するidは @android:id/text1
の必要がある。これを設定しないとPagerAdapterから渡されるtitleが設定されない。 TabView#update()
の中を見るとわかる。
final void update() { final Tab tab = mTab; final View custom = tab.getCustomView(); if (custom != null) { final ViewParent customParent = custom.getParent(); if (customParent != this) { if (customParent != null) { ((ViewGroup) customParent).removeView(custom); } addView(custom); } mCustomView = custom; if (mTextView != null) { mTextView.setVisibility(GONE); } if (mIconView != null) { mIconView.setVisibility(GONE); mIconView.setImageDrawable(null); } mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
アイコンをつけないViewと見た目を揃えたい
多分一番面倒なのがここ。全てのTabをcustomにするのであればいいけど、デフォルトのtabを一緒に表示する場合は違和感をなくすために見た目を揃えたくなると思う。その場合は上記のsetIcon()を使う場合で紹介したTextViewやstyleのxmlを参考にしてできる限り同じattributeを設定してやると違和感がなくせる。
ただしできないこともある
残念ながらcustom viewをセットする場合はstatus(selectedか否か)をTabView
が設定してくれない。TabView
はdefaultデザインの場合に表示されるmTextView
/mIconView
と、custom viewを設定した場合のmCustomTextView
/mCustomIconView
を分けて管理している。
private TextView mTextView; private ImageView mIconView; private TextView mCustomTextView; private ImageView mCustomIconView;
で TabView#setSelected()の中を見ると、mTextView
/mIconView
にしかsetSelected()
を呼んでいない。
@Override public void setSelected(boolean selected) { final boolean changed = (isSelected() != selected); super.setSelected(selected); if (changed && selected) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); if (mTextView != null) { mTextView.setSelected(selected); } if (mIconView != null) { mIconView.setSelected(selected); } } }
したがって、custom viewを設定した場合はタブを動かしてもfont colorなどが変わらない。
回避策としてTabSelectListerでtabの状態を検知してコードから状態を設定するのが良さそう。
こんな感じでselectorでcolorを定義して、
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_selected="true" android:color="?android:textColorPrimary" /> <item android:color="?android:textColorSecondary" /> </selector>
CustomViewのTextViewのtextColorに指定する。
<TextView android:id="@android:id/text1" (省略) android:textColor="@color/tab_text_color"
最後にTabLayout#addOnTabSelectedListener()
を設定してcustomViewの状態を設定する。
val tab = findViewById<TabLayout>(R.id.tab) tab.setupWithViewPager(viewPager) tab.getTabAt(0)?.setCustomView(R.layout.view_custom_view) tab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(p0: TabLayout.Tab?) {} override fun onTabSelected(p0: TabLayout.Tab?) { p0?.customView?.isSelected = true } override fun onTabUnselected(p0: TabLayout.Tab?) { p0?.customView?.isSelected = false } })
これで無事custom Viewの方もstateによってtext colorを変えることができる。
余談
ViewPagerに連動してTabLayoutの方もselected stateが変わるしくみを調べたのでメモ。
ViewPagerとTabLayoutを紐づける時にコールするTabLayout#setupWithViewPager()
の内部でViewPagerに対してTabLayoutOnPageChangeListener
をセットしている。
// Now we'll add our page change listener to the ViewPager viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(this));
で、ViewPagerの方でSwipe動作でPageがscrollされた時に上で設定したlistenerのonPageScrolled()
が呼ばれる。
private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) { if (this.mOnPageChangeListener != null) { this.mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); } if (this.mOnPageChangeListeners != null) { int i = 0; for(int z = this.mOnPageChangeListeners.size(); i < z; ++i) { ViewPager.OnPageChangeListener listener = (ViewPager.OnPageChangeListener)this.mOnPageChangeListeners.get(i); if (listener != null) { listener.onPageScrolled(position, offset, offsetPixels); // ★ここ } } }
TabLayoutOnPageChangeListener#onPageScrolled()でTabLayout#setScrollPosition()が呼ばれる。
@Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { final TabLayout tabLayout = mTabLayoutRef.get(); if (tabLayout != null) { // Update the scroll position, only update the text selection if we're being // dragged (or we're settling after a drag) final boolean updateText = (mScrollState == SCROLL_STATE_DRAGGING) || (mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_DRAGGING); tabLayout.setScrollPosition(position, positionOffset, updateText); // ★ここ } }
さらにsetSelectedTabView()
が呼ばれて、
public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { return; } if (position < 0 || position >= mTabStrip.getChildCount()) { return; } // Set the indicator position and update the scroll to match mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); scrollTo(calculateScrollXForTab(position, positionOffset), 0); // Update the 'selected state' view as we scroll if (updateSelectedText) { setSelectedTabView(Math.round(position + positionOffset)); // ★ここ } }
この中でTabView#setSelected()
をコールし、
private void setSelectedTabView(int position) { final int tabCount = mTabStrip.getChildCount(); for (int i = 0; i < tabCount; i++) { final View child = mTabStrip.getChildAt(i); child.setSelected(i == position); // ★ここ } }
それぞれのViewに対して状態を変えている。先ほども見た通り、custom viewの状態は変わらない。
@Override public void setSelected(boolean selected) { final boolean changed = (isSelected() != selected); super.setSelected(selected); if (changed && selected) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); if (mTextView != null) { mTextView.setSelected(selected); } if (mIconView != null) { mIconView.setSelected(selected); } } }