1 /* 2 * Copyright 2020 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 androidx.leanback.tab; 18 19 import android.annotation.SuppressLint; 20 import android.content.Context; 21 import android.database.DataSetObserver; 22 import android.util.AttributeSet; 23 import android.view.View; 24 import android.widget.LinearLayout; 25 26 import androidx.viewpager.widget.ViewPager; 27 28 import com.google.android.material.tabs.TabLayout; 29 30 import org.jspecify.annotations.NonNull; 31 import org.jspecify.annotations.Nullable; 32 33 import java.util.ArrayList; 34 35 /** 36 * {@link TabLayout} with some specific customizations related to focus navigation for TV to be 37 * used as 38 * top navigation bar. The following modifications have been done on the {@link TabLayout}: 39 * <ul> 40 * <li> When the focused tab changes the viewpager is also update accordingly. With the default 41 * behavior the viewpager is updated only when tab is clicked. </li> 42 * <li> Default behaviour is that focus moves to the tab closest to the last focused item inside 43 * viewpager on DPAD_UP. With the current change the selected tab gets the focus. </li> 44 * <li> Allowing change of current tab only when focus changes from an adjacent tab to current 45 * tab or focus changes from an element outside viewpager/tablayout to the 46 * viewpager/tablayout. This prevents change of tabs on DPAD_LEFT on the leftmost element 47 * inside viewpager and DPAD_RIGHT on the rightmost element inside viewpager. </li> 48 * </ul> 49 * 50 * <p> {@link ViewPager} can be used with this class but some of the behaviour of {@link ViewPager} 51 * might not be suitable for TV usage. Refer {@link LeanbackViewPager} for the modifications done 52 * on {@link ViewPager}. 53 */ 54 public class LeanbackTabLayout extends TabLayout { 55 56 ViewPager mViewPager; 57 final AdapterDataSetObserver mAdapterDataSetObserver = 58 new AdapterDataSetObserver(this); 59 60 /** 61 * Constructs LeanbackTabLayout 62 * @param context 63 */ LeanbackTabLayout(@onNull Context context)64 public LeanbackTabLayout(@NonNull Context context) { 65 super(context); 66 } 67 68 /** 69 * Constructs LeanbackTabLayout 70 * @param context 71 * @param attrs 72 */ LeanbackTabLayout(@onNull Context context, @NonNull AttributeSet attrs)73 public LeanbackTabLayout(@NonNull Context context, @NonNull AttributeSet attrs) { 74 super(context, attrs); 75 } 76 77 /** 78 * Constructs LeanbackTabLayout 79 * @param context 80 * @param attrs 81 * @param defStyleAttr 82 */ LeanbackTabLayout(@onNull Context context, @NonNull AttributeSet attrs, int defStyleAttr)83 public LeanbackTabLayout(@NonNull Context context, @NonNull AttributeSet attrs, 84 int defStyleAttr) { 85 super(context, attrs, defStyleAttr); 86 } 87 88 @Override onLayout(boolean changed, int left, int top, int right, int bottom)89 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 90 super.onLayout(changed, left, top, right, bottom); 91 updatePageTabs(); 92 } 93 94 @Override setupWithViewPager(@ullable ViewPager viewPager)95 public void setupWithViewPager(@Nullable ViewPager viewPager) { 96 super.setupWithViewPager(viewPager); 97 if (this.mViewPager != null && this.mViewPager.getAdapter() != null) { 98 this.mViewPager.getAdapter().unregisterDataSetObserver(mAdapterDataSetObserver); 99 } 100 this.mViewPager = viewPager; 101 if (this.mViewPager != null && this.mViewPager.getAdapter() != null) { 102 this.mViewPager.getAdapter().registerDataSetObserver(mAdapterDataSetObserver); 103 } 104 } 105 106 @Override addFocusables(@uppressLint"ConcreteCollection") @onNull ArrayList<View> views, int direction, int focusableMode)107 public void addFocusables(@SuppressLint("ConcreteCollection") @NonNull ArrayList<View> views, 108 int direction, int focusableMode) { 109 110 boolean isViewPagerFocused = this.mViewPager != null && this.mViewPager.hasFocus(); 111 boolean isCurrentlyFocused = this.hasFocus(); 112 LinearLayout tabStrip = (LinearLayout) this.getChildAt(0); 113 if ((direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) 114 && tabStrip != null && tabStrip.getChildCount() > 0 && this.mViewPager != null) { 115 View selectedTab = tabStrip.getChildAt(this.mViewPager.getCurrentItem()); 116 if (selectedTab != null) { 117 views.add(selectedTab); 118 } 119 } else if ((direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) 120 && !isCurrentlyFocused && isViewPagerFocused) { 121 return; 122 } else { 123 super.addFocusables(views, direction, focusableMode); 124 } 125 } 126 updatePageTabs()127 void updatePageTabs() { 128 LinearLayout tabStrip = (LinearLayout) this.getChildAt(0); 129 130 if (tabStrip == null) { 131 return; 132 } 133 134 int tabCount = tabStrip.getChildCount(); 135 for (int i = 0; i < tabCount; i++) { 136 final View tabView = tabStrip.getChildAt(i); 137 tabView.setFocusable(true); 138 tabView.setOnFocusChangeListener( 139 new TabFocusChangeListener(this, this.mViewPager)); 140 } 141 } 142 143 private static class AdapterDataSetObserver extends DataSetObserver { 144 145 final LeanbackTabLayout mLeanbackTabLayout; 146 AdapterDataSetObserver(LeanbackTabLayout leanbackTabLayout)147 AdapterDataSetObserver(LeanbackTabLayout leanbackTabLayout) { 148 mLeanbackTabLayout = leanbackTabLayout; 149 } 150 151 @Override onChanged()152 public void onChanged() { 153 mLeanbackTabLayout.updatePageTabs(); 154 } 155 156 @Override onInvalidated()157 public void onInvalidated() { 158 mLeanbackTabLayout.updatePageTabs(); 159 } 160 } 161 } 162