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