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