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