• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.systemui.accessibility.accessibilitymenu.view;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.graphics.Insets;
22 import android.util.DisplayMetrics;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
26 import android.view.WindowInsets;
27 import android.view.WindowManager;
28 import android.view.WindowMetrics;
29 import android.widget.GridView;
30 
31 import androidx.recyclerview.widget.RecyclerView;
32 import androidx.viewpager2.widget.ViewPager2;
33 
34 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
35 import com.android.systemui.accessibility.accessibilitymenu.R;
36 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
37 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
38 import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack;
39 import com.android.systemui.utils.windowmanager.WindowManagerUtils;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 /**
45  * This class handles UI for viewPager and footer.
46  * It displays grid pages containing all shortcuts in viewPager,
47  * and handles the click events from footer to switch between pages.
48  */
49 public class A11yMenuViewPager {
50 
51     /** The default index of the ViewPager. */
52     public static final int DEFAULT_PAGE_INDEX = 0;
53 
54     /**
55      * The class holds the static parameters for grid view when large button settings is on/off.
56      */
57     public static final class GridViewParams {
58         /** Total shortcuts count in the grid view when large button settings is off. */
59         public static final int GRID_ITEM_COUNT = 9;
60 
61         /** The number of columns in the grid view when large button settings is off. */
62         public static final int GRID_COLUMN_COUNT = 3;
63 
64         /** Total shortcuts count in the grid view when large button settings is on. */
65         public static final int LARGE_GRID_ITEM_COUNT = 4;
66 
67         /** The number of columns in the grid view when large button settings is on. */
68         public static final int LARGE_GRID_COLUMN_COUNT = 2;
69 
70         /**
71          * Returns the number of items in the grid view.
72          *
73          * @param context The parent context
74          * @return Grid item count
75          */
getGridItemCount(Context context)76         public static int getGridItemCount(Context context) {
77             return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
78                    ? LARGE_GRID_ITEM_COUNT
79                    : GRID_ITEM_COUNT;
80         }
81 
82         /**
83          * Returns the number of columns in the grid view.
84          *
85          * @param context The parent context
86          * @return Grid column count
87          */
getGridColumnCount(Context context)88         public static int getGridColumnCount(Context context) {
89             return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
90                    ? LARGE_GRID_COLUMN_COUNT
91                    : GRID_COLUMN_COUNT;
92         }
93 
94         /**
95          * Returns the number of rows in the grid view.
96          *
97          * @param context The parent context
98          * @return Grid row count
99          */
getGridRowCount(Context context)100         public static int getGridRowCount(Context context) {
101             return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
102                    ? (LARGE_GRID_ITEM_COUNT / LARGE_GRID_COLUMN_COUNT)
103                    : (GRID_ITEM_COUNT / GRID_COLUMN_COUNT);
104         }
105 
106         /**
107          * Separates a provided list of accessibility shortcuts into multiple sub-lists.
108          * Does not modify the original list.
109          *
110          * @param pageItemCount The maximum size of an individual sub-list.
111          * @param shortcutList The list of shortcuts to be separated into sub-lists.
112          * @return A list of shortcut sub-lists.
113          */
generateShortcutSubLists( int pageItemCount, List<A11yMenuShortcut> shortcutList)114         public static List<List<A11yMenuShortcut>> generateShortcutSubLists(
115                 int pageItemCount, List<A11yMenuShortcut> shortcutList) {
116             int start = 0;
117             int end;
118             int shortcutListSize = shortcutList.size();
119             List<List<A11yMenuShortcut>> subLists = new ArrayList<>();
120             while (start < shortcutListSize) {
121                 end = Math.min(start + pageItemCount, shortcutListSize);
122                 subLists.add(shortcutList.subList(start, end));
123                 start = end;
124             }
125             return subLists;
126         }
127 
GridViewParams()128         private GridViewParams() {}
129     }
130 
131     private final AccessibilityMenuService mService;
132 
133     /**
134      * The pager widget, which handles animation and allows swiping horizontally to access previous
135      * and next gridView pages.
136      */
137     protected ViewPager2 mViewPager;
138 
139     private ViewPagerAdapter mViewPagerAdapter;
140     private final List<GridView> mGridPageList = new ArrayList<>();
141 
142     /** The footer, which provides buttons to switch between pages */
143     protected A11yMenuFooter mA11yMenuFooter;
144 
145     /** The shortcut list intended to show in grid pages of viewPager */
146     private List<A11yMenuShortcut> mA11yMenuShortcutList;
147 
148     /** The container layout for a11y menu. */
149     private ViewGroup mA11yMenuLayout;
150 
A11yMenuViewPager(AccessibilityMenuService service)151     public A11yMenuViewPager(AccessibilityMenuService service) {
152         this.mService = service;
153     }
154 
155     /**
156      * Configures UI for view pager and footer.
157      *
158      * @param a11yMenuLayout the container layout for a11y menu
159      * @param shortcutDataList the data list need to show in view pager
160      * @param pageIndex the index of ViewPager to show
161      */
configureViewPagerAndFooter( ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex)162     public void configureViewPagerAndFooter(
163             ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex) {
164         this.mA11yMenuLayout = a11yMenuLayout;
165         mA11yMenuShortcutList = shortcutDataList;
166         initViewPager();
167         initChildPage();
168         if (mA11yMenuFooter == null) {
169             mA11yMenuFooter = new A11yMenuFooter(a11yMenuLayout, mFooterCallbacks);
170         }
171         mA11yMenuFooter.updateRightToLeftDirection(
172                 a11yMenuLayout.getResources().getConfiguration());
173         updateFooterState();
174         registerOnGlobalLayoutListener();
175         goToPage(pageIndex);
176     }
177 
178     /** Initializes viewPager and its adapter. */
initViewPager()179     private void initViewPager() {
180         mViewPager = mA11yMenuLayout.findViewById(R.id.view_pager);
181         mViewPagerAdapter = new ViewPagerAdapter(mService);
182         mViewPager.setOffscreenPageLimit(2);
183         mViewPager.setAdapter(mViewPagerAdapter);
184         mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
185         mViewPager.registerOnPageChangeCallback(
186                 new ViewPager2.OnPageChangeCallback() {
187                     @Override
188                     public void onPageSelected(int position) {
189                         updateFooterState();
190                     }
191                 });
192     }
193 
194     /** Creates child pages of viewPager by the length of shortcuts and initializes them. */
initChildPage()195     private void initChildPage() {
196         if (mA11yMenuShortcutList == null || mA11yMenuShortcutList.isEmpty()) {
197             return;
198         }
199 
200         if (!mGridPageList.isEmpty()) {
201             mGridPageList.clear();
202         }
203 
204         mViewPagerAdapter.set(GridViewParams.generateShortcutSubLists(
205                 GridViewParams.getGridItemCount(mService), mA11yMenuShortcutList));
206     }
207 
208     /** Updates footer's state by index of current page in view pager. */
updateFooterState()209     public void updateFooterState() {
210         int currentPage = mViewPager.getCurrentItem();
211         int lastPage = mViewPager.getAdapter().getItemCount() - 1;
212         mA11yMenuFooter.getPreviousPageBtn().setEnabled(currentPage > 0);
213         mA11yMenuFooter.getNextPageBtn().setEnabled(currentPage < lastPage);
214     }
215 
216     private void goToPage(int pageIndex) {
217         if (mViewPager == null) {
218             return;
219         }
220         if ((pageIndex >= 0) && (pageIndex < mViewPager.getAdapter().getItemCount())) {
221             mViewPager.setCurrentItem(pageIndex);
222         }
223     }
224 
225     /** Registers OnGlobalLayoutListener to adjust menu UI by running callback at first time. */
226     private void registerOnGlobalLayoutListener() {
227         mA11yMenuLayout
228                 .getViewTreeObserver()
229                 .addOnGlobalLayoutListener(
230                         new OnGlobalLayoutListener() {
231 
232                             boolean mIsFirstTime = true;
233 
234                             @Override
235                             public void onGlobalLayout() {
236                                 if (!mIsFirstTime) {
237                                     return;
238                                 }
239 
240                                 if (mViewPagerAdapter.getItemCount() == 0) {
241                                     return;
242                                 }
243 
244                                 RecyclerView.ViewHolder viewHolder =
245                                         ((RecyclerView) mViewPager.getChildAt(0))
246                                                 .findViewHolderForAdapterPosition(0);
247                                 if (viewHolder == null) {
248                                     return;
249                                 }
250                                 GridView firstGridView = (GridView) viewHolder.itemView;
251                                 if (firstGridView == null
252                                         || firstGridView.getChildAt(0) == null) {
253                                     return;
254                                 }
255 
256                                 mIsFirstTime = false;
257 
258                                 int gridItemHeight = firstGridView.getChildAt(0)
259                                                 .getMeasuredHeight();
260                                 adjustMenuUISize(gridItemHeight);
261                             }
262                         });
263     }
264 
265     /**
266      * Adjusts menu UI to fit both landscape and portrait mode.
267      *
268      * <ol>
269      *   <li>Adjust view pager's height.
270      *   <li>Adjust vertical interval between grid items.
271      *   <li>Adjust padding in view pager.
272      * </ol>
273      */
274     private void adjustMenuUISize(int gridItemHeight) {
275         final int rowsInGridView = GridViewParams.getGridRowCount(mService);
276         final int defaultMargin =
277                 (int) mService.getResources().getDimension(R.dimen.a11ymenu_layout_margin);
278         final int topMargin = (int) mService.getResources().getDimension(R.dimen.table_margin_top);
279         final int displayMode = mService.getResources().getConfiguration().orientation;
280         int viewPagerHeight = mViewPager.getMeasuredHeight();
281 
282         if (displayMode == Configuration.ORIENTATION_PORTRAIT) {
283             // In portrait mode, we only need to adjust view pager's height to match its
284             // child's height.
285             viewPagerHeight = gridItemHeight * rowsInGridView + defaultMargin + topMargin;
286         } else if (displayMode == Configuration.ORIENTATION_LANDSCAPE) {
287             // In landscape mode, we need to adjust view pager's height to match screen height
288             // and adjust its child too,
289             // because a11y menu layout height is limited by the screen height.
290             DisplayMetrics displayMetrics = mService.getResources().getDisplayMetrics();
291             float densityScale = (float) displayMetrics.densityDpi
292                     / DisplayMetrics.DENSITY_DEVICE_STABLE;
293             // Keeps footer window height unchanged no matter the density is changed.
294             mA11yMenuFooter.adjustFooterToDensityScale(densityScale);
295             // Adjust the view pager height for system bar and display cutout insets.
296             WindowManager windowManager = WindowManagerUtils
297                     .getWindowManager(mA11yMenuLayout.getContext());
298             WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics();
299             Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility(
300                     WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
301             viewPagerHeight =
302                     windowMetric.getBounds().height()
303                             - mA11yMenuFooter.getHeight()
304                             - windowInsets.bottom;
305             // Sets vertical interval between grid items.
306             int interval =
307                     (viewPagerHeight - topMargin - defaultMargin
308                             - (rowsInGridView * gridItemHeight))
309                             / (rowsInGridView + 1);
310             // The interval is negative number when the viewPagerHeight is not able to fit
311             // the grid items, which result in text overlapping.
312             // Adjust the interval to 0 could solve the issue.
313             interval = Math.max(interval, 0);
314             mViewPagerAdapter.setVerticalSpacing(interval);
315 
316             // Sets padding to view pager.
317             final int finalMarginTop = interval + topMargin;
318             mViewPager.setPadding(0, finalMarginTop, 0, defaultMargin);
319         }
320         final ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
321         layoutParams.height = viewPagerHeight;
322         mViewPager.setLayoutParams(layoutParams);
323     }
324 
325     /** Callback object to handle click events from A11yMenuFooter */
326     protected A11yMenuFooterCallBack mFooterCallbacks =
327             new A11yMenuFooterCallBack() {
328                 @Override
329                 public void onPreviousButtonClicked() {
330                     // Moves to previous page.
331                     int targetPage = mViewPager.getCurrentItem() - 1;
332                     goToPage(targetPage);
333                     updateFooterState();
334                 }
335 
336                 @Override
337                 public void onNextButtonClicked() {
338                     // Moves to next page.
339                     int targetPage = mViewPager.getCurrentItem() + 1;
340                     goToPage(targetPage);
341                     updateFooterState();
342                 }
343             };
344 }
345